docs: rename docs/ -> serverAppConfigs-docs/ per system-wide PROJECTNAME-docs convention
All in-repo path references (AGENTS.md, README, old scripts, report artifacts,
test code paths) rewritten in the same commit; focused-test map extended for
.gitignore, old/scripts/*, dankclaw/comfyui/*, and report/* per this repo's
own convention ('add one before committing'). Obsidian wikilinks are
title-based and unaffected.
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
# pi subagent artifacts
|
# pi subagent artifacts
|
||||||
.pi-subagents/
|
.pi-subagents/
|
||||||
|
|
||||||
# doc-audit scratch (transient, synthesized into docs/ then trashed)
|
# doc-audit scratch (transient, synthesized into serverAppConfigs-docs/ then trashed)
|
||||||
.docaudit/
|
.docaudit/
|
||||||
|
|
||||||
# Agent trash
|
# Agent trash
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Dankserver switched from ik_llama.cpp Ornith CRACK Q8_0 to stock llama.cpp
|
|||||||
Tiel-Coder-35B (qwen35moe hybrid MoE), and Dankclaw switched from the Q6_0
|
Tiel-Coder-35B (qwen35moe hybrid MoE), and Dankclaw switched from the Q6_0
|
||||||
AGENT model to the Flash-Next 56B MoE (qwen4exp, PR #27742 merged into
|
AGENT model to the Flash-Next 56B MoE (qwen4exp, PR #27742 merged into
|
||||||
charlie's ROCmFPX fork, --fit off, 128k context). Runbooks:
|
charlie's ROCmFPX fork, --fit off, 128k context). Runbooks:
|
||||||
[docs/LLM/index.md](docs/LLM/index.md).
|
[serverAppConfigs-docs/LLM/index.md](serverAppConfigs-docs/LLM/index.md).
|
||||||
Retired scripts live in `old/` (reference only) and `.AGENTTRASH/`.
|
Retired scripts live in `old/` (reference only) and `.AGENTTRASH/`.
|
||||||
|
|
||||||
## Repository Layout
|
## Repository Layout
|
||||||
@@ -53,30 +53,30 @@ Retired scripts live in `old/` (reference only) and `.AGENTTRASH/`.
|
|||||||
(service-scoped deployer), LLM serve scripts, systemd units, env tunables,
|
(service-scoped deployer), LLM serve scripts, systemd units, env tunables,
|
||||||
and bench harnesses
|
and bench harnesses
|
||||||
- `old/` — retired PP/NPU-era LLM scripts (reference only)
|
- `old/` — retired PP/NPU-era LLM scripts (reference only)
|
||||||
- `docs/` — knowledge base (Obsidian vault). `docs/LLM/` is the LLM fleet
|
- `serverAppConfigs-docs/` — knowledge base (Obsidian vault). `serverAppConfigs-docs/LLM/` is the LLM fleet
|
||||||
- `tests/` — bats test suite
|
- `tests/` — bats test suite
|
||||||
|
|
||||||
## Documentation Authoring
|
## Documentation Authoring
|
||||||
|
|
||||||
The `docs/` vault is the project knowledge base (Obsidian, wikilinks) and is an
|
The `serverAppConfigs-docs/` vault is the project knowledge base (Obsidian, wikilinks) and is an
|
||||||
OKF (Open Knowledge Format) knowledge bundle. Follow the OKF spec for every doc
|
OKF (Open Knowledge Format) knowledge bundle. Follow the OKF spec for every doc
|
||||||
you create or edit.
|
you create or edit.
|
||||||
|
|
||||||
- **Canonical spec:** `~/.pi/agent/docs/references/okf-spec.md` — the OKF v0.1
|
- **Canonical spec:** `~/.pi/agent/docs/references/okf-spec.md` — the OKF v0.1
|
||||||
spec in the **system-config knowledge base** at `~/.pi/agent/docs`. The repo's
|
spec in the **system-config knowledge base** at `~/.pi/agent/docs`. The repo's
|
||||||
own `docs/references/okf-spec.md` is deprecated/removed; use the system one. The
|
own `serverAppConfigs-docs/references/okf-spec.md` is deprecated/removed; use the system one. The
|
||||||
spec index and maintenance guide are
|
spec index and maintenance guide are
|
||||||
`~/.pi/agent/docs/references/index.md` and
|
`~/.pi/agent/docs/references/index.md` and
|
||||||
`~/.pi/agent/docs/references/okf-maintenance.md`.
|
`~/.pi/agent/docs/references/okf-maintenance.md`.
|
||||||
- **Read before you touch:** open the relevant `index.md` first
|
- **Read before you touch:** open the relevant `index.md` first
|
||||||
(`docs/index.md` → `docs/LLM/index.md`). Read the whole target file before
|
(`serverAppConfigs-docs/index.md` → `serverAppConfigs-docs/LLM/index.md`). Read the whole target file before
|
||||||
editing it.
|
editing it.
|
||||||
- **One concept per file**, with YAML frontmatter that has a non-empty `type:`
|
- **One concept per file**, with YAML frontmatter that has a non-empty `type:`
|
||||||
field (`reference` / `guide` / `configuration` / `research`) plus `description`,
|
field (`reference` / `guide` / `configuration` / `research`) plus `description`,
|
||||||
`tags`, and `timestamp`. Split broad topics; keep pages ~1–500 lines.
|
`tags`, and `timestamp`. Split broad topics; keep pages ~1–500 lines.
|
||||||
- **Progressive disclosure:** an `index.md` at each level links its children.
|
- **Progressive disclosure:** an `index.md` at each level links its children.
|
||||||
Every `docs/**/*.md` (except `index.md`) must be linked from
|
Every `serverAppConfigs-docs/**/*.md` (except `index.md`) must be linked from
|
||||||
`docs/index.md` with `[[Title]]` — the docs-completeness test checks this.
|
`serverAppConfigs-docs/index.md` with `[[Title]]` — the docs-completeness test checks this.
|
||||||
- **Cross-link** siblings as `[[Title]]` wikilinks; cite external claims under a
|
- **Cross-link** siblings as `[[Title]]` wikilinks; cite external claims under a
|
||||||
`# Citations` section.
|
`# Citations` section.
|
||||||
- **Always update docs** when a task changes behavior, then run
|
- **Always update docs** when a task changes behavior, then run
|
||||||
@@ -183,7 +183,7 @@ Test seams: compose validity, env consistency (no leaked secrets), Caddyfile
|
|||||||
validity, quadlet units, docs completeness, script syntax (all of `scripts/`,
|
validity, quadlet units, docs completeness, script syntax (all of `scripts/`,
|
||||||
including the LLM serve scripts), LLM unit integrity (units point at files in
|
including the LLM serve scripts), LLM unit integrity (units point at files in
|
||||||
this repo), LLM secret hygiene, deployment flow, and service-scoped deployment
|
this repo), LLM secret hygiene, deployment flow, and service-scoped deployment
|
||||||
planning/dispatch. See [docs/Architecture/Testing Policy.md](docs/Architecture/Testing%20Policy.md).
|
planning/dispatch. See [serverAppConfigs-docs/Architecture/Testing Policy.md](serverAppConfigs-docs/Architecture/Testing%20Policy.md).
|
||||||
|
|
||||||
## Secret Rules
|
## Secret Rules
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ on qwen4exp hyper-connection config), and reasoning on (Sharp template, budget
|
|||||||
**Serving status (2026-09-01):** deployed the Flash-Next 56B MoE model. The
|
**Serving status (2026-09-01):** deployed the Flash-Next 56B MoE model. The
|
||||||
previous kingjones Q6_0 AGENT model (27B, standalone MTP, 262144 context) is
|
previous kingjones Q6_0 AGENT model (27B, standalone MTP, 262144 context) is
|
||||||
retired. The Q8_0 AGENT experiment is documented in
|
retired. The Q8_0 AGENT experiment is documented in
|
||||||
`docs/LLM/kingjones-q8agent-rocmfpx.md`; it was retired because it crashed in
|
`serverAppConfigs-docs/LLM/kingjones-q8agent-rocmfpx.md`; it was retired because it crashed in
|
||||||
`ggml_compute_forward_set_rows` and did not provide a working service.
|
`ggml_compute_forward_set_rows` and did not provide a working service.
|
||||||
|
|
||||||
### Tiel-Coder-35B-A3B llama.cpp (Dankserver, ACTIVE)
|
### Tiel-Coder-35B-A3B llama.cpp (Dankserver, ACTIVE)
|
||||||
@@ -325,7 +325,7 @@ ubatch bench on 8k context), f16 KV, full 262144 context / 1 slot, TEMPERATURE
|
|||||||
0.7 / TOP_P 0.95, reasoning on (deepseek, budget -1), `--no-mmap`. Two-file
|
0.7 / TOP_P 0.95, reasoning on (deepseek, budget -1), `--no-mmap`. Two-file
|
||||||
payload (~14.4 GiB) in `/home/dank/models/eemin-Qwen3.8-27B-ROCMI4-MTP/`. Full
|
payload (~14.4 GiB) in `/home/dank/models/eemin-Qwen3.8-27B-ROCMI4-MTP/`. Full
|
||||||
runbook:
|
runbook:
|
||||||
[docs/LLM/eemin-rocmi4-mtp.md](docs/LLM/eemin-rocmi4-mtp.md).
|
[serverAppConfigs-docs/LLM/eemin-rocmi4-mtp.md](serverAppConfigs-docs/LLM/eemin-rocmi4-mtp.md).
|
||||||
|
|
||||||
|
|
||||||
### Kairic Edge llama.cpp (Dankclaw, RETIRED experiment 2026-08-22)
|
### Kairic Edge llama.cpp (Dankclaw, RETIRED experiment 2026-08-22)
|
||||||
@@ -339,7 +339,7 @@ verification shapes through the RDNA 3.5 native IU4 matrix instruction.
|
|||||||
`config.h.in`. The release script and guide never generate
|
`config.h.in`. The release script and guide never generate
|
||||||
`third_party/composable_kernel/include/ck/config.h`. Generate it via a CK
|
`third_party/composable_kernel/include/ck/config.h`. Generate it via a CK
|
||||||
CMake configure and copy it in before building (full runbook in
|
CMake configure and copy it in before building (full runbook in
|
||||||
[docs/LLM/kairic-edge.md](docs/LLM/kairic-edge.md)). The file is untracked
|
[serverAppConfigs-docs/LLM/kairic-edge.md](serverAppConfigs-docs/LLM/kairic-edge.md)). The file is untracked
|
||||||
in CK git. A `git clean` removes it. Local rebuilds are byte-identical
|
in CK git. A `git clean` removes it. Local rebuilds are byte-identical
|
||||||
(`d7625a0c...`).
|
(`d7625a0c...`).
|
||||||
|
|
||||||
@@ -360,7 +360,7 @@ Restored to v3 on 2026-08-22 (commit `c60156f`).
|
|||||||
A DFlash2 Vulkan deployment briefly replaced CIRU on 2026-08-21, then was
|
A DFlash2 Vulkan deployment briefly replaced CIRU on 2026-08-21, then was
|
||||||
reverted the same day for speed. Its serve script, env, and the build
|
reverted the same day for speed. Its serve script, env, and the build
|
||||||
worktree are archived in `.AGENTTRASH/` and documented in
|
worktree are archived in `.AGENTTRASH/` and documented in
|
||||||
[docs/LLM/dflash2-large-vulkan.md](docs/LLM/dflash2-large-vulkan.md).
|
[serverAppConfigs-docs/LLM/dflash2-large-vulkan.md](serverAppConfigs-docs/LLM/dflash2-large-vulkan.md).
|
||||||
|
|
||||||
### FastMTP patched llama.cpp (Dankserver, retired 2026-08-18)
|
### FastMTP patched llama.cpp (Dankserver, retired 2026-08-18)
|
||||||
|
|
||||||
@@ -442,17 +442,17 @@ graph TD
|
|||||||
(EOS or limit) logs only `slot release` with `truncated = 0`. The server
|
(EOS or limit) logs only `slot release` with `truncated = 0`. The server
|
||||||
never preempts a running generation. When all slots are busy, new tasks
|
never preempts a running generation. When all slots are busy, new tasks
|
||||||
defer into a queue. See the 2026-08-13 incident in
|
defer into a queue. See the 2026-08-13 incident in
|
||||||
[docs/LLM/deployment.md](docs/LLM/deployment.md).
|
[serverAppConfigs-docs/LLM/deployment.md](serverAppConfigs-docs/LLM/deployment.md).
|
||||||
- ComfyUI on Dankclaw runs CPU-only (`--cpu`). ROCm wheels do not support
|
- ComfyUI on Dankclaw runs CPU-only (`--cpu`). ROCm wheels do not support
|
||||||
gfx1151. Do not use CUDA images.
|
gfx1151. Do not use CUDA images.
|
||||||
- SearXNG outbound traffic must stay on the Gluetun tunnel. Do not move
|
- SearXNG outbound traffic must stay on the Gluetun tunnel. Do not move
|
||||||
its ports off the gluetun service.
|
its ports off the gluetun service.
|
||||||
- Retired hardware history: the Dankserver 35B cuBLAS crash and the DFlash
|
- Retired hardware history: the Dankserver 35B cuBLAS crash and the DFlash
|
||||||
premature-EOS bug are historical. See
|
premature-EOS bug are historical. See
|
||||||
[docs/LLM/dankserver-vram-growth.md](docs/LLM/dankserver-vram-growth.md)
|
[serverAppConfigs-docs/LLM/dankserver-vram-growth.md](serverAppConfigs-docs/LLM/dankserver-vram-growth.md)
|
||||||
and [docs/LLM/qwen3.8-spec-matrix.md](docs/LLM/qwen3.8-spec-matrix.md).
|
and [serverAppConfigs-docs/LLM/qwen3.8-spec-matrix.md](serverAppConfigs-docs/LLM/qwen3.8-spec-matrix.md).
|
||||||
The FastFlowLM NPU endpoint is retired. See
|
The FastFlowLM NPU endpoint is retired. See
|
||||||
[docs/LLM/FastFlowLM NPU Inference.md](docs/LLM/FastFlowLM%20NPU%20Inference.md).
|
[serverAppConfigs-docs/LLM/FastFlowLM NPU Inference.md](serverAppConfigs-docs/LLM/FastFlowLM%20NPU%20Inference.md).
|
||||||
|
|
||||||
### Dankclaw memory model
|
### Dankclaw memory model
|
||||||
|
|
||||||
@@ -478,7 +478,7 @@ two hosts, one Caddy, and one Gitea. One repo keeps them in one history.
|
|||||||
| Editing a compose/Caddyfile/serve script | **Work through git** — fetch, commit, edit, test, push, pull, deploy. Never edit on the remote directly. |
|
| Editing a compose/Caddyfile/serve script | **Work through git** — fetch, commit, edit, test, push, pull, deploy. Never edit on the remote directly. |
|
||||||
| Config exists only on remote | **Pull it into the repo first** — commit the fetched version (redacted) before making changes. |
|
| Config exists only on remote | **Pull it into the repo first** — commit the fetched version (redacted) before making changes. |
|
||||||
| Unstaged local changes | **Commit or discard** — check `git status` before starting work. Ask the user if unsure. |
|
| Unstaged local changes | **Commit or discard** — check `git status` before starting work. Ask the user if unsure. |
|
||||||
| Deploying a committed change | **Run `scripts/deploy.sh <target>`** — it pushes, pulls with fast-forward-only, plans changed paths, dispatches selected services, and checks their health. See [docs/LLM/deployment.md](docs/LLM/deployment.md). | | Container without a Quadlet unit | **Give it one** — self-healing, boot-persistent `.container` unit via git (see Boot Persistence & Self-Healing). |
|
| Deploying a committed change | **Run `scripts/deploy.sh <target>`** — it pushes, pulls with fast-forward-only, plans changed paths, dispatches selected services, and checks their health. See [serverAppConfigs-docs/LLM/deployment.md](serverAppConfigs-docs/LLM/deployment.md). | | Container without a Quadlet unit | **Give it one** — self-healing, boot-persistent `.container` unit via git (see Boot Persistence & Self-Healing). |
|
||||||
| Benchmarking | **Stop the service first**, restart, verify health. See the Benchmarking Policy above. |
|
| Benchmarking | **Stop the service first**, restart, verify health. See the Benchmarking Policy above. |
|
||||||
| Deleting a file | **Move it to trash** — `.AGENTTRASH/` (in-repo) or `~/.AGENTTRASH/YYYYMMDD-topic`. Never `rm`. |
|
| Deleting a file | **Move it to trash** — `.AGENTTRASH/` (in-repo) or `~/.AGENTTRASH/YYYYMMDD-topic`. Never `rm`. |
|
||||||
| Tests exist | **Run them** — verify tests pass after every change. Do not commit failing tests. |
|
| Tests exist | **Run them** — verify tests pass after every change. Do not commit failing tests. |
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ one deploy script.
|
|||||||
- `shared/` — homepage, prometheus config, deploy helpers
|
- `shared/` — homepage, prometheus config, deploy helpers
|
||||||
- `scripts/` — `deploy.sh` and `deploy-remote.sh` (service-scoped deployment), the planner, LLM serve scripts, systemd units, env tunables, and bench harnesses
|
- `scripts/` — `deploy.sh` and `deploy-remote.sh` (service-scoped deployment), the planner, LLM serve scripts, systemd units, env tunables, and bench harnesses
|
||||||
- `old/` — retired PP/NPU-era LLM scripts (reference only)
|
- `old/` — retired PP/NPU-era LLM scripts (reference only)
|
||||||
- `docs/` — project knowledge base (Obsidian vault, wikilinks). `docs/LLM/` is the LLM fleet
|
- `serverAppConfigs-docs/` — project knowledge base (Obsidian vault, wikilinks). `serverAppConfigs-docs/LLM/` is the LLM fleet
|
||||||
- `tests/` — bats test suite (see [[Testing Policy]])
|
- `tests/` — bats test suite (see [[Testing Policy]])
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
See [docs/Architecture/Deployment Flow.md](docs/Architecture/Deployment%20Flow.md)
|
See [serverAppConfigs-docs/Architecture/Deployment Flow.md](serverAppConfigs-docs/Architecture/Deployment%20Flow.md)
|
||||||
and [docs/LLM/deployment.md](docs/LLM/deployment.md). Run
|
and [serverAppConfigs-docs/LLM/deployment.md](serverAppConfigs-docs/LLM/deployment.md). Run
|
||||||
`./scripts/deploy.sh <dankclaw|dankserver>` after committing changes. It
|
`./scripts/deploy.sh <dankclaw|dankserver>` after committing changes. It
|
||||||
plans and deploys only the changed services. Use `--service NAME` for an
|
plans and deploys only the changed services. Use `--service NAME` for an
|
||||||
explicit single-service retry; inference is restarted only when its own files
|
explicit single-service retry; inference is restarted only when its own files
|
||||||
@@ -53,11 +53,11 @@ explicit single-service retry; inference is restarted only when its own files
|
|||||||
|
|
||||||
## Knowledge Base
|
## Knowledge Base
|
||||||
|
|
||||||
The `docs/` directory is an Obsidian vault. Start at
|
The `serverAppConfigs-docs/` directory is an Obsidian vault. Start at
|
||||||
[docs/index.md](docs/index.md). The LLM fleet knowledge base lives at
|
[serverAppConfigs-docs/index.md](serverAppConfigs-docs/index.md). The LLM fleet knowledge base lives at
|
||||||
[docs/LLM/index.md](docs/LLM/index.md).
|
[serverAppConfigs-docs/LLM/index.md](serverAppConfigs-docs/LLM/index.md).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run `./tests/run.sh` before every push. See
|
Run `./tests/run.sh` before every push. See
|
||||||
[docs/Architecture/Testing Policy.md](docs/Architecture/Testing%20Policy.md).
|
[serverAppConfigs-docs/Architecture/Testing Policy.md](serverAppConfigs-docs/Architecture/Testing%20Policy.md).
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
# published LAN port (quadlet services on isolated networks)
|
# published LAN port (quadlet services on isolated networks)
|
||||||
# - dankserver services (192.168.0.235): by published LAN port
|
# - dankserver services (192.168.0.235): by published LAN port
|
||||||
#
|
#
|
||||||
# Naming: flat franksdank.cloud scheme (see docs/Infrastructure/LAN-DNS.md).
|
# Naming: flat franksdank.cloud scheme (see serverAppConfigs-docs/Infrastructure/LAN-DNS.md).
|
||||||
# DNS for these names is served by the Pi-hole quadlet on this host
|
# DNS for these names is served by the Pi-hole quadlet on this host
|
||||||
# (dankclaw/pihole/addn-hosts — every name resolves to 192.168.0.143)
|
# (dankclaw/pihole/addn-hosts — every name resolves to 192.168.0.143)
|
||||||
# and handed to LAN clients by the SURFboard G36 router.
|
# and handed to LAN clients by the SURFboard G36 router.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# directly through the Porkbun API, so the wildcard cert for
|
# directly through the Porkbun API, so the wildcard cert for
|
||||||
# *.franksdank.cloud / *.franksdank.casa can be issued and renewed without
|
# *.franksdank.cloud / *.franksdank.casa can be issued and renewed without
|
||||||
# any port 80/443 reachability from the internet (see the Caddyfile in
|
# any port 80/443 reachability from the internet (see the Caddyfile in
|
||||||
# ../Caddyfile and docs/Infrastructure/LAN-DNS.md).
|
# ../Caddyfile and serverAppConfigs-docs/Infrastructure/LAN-DNS.md).
|
||||||
#
|
#
|
||||||
# Build on dankclaw (podman rootless, same as every other container here):
|
# Build on dankclaw (podman rootless, same as every other container here):
|
||||||
# podman build -t franksdank/caddy-porkbun:latest -f dankclaw/caddy/Containerfile .
|
# podman build -t franksdank/caddy-porkbun:latest -f dankclaw/caddy/Containerfile .
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
# back: rootless Podman has no boot-time auto-start for bare containers, and
|
# back: rootless Podman has no boot-time auto-start for bare containers, and
|
||||||
# `restart: unless-stopped` does not survive the explicit stop that shutdown
|
# `restart: unless-stopped` does not survive the explicit stop that shutdown
|
||||||
# performs. See AGENTS.md "Boot Persistence & Self-Healing" and
|
# performs. See AGENTS.md "Boot Persistence & Self-Healing" and
|
||||||
# docs/Infrastructure/Caddy.md.
|
# serverAppConfigs-docs/Infrastructure/Caddy.md.
|
||||||
#
|
#
|
||||||
# It joins the existing compose network (dankclaw_default) so the Caddyfile's
|
# It joins the existing compose network (dankclaw_default) so the Caddyfile's
|
||||||
# container-name upstreams (authentik-server, grafana, ...) still resolve; if
|
# container-name upstreams (authentik-server, grafana, ...) still resolve; if
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#
|
#
|
||||||
# The base is heavier than a dedicated ComfyUI image (it carries vLLM), but it
|
# The base is heavier than a dedicated ComfyUI image (it carries vLLM), but it
|
||||||
# is the only prebuilt image that ships gfx1151 torch kernels. See
|
# is the only prebuilt image that ships gfx1151 torch kernels. See
|
||||||
# docs/Research/ComfyUI ROCm and Vulkan.md.
|
# serverAppConfigs-docs/Research/ComfyUI ROCm and Vulkan.md.
|
||||||
|
|
||||||
FROM docker.io/rocm/vllm:rocm7.12.0_gfx1151_ubuntu24.04_py3.12_pytorch_2.9.1_vllm_0.16.0
|
FROM docker.io/rocm/vllm:rocm7.12.0_gfx1151_ubuntu24.04_py3.12_pytorch_2.9.1_vllm_0.16.0
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# Unit name: gitea-postgres.service (podman 5.x drops the container- prefix)
|
# Unit name: gitea-postgres.service (podman 5.x drops the container- prefix)
|
||||||
#
|
#
|
||||||
# The DB password lives in /opt/gitea/.env on the host (mode 0600).
|
# The DB password lives in /opt/gitea/.env on the host (mode 0600).
|
||||||
# This file holds no secrets. See docs/Dankclaw/Gitea.md.
|
# This file holds no secrets. See serverAppConfigs-docs/Dankclaw/Gitea.md.
|
||||||
# The [Service] EnvironmentFile= line lets systemd expand
|
# The [Service] EnvironmentFile= line lets systemd expand
|
||||||
# ${GITEA_DB_PASSWORD} in the generated ExecStart.
|
# ${GITEA_DB_PASSWORD} in the generated ExecStart.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# The [Service] EnvironmentFile= line lets systemd expand
|
# The [Service] EnvironmentFile= line lets systemd expand
|
||||||
# ${GITEA_DB_PASSWORD} in the generated ExecStart.
|
# ${GITEA_DB_PASSWORD} in the generated ExecStart.
|
||||||
# The DB password lives in /opt/gitea/.env on the host (mode 0600).
|
# The DB password lives in /opt/gitea/.env on the host (mode 0600).
|
||||||
# This file holds no secrets. See docs/Dankclaw/Gitea.md.
|
# This file holds no secrets. See serverAppConfigs-docs/Dankclaw/Gitea.md.
|
||||||
#
|
#
|
||||||
# Ports (live): 3000 = HTTP UI/API, 2222 = git SSH. The docs table in
|
# Ports (live): 3000 = HTTP UI/API, 2222 = git SSH. The docs table in
|
||||||
# Dankclaw Stack.md historically said 3001; the live host port is 3000.
|
# Dankclaw Stack.md historically said 3001; the live host port is 3000.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# its OpenAI connection at the live llama.cpp backend on this host via the
|
# its OpenAI connection at the live llama.cpp backend on this host via the
|
||||||
# host LAN IP (192.168.0.143:7770, model frank-default-large). It publishes 3005
|
# host LAN IP (192.168.0.143:7770, model frank-default-large). It publishes 3005
|
||||||
# (3000 is Gitea's HTTP port). Data persists under
|
# (3000 is Gitea's HTTP port). Data persists under
|
||||||
# /home/dank/Containers/open-webui/data. See docs/Dankclaw/OpenWebUI.md.
|
# /home/dank/Containers/open-webui/data. See serverAppConfigs-docs/Dankclaw/OpenWebUI.md.
|
||||||
#
|
#
|
||||||
# Network history (2026-08-22): this unit used Network=slirp4netns. When the
|
# Network history (2026-08-22): this unit used Network=slirp4netns. When the
|
||||||
# slirp4netns helper process died, the container kept running but lost its tap
|
# slirp4netns helper process died, the container kept running but lost its tap
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
# Runs on slirp4netns with port 53 (DNS) and 8083 (web UI) published to the
|
# Runs on slirp4netns with port 53 (DNS) and 8083 (web UI) published to the
|
||||||
# LAN (the same pattern as open-webui). Config + state mount from
|
# LAN (the same pattern as open-webui). Config + state mount from
|
||||||
# /home/dank/Containers/pihole. The web UI password is WEBPASSWORD in the env
|
# /home/dank/Containers/pihole. The web UI password is WEBPASSWORD in the env
|
||||||
# file. See docs/Infrastructure/LAN-DNS.md.
|
# file. See serverAppConfigs-docs/Infrastructure/LAN-DNS.md.
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Pi-hole LAN DNS container
|
Description=Pi-hole LAN DNS container
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# Creates the WireGuard tunnel to ProtonVPN (US-WA). SearXNG reuses this
|
# Creates the WireGuard tunnel to ProtonVPN (US-WA). SearXNG reuses this
|
||||||
# container's network namespace (see searxng.container), so its outbound search
|
# container's network namespace (see searxng.container), so its outbound search
|
||||||
# traffic exits here. The WireGuard credentials live in /opt/searxng/.env
|
# traffic exits here. The WireGuard credentials live in /opt/searxng/.env
|
||||||
# (mode 0600); this file holds no secrets. See docs/Dankclaw/Gluetun.md.
|
# (mode 0600); this file holds no secrets. See serverAppConfigs-docs/Dankclaw/Gluetun.md.
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Gluetun VPN tunnel container
|
Description=Gluetun VPN tunnel container
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# The limiter's rate-limit state store. Joins the searxng network and is
|
# The limiter's rate-limit state store. Joins the searxng network and is
|
||||||
# reachable by the name "valkey" (settings.yml points there). Data persists in
|
# reachable by the name "valkey" (settings.yml points there). Data persists in
|
||||||
# /opt/searxng/valkey. See docs/Dankclaw/Valkey.md.
|
# /opt/searxng/valkey. See serverAppConfigs-docs/Dankclaw/Valkey.md.
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=SearXNG Valkey cache container
|
Description=SearXNG Valkey cache container
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# Reuses gluetun's network namespace (Network=gluetun.container) so its
|
# Reuses gluetun's network namespace (Network=gluetun.container) so its
|
||||||
# outbound traffic exits the WireGuard tunnel. It publishes no port of its own;
|
# outbound traffic exits the WireGuard tunnel. It publishes no port of its own;
|
||||||
# the port lives on gluetun. It reaches valkey by name on the searxng network.
|
# the port lives on gluetun. It reaches valkey by name on the searxng network.
|
||||||
# Config mounts from /opt/searxng/config. See docs/Dankclaw/SearXNG.md.
|
# Config mounts from /opt/searxng/config. See serverAppConfigs-docs/Dankclaw/SearXNG.md.
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=SearXNG search engine container
|
Description=SearXNG search engine container
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# Prefer the passwordless restart helpers installed on each target
|
# Prefer the passwordless restart helpers installed on each target
|
||||||
# (/usr/local/sbin/frank-restart-<large|small>.sh); they cover the unit
|
# (/usr/local/sbin/frank-restart-<large|small>.sh); they cover the unit
|
||||||
# symlink, cache drop (dankclaw), daemon-reload, and restart in one root
|
# symlink, cache drop (dankclaw), daemon-reload, and restart in one root
|
||||||
# script. See docs/deployment.md -> Passwordless restart.
|
# script. See serverAppConfigs-docs/deployment.md -> Passwordless restart.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./deploy.sh <target> [--dry-run]
|
# ./deploy.sh <target> [--dry-run]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Tunables for frank-default-small (FastFlowLM NPU endpoint, dankclaw)
|
# Tunables for frank-default-small (FastFlowLM NPU endpoint, dankclaw)
|
||||||
# Runtime: FLM 1.0.1 on the XDNA2 NPU (50 TOPS). Model choice, measured
|
# Runtime: FLM 1.0.1 on the XDNA2 NPU (50 TOPS). Model choice, measured
|
||||||
# results, and the NPU memory budget are in docs/FastFlowLM NPU Inference.md.
|
# results, and the NPU memory budget are in serverAppConfigs-docs/FastFlowLM NPU Inference.md.
|
||||||
# The NPU allocates from the unified pool shared with frank-default-large
|
# The NPU allocates from the unified pool shared with frank-default-large
|
||||||
# (the PP head). Keep the resident footprint small enough that the head
|
# (the PP head). Keep the resident footprint small enough that the head
|
||||||
# keeps its headroom.
|
# keeps its headroom.
|
||||||
@@ -15,7 +15,7 @@ FLM_MODEL=qwythos:9b
|
|||||||
FLM_HOST=0.0.0.0
|
FLM_HOST=0.0.0.0
|
||||||
FLM_PORT=52625
|
FLM_PORT=52625
|
||||||
# Power mode: turbo. Smoke decode ~5-7 t/s on the 9B dense at 262k ctx
|
# Power mode: turbo. Smoke decode ~5-7 t/s on the 9B dense at 262k ctx
|
||||||
# (not benchmarked; see docs/FastFlowLM NPU Inference.md).
|
# (not benchmarked; see serverAppConfigs-docs/FastFlowLM NPU Inference.md).
|
||||||
FLM_PMODE=turbo
|
FLM_PMODE=turbo
|
||||||
# Load the ASR model (Whisper-V3-Turbo-NPU2) alongside the LLM. Exposes
|
# Load the ASR model (Whisper-V3-Turbo-NPU2) alongside the LLM. Exposes
|
||||||
# /v1/audio/transcriptions. Adds ~1 GiB to the resident footprint.
|
# /v1/audio/transcriptions. Adds ~1 GiB to the resident footprint.
|
||||||
@@ -28,6 +28,6 @@ FLM_CTX_LEN=262144
|
|||||||
# Custom-model resolution: the qwythos:9b tag and its xclbin symlinks live
|
# Custom-model resolution: the qwythos:9b tag and its xclbin symlinks live
|
||||||
# in the user config, not the system registry. Without these, flm treats
|
# in the user config, not the system registry. Without these, flm treats
|
||||||
# the tag as unknown and silently serves llama3.2:1b (see the unknown-tag
|
# the tag as unknown and silently serves llama3.2:1b (see the unknown-tag
|
||||||
# fallback in docs/FastFlowLM NPU Inference.md).
|
# fallback in serverAppConfigs-docs/FastFlowLM NPU Inference.md).
|
||||||
FLM_CONFIG_PATH=/home/dank/.config/flm/model_list.json
|
FLM_CONFIG_PATH=/home/dank/.config/flm/model_list.json
|
||||||
FLM_XCLBIN_PATH=/home/dank/.config/flm
|
FLM_XCLBIN_PATH=/home/dank/.config/flm
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#
|
#
|
||||||
# WARNING: do not name any variable here TEMP, HOST, or GPU_MAX_HEAP_SIZE.
|
# WARNING: do not name any variable here TEMP, HOST, or GPU_MAX_HEAP_SIZE.
|
||||||
# The ROCm HIP runtime reads those bare env vars (see the 2026-08-13 TEMP
|
# The ROCm HIP runtime reads those bare env vars (see the 2026-08-13 TEMP
|
||||||
# incident in docs/deployment.md).
|
# incident in serverAppConfigs-docs/deployment.md).
|
||||||
|
|
||||||
# Bind address and port for ggml-rpc-server. Must be the LAN address the head
|
# Bind address and port for ggml-rpc-server. Must be the LAN address the head
|
||||||
# can reach; the default bind (127.0.0.1) refuses remote clients.
|
# can reach; the default bind (127.0.0.1) refuses remote clients.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# unbounded, the directory filled the root disk to 100% on 2026-08-17;
|
# unbounded, the directory filled the root disk to 100% on 2026-08-17;
|
||||||
# failed cache writes left 0-byte entries, and a later reload served
|
# failed cache writes left 0-byte entries, and a later reload served
|
||||||
# corrupt weights -> degenerate model output (see the cache incident in
|
# corrupt weights -> degenerate model output (see the cache incident in
|
||||||
# docs/Pipeline Parallelism/Qwen3.8 PP Deployment.md).
|
# serverAppConfigs-docs/Pipeline Parallelism/Qwen3.8 PP Deployment.md).
|
||||||
#
|
#
|
||||||
# This script prunes the cache to a byte cap, oldest files first. Cache
|
# This script prunes the cache to a byte cap, oldest files first. Cache
|
||||||
# entries are derived data: safe to delete. Run from cron on Dankserver.
|
# entries are derived data: safe to delete. Run from cron on Dankserver.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# (XDNA2, 50 TOPS) is a single-lane device: generations serialize in FIFO
|
# (XDNA2, 50 TOPS) is a single-lane device: generations serialize in FIFO
|
||||||
# order, so this is a background/triage endpoint, not an interactive
|
# order, so this is a background/triage endpoint, not an interactive
|
||||||
# multi-user chat backend. The NPU shares the 30 GiB unified pool with the
|
# multi-user chat backend. The NPU shares the 30 GiB unified pool with the
|
||||||
# frank-default-large PP head; see docs/FastFlowLM NPU Inference.md for the
|
# frank-default-large PP head; see serverAppConfigs-docs/FastFlowLM NPU Inference.md for the
|
||||||
# memory budget and the measured model matrix.
|
# memory budget and the measured model matrix.
|
||||||
#
|
#
|
||||||
# NOTE: FLM requires unlimited memlock. The unit sets LimitMEMLOCK=infinity.
|
# NOTE: FLM requires unlimited memlock. The unit sets LimitMEMLOCK=infinity.
|
||||||
|
|||||||
@@ -191,9 +191,9 @@ next answer Q6-Q43 in the morning; then build order is fixed
|
|||||||
<h2>What I changed (knowledge base)</h2>
|
<h2>What I changed (knowledge base)</h2>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>File</th><th>Change</th></tr>
|
<tr><th>File</th><th>Change</th></tr>
|
||||||
<tr><td><code>docs/Infrastructure/Homepage.md</code></td><td>Rewritten to the settled design: structure, status model, checker, auth, classification, layout. Added three mermaid diagrams (site structure, status states, checker flow).</td></tr>
|
<tr><td><code>serverAppConfigs-docs/Infrastructure/Homepage.md</code></td><td>Rewritten to the settled design: structure, status model, checker, auth, classification, layout. Added three mermaid diagrams (site structure, status states, checker flow).</td></tr>
|
||||||
<tr><td><code>docs/Infrastructure/Homepage Design Grilling.md</code></td><td>New. The decision log: settled Q1–Q5 + the 38 open questions (Q6–Q43) with recommendations + the build-order diagram.</td></tr>
|
<tr><td><code>serverAppConfigs-docs/Infrastructure/Homepage Design Grilling.md</code></td><td>New. The decision log: settled Q1–Q5 + the 38 open questions (Q6–Q43) with recommendations + the build-order diagram.</td></tr>
|
||||||
<tr><td><code>docs/index.md</code></td><td>Linked the new grilling page (satisfies the "index links every page" test).</td></tr>
|
<tr><td><code>serverAppConfigs-docs/index.md</code></td><td>Linked the new grilling page (satisfies the "index links every page" test).</td></tr>
|
||||||
<tr><td><code>~/.pi/agent/docs/log.md</code></td><td>Added a homepage-design bullet to the 2026-08-22 entry (design, baseline, threads).</td></tr>
|
<tr><td><code>~/.pi/agent/docs/log.md</code></td><td>Added a homepage-design bullet to the 2026-08-22 entry (design, baseline, threads).</td></tr>
|
||||||
<tr><td><code>report/homepage-grilling-2026-08-22.html</code></td><td>This report. Self-contained, terminal-TUI styled.</td></tr>
|
<tr><td><code>report/homepage-grilling-2026-08-22.html</code></td><td>This report. Self-contained, terminal-TUI styled.</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Executable
+192
@@ -0,0 +1,192 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Decode throughput benchmark for frank-default-large.
|
||||||
|
Starts the server with a given --threads-batch value, sends an 8K+ token prompt,
|
||||||
|
generates 256 tokens, measures generation tok/s.
|
||||||
|
|
||||||
|
Each --threads-batch value requires a cold server load (~3 min).
|
||||||
|
"""
|
||||||
|
import json, time, urllib.request, urllib.error, subprocess, os, sys
|
||||||
|
|
||||||
|
MODEL = "/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX/Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf"
|
||||||
|
SERVE = "/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-server"
|
||||||
|
PORT = 7770
|
||||||
|
N_GEN = 256
|
||||||
|
PROMPT_FILE = "/tmp/bench-prompts/prompt_code.txt"
|
||||||
|
ENV_FILE = os.path.join(os.path.dirname(__file__), "frank-default-large.env")
|
||||||
|
SERVER_DIR = os.path.dirname(SERVE)
|
||||||
|
ROCM_PATH = "/opt/rocm"
|
||||||
|
|
||||||
|
def make_env():
|
||||||
|
"""Build the same env as serve-frank-large.sh."""
|
||||||
|
env = {**os.environ, "HSA_OVERRIDE_GFX_VERSION": "11.5.1", "HIP_VISIBLE_DEVICES": "0"}
|
||||||
|
|
||||||
|
# Source env file for tunables
|
||||||
|
if os.path.exists(ENV_FILE):
|
||||||
|
with open(ENV_FILE) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith("#") and "=" in line:
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
env[k] = v
|
||||||
|
|
||||||
|
# Build LD_LIBRARY_PATH
|
||||||
|
lib_path = f"{SERVER_DIR}:{ROCM_PATH}/lib:{ROCM_PATH}/lib/rocm_sysdeps/lib:{ROCM_PATH}/lib/llvm/lib"
|
||||||
|
if os.environ.get("LD_LIBRARY_PATH"):
|
||||||
|
lib_path += f":{os.environ['LD_LIBRARY_PATH']}"
|
||||||
|
env["LD_LIBRARY_PATH"] = lib_path
|
||||||
|
env["ROCM_PATH"] = ROCM_PATH
|
||||||
|
|
||||||
|
return env
|
||||||
|
|
||||||
|
ENV = make_env()
|
||||||
|
|
||||||
|
def kill_all():
|
||||||
|
subprocess.run(["pkill", "-9", "llama-server"], capture_output=True)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
def wait_ready():
|
||||||
|
for i in range(600):
|
||||||
|
try:
|
||||||
|
r = urllib.request.urlopen(f"http://127.0.0.1:{PORT}/health", timeout=2)
|
||||||
|
if json.loads(r.read()).get("status") == "ok":
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_request(prompt, max_tokens=N_GEN):
|
||||||
|
"""Send chat completion, measure generation throughput."""
|
||||||
|
body = json.dumps({
|
||||||
|
"model": "frank-default-large",
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"stream": False,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"top_p": 0.8
|
||||||
|
}).encode()
|
||||||
|
|
||||||
|
t0 = time.time()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"http://127.0.0.1:{PORT}/v1/chat/completions",
|
||||||
|
data=body,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST"
|
||||||
|
)
|
||||||
|
r = urllib.request.urlopen(req, timeout=1200)
|
||||||
|
resp = json.loads(r.read())
|
||||||
|
wall = time.time() - t0
|
||||||
|
|
||||||
|
usage = resp.get("usage", {})
|
||||||
|
pt = usage.get("prompt_tokens", 0)
|
||||||
|
gt = usage.get("completion_tokens", 0)
|
||||||
|
gtps = gt / wall if wall > 0 else 0
|
||||||
|
return pt, gt, wall, gtps
|
||||||
|
|
||||||
|
def load_and_extend_prompt():
|
||||||
|
"""Load prompt and extend to ~8K tokens if needed."""
|
||||||
|
with open(PROMPT_FILE) as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
# Count approximate tokens (rough: ~4 chars per token for code)
|
||||||
|
approx_tokens = len(text) / 4.0
|
||||||
|
needed = 8200
|
||||||
|
|
||||||
|
if approx_tokens < needed:
|
||||||
|
repeats = needed // max(int(approx_tokens), 1) + 1
|
||||||
|
text = (text + "\n") * repeats
|
||||||
|
words = text.split()[:needed]
|
||||||
|
text = " ".join(words)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
def main():
|
||||||
|
tb_values = [int(x) for x in sys.argv[1:]] if len(sys.argv) > 1 else [16, 32, 64, 128]
|
||||||
|
results = []
|
||||||
|
|
||||||
|
prompt = load_and_extend_prompt()
|
||||||
|
n_words = len(prompt.split())
|
||||||
|
print(f"Prompt: {n_words} words (~{n_words} tokens), {len(prompt)} chars\n", flush=True)
|
||||||
|
|
||||||
|
for tb in tb_values:
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"threads-batch = {tb}")
|
||||||
|
print(f"{'='*60}", flush=True)
|
||||||
|
|
||||||
|
kill_all()
|
||||||
|
print(" Starting server...", flush=True)
|
||||||
|
|
||||||
|
lib_path = ENV.get("LD_LIBRARY_PATH", "")
|
||||||
|
cmd = [
|
||||||
|
"/usr/bin/env",
|
||||||
|
"HSA_OVERRIDE_GFX_VERSION=11.5.1",
|
||||||
|
"HIP_VISIBLE_DEVICES=0",
|
||||||
|
f"LD_LIBRARY_PATH={lib_path}",
|
||||||
|
f"ROCM_PATH={ROCM_PATH}",
|
||||||
|
SERVE, "--model", MODEL, "--port", str(PORT), "--jinja",
|
||||||
|
"--device", "ROCm0", "--gpu-layers", "999", "--fit", "off",
|
||||||
|
"--threads", "16", "--threads-batch", str(tb),
|
||||||
|
"--parallel", "2", "--ctx-size", "131072",
|
||||||
|
"--batch-size", "4096", "--ubatch-size", "4096",
|
||||||
|
"--flash-attn", "on",
|
||||||
|
"--cache-type-k", "f16", "--cache-type-v", "f16",
|
||||||
|
"--cache-prompt", "--cache-idle-slots", "--metrics"
|
||||||
|
]
|
||||||
|
|
||||||
|
srv = subprocess.Popen(cmd, env=ENV, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||||
|
|
||||||
|
ready = wait_ready()
|
||||||
|
if not ready:
|
||||||
|
print(" FAILED: server did not become ready", flush=True)
|
||||||
|
srv.terminate()
|
||||||
|
srv.wait()
|
||||||
|
for line in (srv.stdout.readlines() or [])[-20:]:
|
||||||
|
print(f" LOG: {line.rstrip()}", flush=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(" Server ready.", flush=True)
|
||||||
|
|
||||||
|
# Warmup (small request)
|
||||||
|
try:
|
||||||
|
send_request("Hi", 16)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Warmup failed: {e}", flush=True)
|
||||||
|
srv.terminate()
|
||||||
|
srv.wait()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Benchmark
|
||||||
|
print(f" Generating {N_GEN} tokens...", flush=True)
|
||||||
|
try:
|
||||||
|
pt, gt, wall, gtps = send_request(prompt, N_GEN)
|
||||||
|
print(f" {gt}/{N_GEN} gen tokens in {wall:.1f}s = {gtps:.1f} tok/s")
|
||||||
|
print(f" Prompt tokens: {pt}")
|
||||||
|
results.append({"tb": tb, "wall": wall, "gtps": gtps, "gt": gt, "pt": pt})
|
||||||
|
except Exception as e:
|
||||||
|
print(f" FAILED: {e}", flush=True)
|
||||||
|
|
||||||
|
srv.terminate()
|
||||||
|
srv.wait()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print("SUMMARY")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
for r in results:
|
||||||
|
print(f" tb={r['tb']:>3}: {r['gt']} gen tokens, {r['wall']:.1f}s, {r['gtps']:.1f} tok/s")
|
||||||
|
|
||||||
|
if results:
|
||||||
|
best = max(results, key=lambda r: r['gtps'])
|
||||||
|
print(f"\nBest: tb={best['tb']} at {best['gtps']:.1f} tok/s")
|
||||||
|
|
||||||
|
with open("/tmp/bench_decode_results.txt", "w") as f:
|
||||||
|
f.write("tb,gen_tokens,wall_s,gen_tps,prompt_tokens\n")
|
||||||
|
for r in results:
|
||||||
|
f.write(f"{r['tb']},{r['gt']},{r['wall']:.1f},{r['gtps']:.1f},{r['pt']}\n")
|
||||||
|
|
||||||
|
print(f"\nResults: /tmp/bench_decode_results.txt")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+129
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Decode throughput benchmark for frank-default-large.
|
||||||
|
# Measures generation tok/s with 8K+ token prompts and 256 generated tokens.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SERVE=/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-server
|
||||||
|
MODEL=/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX/Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf
|
||||||
|
PROMPT_FILE=/tmp/bench-prompts/prompt_code.txt
|
||||||
|
PORT=7770
|
||||||
|
N_GEN=256
|
||||||
|
SERVER_DIR=/home/dank/GitPackages/ROCmFPX/build-qwen38/bin
|
||||||
|
ROCM_PATH=/opt/rocm
|
||||||
|
LIB_PATH="$SERVER_DIR:$ROCM_PATH/lib:$ROCM_PATH/lib/rocm_sysdeps/lib:$ROCM_PATH/lib/llvm/lib"
|
||||||
|
|
||||||
|
kill_all() {
|
||||||
|
pkill -9 -f llama-server 2>/dev/null || true
|
||||||
|
sleep 3
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_ready() {
|
||||||
|
for i in $(seq 1 600); do
|
||||||
|
if curl -s http://127.0.0.1:$PORT/health 2>/dev/null | grep -q ok; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_bench() {
|
||||||
|
local tb=$1
|
||||||
|
local logfile=/tmp/server_tb${tb}.log
|
||||||
|
|
||||||
|
kill_all
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
env HSA_OVERRIDE_GFX_VERSION=11.5.1 HIP_VISIBLE_DEVICES=0 \
|
||||||
|
LD_LIBRARY_PATH="$LIB_PATH" ROCM_PATH="$ROCM_PATH" \
|
||||||
|
nohup $SERVE \
|
||||||
|
--model "$MODEL" --port $PORT --jinja \
|
||||||
|
--device ROCm0 --gpu-layers 999 --fit off \
|
||||||
|
--threads 16 --threads-batch $tb \
|
||||||
|
--parallel 2 --ctx-size 131072 \
|
||||||
|
--batch-size 4096 --ubatch-size 4096 \
|
||||||
|
--flash-attn on \
|
||||||
|
--cache-type-k f16 --cache-type-v f16 \
|
||||||
|
--cache-prompt --cache-idle-slots --metrics \
|
||||||
|
--temp 0.7 --top-p 0.8 --top-k 20 --min-p 0.0 \
|
||||||
|
--reasoning on --reasoning-format deepseek \
|
||||||
|
--reasoning-budget -1 > "$logfile" 2>&1 &
|
||||||
|
|
||||||
|
if ! wait_ready; then
|
||||||
|
echo "TB=$tb: FAILED (server not ready)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Warmup (small request)
|
||||||
|
curl -s -m 60 http://127.0.0.1:$PORT/v1/chat/completions \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"frank-default-large","messages":[{"role":"user","content":"Hi"}],"max_tokens":16,"stream":false}' > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Build 8K prompt
|
||||||
|
local prompt
|
||||||
|
prompt=$(python3 -c "
|
||||||
|
import json
|
||||||
|
with open('$PROMPT_FILE') as f: text=f.read()
|
||||||
|
words=text.split()[:8200]
|
||||||
|
print(json.dumps(' '.join(words)))
|
||||||
|
")
|
||||||
|
|
||||||
|
echo "TB=$tb: Starting benchmark..."
|
||||||
|
|
||||||
|
# Benchmark
|
||||||
|
local t0 t1 resp pt gt wall gtps
|
||||||
|
t0=$(date +%s%N)
|
||||||
|
resp=$(curl -s -m 600 http://127.0.0.1:$PORT/v1/chat/completions \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"model\":\"frank-default-large\",\"messages\":[{\"role\":\"user\",\"content\":$prompt}],\"max_tokens\":$N_GEN,\"stream\":false}" 2>&1)
|
||||||
|
t1=$(date +%s%N)
|
||||||
|
|
||||||
|
if ! echo "$resp" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
|
||||||
|
echo "TB=$tb: FAILED (HTTP error: $(echo "$resp" | head -1))"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
pt=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['usage']['prompt_tokens'])")
|
||||||
|
gt=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['usage']['completion_tokens'])")
|
||||||
|
wall=$(python3 -c "print(round(($t1-$t0)/1000000000,1))")
|
||||||
|
gtps=$(python3 -c "print(round($N_GEN/($t1-$t0)*1000000000,2))")
|
||||||
|
|
||||||
|
echo "TB=$tb: pt=$pt gt=$gt wall=${wall}s gen=${gtps} tok/s"
|
||||||
|
|
||||||
|
# Extract prefill timing from server log
|
||||||
|
local prefill_time
|
||||||
|
prefill_time=$(grep -oP 'prompt eval time = \K[0-9.]+' "$logfile" | head -1 || echo "N/A")
|
||||||
|
if [ "$prefill_time" != "N/A" ]; then
|
||||||
|
echo "TB=$tb: prefill=$(python3 -c "print(round($N_GEN/($wall),2))") tok/s"
|
||||||
|
fi
|
||||||
|
|
||||||
|
kill_all
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "$tb,$gt,$wall,$gtps,$pt" >> /tmp/bench_decode_results.csv
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
echo "=== Decode throughput benchmark ==="
|
||||||
|
echo "Prompt: ~8K tokens, Generation: $N_GEN tokens"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Clear results
|
||||||
|
echo "tb,gen_tokens,wall_s,gen_tps,prompt_tokens" > /tmp/bench_decode_results.csv
|
||||||
|
|
||||||
|
# Run benchmarks
|
||||||
|
for tb in 16 32; do
|
||||||
|
run_bench $tb
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo "=== SUMMARY ==="
|
||||||
|
echo "Prefill throughput (from earlier benchmark):"
|
||||||
|
echo " TB=16: 373.5 tok/s"
|
||||||
|
echo " TB=32: 354.4 tok/s"
|
||||||
|
echo ""
|
||||||
|
echo "Decode throughput (generation tok/s with 8K prompt):"
|
||||||
|
tail -n +2 /tmp/bench_decode_results.csv | while IFS=, read tb gt wall gtps pt; do
|
||||||
|
echo " TB=$tb: $gt tokens in ${wall}s = ${gtps} tok/s (pt=$pt)"
|
||||||
|
done
|
||||||
Executable
+268
@@ -0,0 +1,268 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Benchmark n-gram speculative decoding vs no-spec for frank-default-large.
|
||||||
|
|
||||||
|
Tests prompt-processing throughput with 16K token prompts (manageable request size).
|
||||||
|
Code-heavy prompt (high n-gram redundancy) vs prose prompt (low redundancy).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# Generate prompts: python3 scripts/bench-ngram-mod.py generate /tmp/prompts/
|
||||||
|
# Run: python3 scripts/bench-ngram-mod.py /tmp/prompts/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
MODEL = "/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX/Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf"
|
||||||
|
CHAT_TEMPLATE = "/home/dank/serverAppConfigs/scripts/chat-template.jinja"
|
||||||
|
BUILD_DIR = "/home/dank/GitPackages/ROCmFPX/build-qwen38/bin"
|
||||||
|
LLAMA_BIN = os.path.join(BUILD_DIR, "llama-server")
|
||||||
|
|
||||||
|
# 16K tokens ~= 64KB text
|
||||||
|
TARGET_PROMPT_TOKENS = 16000
|
||||||
|
TOKEN_CHAR_RATIO = 4
|
||||||
|
TARGET_PROMPT_BYTES = TARGET_PROMPT_TOKENS * TOKEN_CHAR_RATIO
|
||||||
|
|
||||||
|
CONTEXT = 65536
|
||||||
|
BATCH = 4096
|
||||||
|
UBATCH = 4096
|
||||||
|
THREADS = 16
|
||||||
|
THREAD_BATCH = 32
|
||||||
|
NGRAM_N_MAX = 24
|
||||||
|
NGRAM_N_MIN = 4
|
||||||
|
TEST_TOKENS = 128
|
||||||
|
SERVER_WAIT = 300
|
||||||
|
|
||||||
|
|
||||||
|
def start_server(port, spec_type):
|
||||||
|
cmd = [
|
||||||
|
LLAMA_BIN,
|
||||||
|
"--model", MODEL,
|
||||||
|
"--chat-template-file", CHAT_TEMPLATE,
|
||||||
|
"--jinja",
|
||||||
|
"--device", "ROCm0",
|
||||||
|
"--gpu-layers", "999",
|
||||||
|
"--parallel", "1",
|
||||||
|
"--ctx-size", str(CONTEXT),
|
||||||
|
"--batch-size", str(BATCH),
|
||||||
|
"--ubatch-size", str(UBATCH),
|
||||||
|
"--fit", "off",
|
||||||
|
"--flash-attn", "on",
|
||||||
|
"--cache-type-k", "f16",
|
||||||
|
"--cache-type-v", "f16",
|
||||||
|
"--threads", str(THREADS),
|
||||||
|
"--threads-batch", str(THREAD_BATCH),
|
||||||
|
"--cache-prompt",
|
||||||
|
"--cache-idle-slots",
|
||||||
|
"--metrics",
|
||||||
|
"--host", "127.0.0.1",
|
||||||
|
"--port", str(port),
|
||||||
|
"--temp", "0",
|
||||||
|
]
|
||||||
|
if spec_type == "ngram-mod":
|
||||||
|
cmd.extend([
|
||||||
|
"--spec-type", "ngram-mod",
|
||||||
|
"--spec-ngram-mod-n-max", str(NGRAM_N_MAX),
|
||||||
|
"--spec-ngram-mod-n-min", str(NGRAM_N_MIN),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
cmd.extend(["--spec-type", "none"])
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["ROCM_PATH"] = "/opt/rocm"
|
||||||
|
env["LD_LIBRARY_PATH"] = f"{BUILD_DIR}:/opt/rocm/lib:/opt/rocm/lib/rocm_sysdeps/lib:/opt/rocm/lib/llvm/lib"
|
||||||
|
env["HSA_OVERRIDE_GFX_VERSION"] = "11.5.1"
|
||||||
|
env["HIP_VISIBLE_DEVICES"] = "0"
|
||||||
|
|
||||||
|
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
print(f" Waiting for server on port {port} (spec={spec_type})...")
|
||||||
|
for i in range(SERVER_WAIT):
|
||||||
|
try:
|
||||||
|
req = urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=2)
|
||||||
|
resp = json.loads(req.read().decode())
|
||||||
|
if resp.get("status") == "ok":
|
||||||
|
print(f" Server ready after {i+1}s")
|
||||||
|
return proc
|
||||||
|
except:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
proc.terminate()
|
||||||
|
out, err = proc.communicate()
|
||||||
|
print(f"ERROR: Server failed to start in {SERVER_WAIT}s")
|
||||||
|
print(err.decode()[-2000:])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def run_test(prompt_text, spec_type, port):
|
||||||
|
"""Send a request and extract prompt processing metrics."""
|
||||||
|
data = json.dumps({
|
||||||
|
"model": "frank-default-large",
|
||||||
|
"messages": [{"role": "user", "content": prompt_text}],
|
||||||
|
"max_tokens": TEST_TOKENS,
|
||||||
|
"stream": False,
|
||||||
|
"temperature": 0,
|
||||||
|
}).encode()
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"http://127.0.0.1:{port}/v1/chat/completions",
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
resp = urllib.request.urlopen(req, timeout=600)
|
||||||
|
elapsed = time.monotonic() - start
|
||||||
|
|
||||||
|
result = json.loads(resp.read().decode())
|
||||||
|
timings = result.get("timings", {})
|
||||||
|
usage = result.get("usage", {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"prompt_tokens": usage.get("prompt_tokens", 0),
|
||||||
|
"prompt_ms": timings.get("prompt_ms", 0),
|
||||||
|
"prompt_per_second": timings.get("prompt_per_second", 0),
|
||||||
|
"prompt_per_token_ms": timings.get("prompt_per_token_ms", 0),
|
||||||
|
"predicted_per_second": timings.get("predicted_per_second", 0),
|
||||||
|
"total_time_s": elapsed,
|
||||||
|
"cache_n": timings.get("cache_n", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} <prompt_dir>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
prompt_dir = Path(sys.argv[1])
|
||||||
|
prompt_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
code_prompt_file = prompt_dir / "prompt_code.txt"
|
||||||
|
prose_prompt_file = prompt_dir / "prompt_prose.txt"
|
||||||
|
|
||||||
|
# Generate prompts if they don't exist
|
||||||
|
if not code_prompt_file.exists() or not prose_prompt_file.exists():
|
||||||
|
print("Generating prompts...")
|
||||||
|
# Generate deterministic prompts
|
||||||
|
# Code prompt: structured algorithms + data generation + config schemas
|
||||||
|
code_lines = []
|
||||||
|
for i in range(50):
|
||||||
|
code_lines.append(f"# Algorithm {i}\n")
|
||||||
|
code_lines.append(f"def algo_{i}(x, y):\n")
|
||||||
|
code_lines.append(f" \"\"\"Algorithm {i} processing.\"\"\"\n")
|
||||||
|
code_lines.append(f" result = 0\n")
|
||||||
|
code_lines.append(f" for j in range({i * 10}):\n")
|
||||||
|
code_lines.append(f" result += x * y + j\n")
|
||||||
|
code_lines.append(f" return result\n\n")
|
||||||
|
code_text = "".join(code_lines)
|
||||||
|
# Pad/truncate to target
|
||||||
|
while len(code_text) < TARGET_PROMPT_BYTES:
|
||||||
|
code_text += "# Additional algorithm block\n"
|
||||||
|
code_text += f"def extra_{len(code_text)//100}():\n"
|
||||||
|
code_text += " x = 1\n"
|
||||||
|
code_text += " y = 2\n"
|
||||||
|
code_text += " return x + y\n\n"
|
||||||
|
code_text = code_text[:TARGET_PROMPT_BYTES]
|
||||||
|
|
||||||
|
# Prose prompt: historical/scientific/philosophical text
|
||||||
|
prose_text = ""
|
||||||
|
topics = [
|
||||||
|
"The Renaissance brought profound transformation to European civilization. Artists and scholars pushed the boundaries of human expression.",
|
||||||
|
"Quantum mechanics describes the behavior of matter and energy at atomic scales. The uncertainty principle limits measurement precision.",
|
||||||
|
"Ethical philosophy grapples with questions of right and wrong. Utilitarianism judges actions by their consequences and the happiness they produce.",
|
||||||
|
"Mountains rise like ancient sentinels, their peaks lost in clouds that swirl and dance across the sky above the valleys below.",
|
||||||
|
"The ocean whispers secrets to the shore, each wave carrying fragments of distant lands across the water before retreating back to sea.",
|
||||||
|
]
|
||||||
|
for _ in range(500):
|
||||||
|
prose_text += f"{topics[len(prose_text) % len(topics)]} "
|
||||||
|
prose_text = prose_text[:TARGET_PROMPT_BYTES]
|
||||||
|
|
||||||
|
code_prompt_file.write_text(code_text)
|
||||||
|
prose_prompt_file.write_text(prose_text)
|
||||||
|
print(f" Code: {code_prompt_file.stat().st_size:,} bytes")
|
||||||
|
print(f" Prose: {prose_prompt_file.stat().st_size:,} bytes")
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print(" N-gram Mod Benchmark — frank-default-large")
|
||||||
|
print(f" STRIX ROCmFP4 · 64K context · 16K prompts")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
port_base = 7772
|
||||||
|
port_ngram = 7773
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
("code", "none", port_base, code_prompt_file),
|
||||||
|
("prose", "none", port_ngram, prose_prompt_file),
|
||||||
|
("code", "ngram-mod", port_base, code_prompt_file),
|
||||||
|
("prose", "ngram-mod", port_ngram, prose_prompt_file),
|
||||||
|
]
|
||||||
|
|
||||||
|
for prompt_type, spec_type, port, pfile in tests:
|
||||||
|
print(f"\n=== {spec_type} ({prompt_type}) ===")
|
||||||
|
print(f" File: {pfile} ({pfile.stat().st_size:,} bytes)")
|
||||||
|
print(f" Spec: {spec_type}")
|
||||||
|
if spec_type == "ngram-mod":
|
||||||
|
print(f" n_max={NGRAM_N_MAX}, n_min={NGRAM_N_MIN}")
|
||||||
|
|
||||||
|
proc = start_server(port, spec_type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Warm-up
|
||||||
|
print(" Warm-up...")
|
||||||
|
run_test("Hello test", spec_type, port)
|
||||||
|
|
||||||
|
# Full prompt
|
||||||
|
print(f" Sending {TARGET_PROMPT_TOKENS} token prompt...")
|
||||||
|
with open(pfile) as f:
|
||||||
|
prompt_text = f.read()
|
||||||
|
result = run_test(prompt_text, spec_type, port)
|
||||||
|
|
||||||
|
tps = result["prompt_per_second"]
|
||||||
|
print(f" Tokens: {result['prompt_tokens']}")
|
||||||
|
print(f" Prompt: {result['prompt_ms']:.0f}ms")
|
||||||
|
print(f" TPS: {tps:.1f} tok/s")
|
||||||
|
print(f" Total: {result['total_time_s']:.1f}s")
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"prompt_type": prompt_type,
|
||||||
|
"spec_type": spec_type,
|
||||||
|
"prompt_tokens": result["prompt_tokens"],
|
||||||
|
"prompt_ms": result["prompt_ms"],
|
||||||
|
"tps": tps,
|
||||||
|
"total_time_s": result["total_time_s"],
|
||||||
|
})
|
||||||
|
|
||||||
|
finally:
|
||||||
|
proc.terminate()
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except:
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(" RESULTS SUMMARY")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
for ptype in ["code", "prose"]:
|
||||||
|
baseline = next((r for r in results if r["prompt_type"] == ptype and r["spec_type"] == "none"), None)
|
||||||
|
ngram = next((r for r in results if r["prompt_type"] == ptype and r["spec_type"] == "ngram-mod"), None)
|
||||||
|
if baseline and ngram:
|
||||||
|
gain = ((ngram["tps"] - baseline["tps"]) / baseline["tps"]) * 100 if baseline["tps"] > 0 else 0
|
||||||
|
print(f"\n{ptype.upper()} PROMPT:")
|
||||||
|
print(f" Baseline TPS: {baseline['tps']:.1f} tok/s ({baseline['prompt_ms']:.0f}ms)")
|
||||||
|
print(f" N-gram TPS: {ngram['tps']:.1f} tok/s ({ngram['prompt_ms']:.0f}ms)")
|
||||||
|
print(f" Speedup: {gain:+.1f}%")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+304
@@ -0,0 +1,304 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Benchmark n-gram speculative decoding vs no-spec for frank-default-large.
|
||||||
|
#
|
||||||
|
# This tests prompt-processing throughput at 64k context with two distinct
|
||||||
|
# prompts: one code-heavy and one prose-heavy. Uses llama-server on scratch
|
||||||
|
# ports with the OpenAI-compatible API to measure prompt_eval_duration_ms.
|
||||||
|
#
|
||||||
|
# Run on Dankclaw after stopping the production service:
|
||||||
|
# sudo systemctl stop frank-default-large
|
||||||
|
# ./scripts/bench-ngram-mod.sh
|
||||||
|
# sudo systemctl start frank-default-large
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${FRANK_LARGE_ENV_FILE:-$SCRIPT_DIR/frank-default-large.env}"
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
set -a; source "$ENV_FILE"; set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
: "${LLAMA_BIN:=/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-server}"
|
||||||
|
: "${MODEL_DIR:=/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX}"
|
||||||
|
: "${MODEL_FILE:=Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf}"
|
||||||
|
: "${CHAT_TEMPLATE_FILE:=$SCRIPT_DIR/chat-template.jinja}"
|
||||||
|
: "${CHAT_TEMPLATE_KWARGS:={}}"
|
||||||
|
: "${BATCH_SIZE:=4096}"
|
||||||
|
: "${UBATCH_SIZE:=4096}"
|
||||||
|
: "${CTX_SIZE:=65536}"
|
||||||
|
: "${NUM_THREADS:=16}"
|
||||||
|
: "${THREAD_BATCH:=32}"
|
||||||
|
: "${CACHE_TYPE_K:=f16}"
|
||||||
|
: "${CACHE_TYPE_V:=f16}"
|
||||||
|
: "${FIT:=off}"
|
||||||
|
: "${TEMPERATURE:=0.7}"
|
||||||
|
: "${TOP_P:=0.8}"
|
||||||
|
: "${TOP_K:=20}"
|
||||||
|
: "${MIN_P:=0.0}"
|
||||||
|
: "${REASONING:=on}"
|
||||||
|
: "${REASONING_FORMAT:=deepseek}"
|
||||||
|
|
||||||
|
MODEL="$MODEL_DIR/$MODEL_FILE"
|
||||||
|
HOSTNAME=$(hostname 2>/dev/null || echo "dankclaw")
|
||||||
|
|
||||||
|
# Prompt files (generated by generate-prompts.py or passed via args)
|
||||||
|
PROMPT_CODE="${1:-}"
|
||||||
|
PROMPT_PROSE="${2:-}"
|
||||||
|
|
||||||
|
if [[ -z "$PROMPT_CODE" || -z "$PROMPT_PROSE" ]]; then
|
||||||
|
echo "ERROR: Need two prompt file paths."
|
||||||
|
echo "Usage: $0 <prompt_code.txt> <prompt_prose.txt>"
|
||||||
|
echo "Generate with: python3 scripts/generate-prompts.py"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -r "$MODEL" ]]; then
|
||||||
|
echo "missing or unreadable model: $MODEL" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVER_DIR="$(cd -- "$(dirname -- "$LLAMA_BIN")" && pwd)"
|
||||||
|
ROCM_PATH="${ROCM_PATH:-/opt/rocm}"
|
||||||
|
LIBRARY_PATH="$SERVER_DIR:$ROCM_PATH/lib:$ROCM_PATH/lib/rocm_sysdeps/lib:$ROCM_PATH/lib/llvm/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||||
|
|
||||||
|
# Scratch ports
|
||||||
|
PORT_NGRAM=7771
|
||||||
|
PORT_BASELINE=7772
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo "Cleaning up servers..."
|
||||||
|
# Kill any leftover llama-server on our ports
|
||||||
|
for port in $PORT_NGRAM $PORT_BASELINE; do
|
||||||
|
fuser -k "$port"/tcp 2>/dev/null || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
wait_for_server() {
|
||||||
|
local port=$1
|
||||||
|
local max_wait=120
|
||||||
|
local elapsed=0
|
||||||
|
while (( elapsed < max_wait )); do
|
||||||
|
if curl -s "http://127.0.0.1:$port/health" | grep -q '"running"'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
(( elapsed++ ))
|
||||||
|
done
|
||||||
|
echo "ERROR: Server on port $port did not start within ${max_wait}s" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get_prompt_tokens() {
|
||||||
|
local file=$1
|
||||||
|
# Count tokens approximately (1 token ~ 4 chars for English/code)
|
||||||
|
wc -c < "$file" | awk '{printf "%d", $1/4}'
|
||||||
|
}
|
||||||
|
|
||||||
|
run_single_test() {
|
||||||
|
local port=$1
|
||||||
|
local spec_type="$2"
|
||||||
|
local prompt_file=$3
|
||||||
|
local label=$4
|
||||||
|
local ngram_n_max="${5:-24}"
|
||||||
|
local ngram_n_min="${6:-4}"
|
||||||
|
|
||||||
|
echo "=== $label ==="
|
||||||
|
echo " spec-type: $spec_type"
|
||||||
|
echo " ngram-mod n-max: $ngram_n_max (if enabled)"
|
||||||
|
echo " ngram-mod n-min: $ngram_n_min (if enabled)"
|
||||||
|
echo " prompt: $prompt_file ($(wc -c < "$prompt_file") bytes)"
|
||||||
|
|
||||||
|
# Build command
|
||||||
|
local cmd=(
|
||||||
|
/usr/bin/env
|
||||||
|
ROCM_PATH="$ROCM_PATH"
|
||||||
|
HIP_VISIBLE_DEVICES=0
|
||||||
|
LD_LIBRARY_PATH="$LIBRARY_PATH"
|
||||||
|
HSA_OVERRIDE_GFX_VERSION="${HSA_OVERRIDE_GFX_VERSION:-11.5.1}"
|
||||||
|
"$LLAMA_BIN"
|
||||||
|
--model "$MODEL"
|
||||||
|
--chat-template-file "$CHAT_TEMPLATE_FILE"
|
||||||
|
--chat-template-kwargs "$CHAT_TEMPLATE_KWARGS"
|
||||||
|
--host 127.0.0.1
|
||||||
|
--port "$port"
|
||||||
|
--jinja
|
||||||
|
--device ROCm0
|
||||||
|
--gpu-layers 999
|
||||||
|
--parallel 1
|
||||||
|
--ctx-size "$CTX_SIZE"
|
||||||
|
--batch-size "$BATCH_SIZE"
|
||||||
|
--ubatch-size "$UBATCH_SIZE"
|
||||||
|
--fit "$FIT"
|
||||||
|
--flash-attn on
|
||||||
|
--cache-type-k "$CACHE_TYPE_K"
|
||||||
|
--cache-type-v "$CACHE_TYPE_V"
|
||||||
|
--threads "$NUM_THREADS"
|
||||||
|
--threads-batch "$THREAD_BATCH"
|
||||||
|
--cache-prompt
|
||||||
|
--cache-idle-slots
|
||||||
|
--metrics
|
||||||
|
--temp "$TEMPERATURE"
|
||||||
|
--top-p "$TOP_P"
|
||||||
|
--top-k "$TOP_K"
|
||||||
|
--min-p "$MIN_P"
|
||||||
|
--reasoning "$REASONING"
|
||||||
|
--reasoning-format "$REASONING_FORMAT"
|
||||||
|
--reasoning-budget -1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add spec flags
|
||||||
|
if [[ "$spec_type" == "ngram-mod" ]]; then
|
||||||
|
cmd+=(
|
||||||
|
--spec-type "ngram-mod"
|
||||||
|
--spec-ngram-mod-n-max "$ngram_n_max"
|
||||||
|
--spec-ngram-mod-n-min "$ngram_n_min"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
cmd+=(
|
||||||
|
--spec-type "none"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start server with stderr to log
|
||||||
|
local log_file="/tmp/ngram-bench-${label//[^a-zA-Z0-9]/-}.log"
|
||||||
|
echo " Starting server (log: $log_file)..."
|
||||||
|
|
||||||
|
"${cmd[@]}" 2> "$log_file" &
|
||||||
|
local server_pid=$!
|
||||||
|
|
||||||
|
if ! wait_for_server "$port"; then
|
||||||
|
echo "ERROR: Server failed to start" >&2
|
||||||
|
kill "$server_pid" 2>/dev/null || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Server ready."
|
||||||
|
|
||||||
|
# Send prompt via API
|
||||||
|
echo " Sending prompt..."
|
||||||
|
local prompt_text
|
||||||
|
prompt_text=$(cat "$prompt_file")
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -s -X POST "http://127.0.0.1:$port/v1/completions" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"model\": \"frank-default-large\",
|
||||||
|
\"prompt\": \"$prompt_text\",
|
||||||
|
\"max_tokens\": 128,
|
||||||
|
\"temperature\": 0,
|
||||||
|
\"stream\": false
|
||||||
|
}")
|
||||||
|
|
||||||
|
# Extract metrics
|
||||||
|
local prompt_tokens prompt_eval_duration_ms total_duration_ms
|
||||||
|
prompt_tokens=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['prompt_eval_count'])" 2>/dev/null || echo "0")
|
||||||
|
prompt_eval_duration_ms=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['prompt_eval_duration_ms'])" 2>/dev/null || echo "0")
|
||||||
|
total_duration_ms=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['total_duration_ms'])" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
# Calculate throughput
|
||||||
|
local prompt_tps=0
|
||||||
|
if [[ "$prompt_eval_duration_ms" -gt 0 ]]; then
|
||||||
|
prompt_tps=$(python3 -c "print(f'{($prompt_tokens / $prompt_eval_duration_ms) * 1000:.1f}')")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Prompt tokens: $prompt_tokens"
|
||||||
|
echo " Prompt eval: ${prompt_eval_duration_ms}ms"
|
||||||
|
echo " Prompt TPS: $prompt_tps tok/s"
|
||||||
|
echo " Total duration: ${total_duration_ms}ms"
|
||||||
|
|
||||||
|
# Kill server
|
||||||
|
kill "$server_pid" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo " Done."
|
||||||
|
echo "---"
|
||||||
|
|
||||||
|
# Output structured results for parsing
|
||||||
|
echo "RESULT|$label|$prompt_file|$prompt_tokens|$prompt_eval_duration_ms|$total_duration_ms|$prompt_tps"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "╔══════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ N-gram Mod Benchmark — frank-default-large ║"
|
||||||
|
echo "║ STRIX ROCmFP4 · 64k context · two prompts ║"
|
||||||
|
echo "╚══════════════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
echo "Host: $HOSTNAME"
|
||||||
|
echo "Model: $MODEL"
|
||||||
|
echo "Build: $(basename "$LLAMA_BIN")"
|
||||||
|
echo "Context: $CTX_SIZE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run baseline (no spec)
|
||||||
|
echo "=== BASELINE (no speculative decoding) ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
BASELINE_CODE_RESULT=$(run_single_test $PORT_BASELINE "none" "$PROMPT_CODE" "Baseline-Code" 24 4)
|
||||||
|
BASELINE_PROSE_RESULT=$(run_single_test $PORT_NGRAM "none" "$PROMPT_PROSE" "Baseline-Prose" 24 4)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== NGRAM-MOD (speculative decoding enabled) ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run n-gram mod
|
||||||
|
NGRAM_CODE_RESULT=$(run_single_test $PORT_NGRAM "ngram-mod" "$PROMPT_CODE" "Ngram-Code" 24 4)
|
||||||
|
NGRAM_PROSE_RESULT=$(run_single_test $PORT_BASELINE "ngram-mod" "$PROMPT_PROSE" "Ngram-Prose" 24 4)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "╔══════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ RESULTS SUMMARY ║"
|
||||||
|
echo "╚══════════════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Parse and display results
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import sys
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for line in sys.stdin:
|
||||||
|
line = line.strip()
|
||||||
|
if not line.startswith("RESULT|"):
|
||||||
|
continue
|
||||||
|
parts = line.split("|")
|
||||||
|
if len(parts) != 8:
|
||||||
|
continue
|
||||||
|
label, prompt_file, prompt_tokens, prompt_eval_ms, total_ms, tps = parts[1], parts[2], int(parts[3]), int(parts[4]), int(parts[5]), float(parts[6])
|
||||||
|
results.append({
|
||||||
|
'label': label,
|
||||||
|
'prompt_file': prompt_file,
|
||||||
|
'prompt_tokens': prompt_tokens,
|
||||||
|
'prompt_eval_ms': prompt_eval_ms,
|
||||||
|
'total_ms': total_ms,
|
||||||
|
'tps': tps
|
||||||
|
})
|
||||||
|
|
||||||
|
# Compare baseline vs ngram for each prompt type
|
||||||
|
for code_result in results:
|
||||||
|
if not code_result['label'].startswith('Ngram-'):
|
||||||
|
continue
|
||||||
|
prompt_type = code_result['label'].replace('Ngram-', '').replace('Ngram', 'Baseline').replace('-', '-')
|
||||||
|
baseline = next((r for r in results if r['label'] == 'Baseline-' + prompt_type), None)
|
||||||
|
if not baseline:
|
||||||
|
continue
|
||||||
|
|
||||||
|
spec = code_result['label'] == 'Ngram-' + prompt_type
|
||||||
|
|
||||||
|
print(f"Prompt: {prompt_type}")
|
||||||
|
print(f" Tokens: {code_result['prompt_tokens']}")
|
||||||
|
if spec:
|
||||||
|
print(f" Spec: ngram-mod (n_max=24, n_min=4)")
|
||||||
|
print(f" Prompt: {code_result['prompt_eval_ms']}ms | {code_result['tps']:>10.1f} tok/s")
|
||||||
|
baseline_tps = baseline['tps']
|
||||||
|
if baseline_tps > 0:
|
||||||
|
improvement = ((code_result['tps'] - baseline_tps) / baseline_tps) * 100
|
||||||
|
print(f" Speedup: {improvement:+.1f}% vs baseline ({baseline_tps:.1f} tok/s)")
|
||||||
|
else:
|
||||||
|
print(f" Spec: none (baseline)")
|
||||||
|
print(f" Prompt: {baseline['prompt_eval_ms']}ms | {baseline['tps']:>10.1f} tok/s")
|
||||||
|
print()
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Raw data lines above (RESULT|... format) can be parsed for automation."
|
||||||
|
echo ""
|
||||||
Executable
+122
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
HTTP-based prefill-throughput sweep for frank-default-large.
|
||||||
|
Starts the server with a given --threads-batch value, sends a 64K-prompt via
|
||||||
|
the chat API (max_tokens=0), measures prompt_tokens/wall_time.
|
||||||
|
|
||||||
|
One request per value. Each --threads-batch requires a fresh cold server load
|
||||||
|
(~3 min), so repeating would add 2x overhead. The server also crashes on 2nd
|
||||||
|
request with max_tokens=0, making repeats impractical.
|
||||||
|
"""
|
||||||
|
import subprocess, time, os, sys, json, urllib.request, urllib.error
|
||||||
|
|
||||||
|
MODEL = "/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX/Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf"
|
||||||
|
SERVE = "/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-server"
|
||||||
|
TB_LIST = [16, 32, 48, 64, 80, 96, 128]
|
||||||
|
PROMPT_FILE = "/tmp/bench-prompts/prompt_code.txt"
|
||||||
|
PORT = 7770
|
||||||
|
ENV = {**os.environ, "HSA_OVERRIDE_GFX_VERSION": "11.5.1", "HIP_VISIBLE_DEVICES": "0"}
|
||||||
|
|
||||||
|
def kill_all():
|
||||||
|
subprocess.run(["pkill", "-9", "llama-server"], capture_output=True)
|
||||||
|
subprocess.run(["pkill", "-9", "llama-bench"], capture_output=True)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
def wait_ready():
|
||||||
|
for i in range(600):
|
||||||
|
try:
|
||||||
|
r = urllib.request.urlopen(f"http://127.0.0.1:{PORT}/health", timeout=2)
|
||||||
|
data = json.loads(r.read())
|
||||||
|
if data.get("status") == "ok":
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_prefill_request():
|
||||||
|
"""Send chat completion with max_tokens=0 (prefill only)."""
|
||||||
|
with open(PROMPT_FILE, "r") as f:
|
||||||
|
prompt = f.read()
|
||||||
|
body = json.dumps({
|
||||||
|
"model": "frank-default-large",
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"max_tokens": 1,
|
||||||
|
"stream": False
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"http://127.0.0.1:{PORT}/v1/chat/completions",
|
||||||
|
data=body,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST"
|
||||||
|
)
|
||||||
|
t0 = time.time()
|
||||||
|
r = urllib.request.urlopen(req, timeout=600)
|
||||||
|
resp = json.loads(r.read())
|
||||||
|
wall = time.time() - t0
|
||||||
|
usage = resp.get("usage", {})
|
||||||
|
prompt_tokens = usage.get("prompt_tokens", 0)
|
||||||
|
return prompt_tokens, wall
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for tb in TB_LIST:
|
||||||
|
print(f"=== threads-batch={tb} ===", flush=True)
|
||||||
|
kill_all()
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
print(" Starting server...", flush=True)
|
||||||
|
srv = subprocess.Popen(
|
||||||
|
[SERVE, "--model", MODEL, "--port", str(PORT), "--jinja",
|
||||||
|
"--device", "ROCm0", "--gpu-layers", "999", "--fit", "off",
|
||||||
|
"--threads", "16", "--threads-batch", str(tb),
|
||||||
|
"--parallel", "2", "--ctx-size", "524288",
|
||||||
|
"--batch-size", "4096", "--ubatch-size", "4096",
|
||||||
|
"--flash-attn", "on",
|
||||||
|
"--cache-type-k", "f16", "--cache-type-v", "f16",
|
||||||
|
"--cache-prompt", "--cache-idle-slots", "--metrics"],
|
||||||
|
env=ENV, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||||
|
|
||||||
|
ready = wait_ready()
|
||||||
|
if not ready:
|
||||||
|
srv.terminate()
|
||||||
|
srv.wait()
|
||||||
|
print(" FAILED: server didn't start")
|
||||||
|
sys.exit(1)
|
||||||
|
print(" Ready.", flush=True)
|
||||||
|
|
||||||
|
# Single prefill request
|
||||||
|
try:
|
||||||
|
prompt_tokens, wall = send_prefill_request()
|
||||||
|
if prompt_tokens > 0:
|
||||||
|
tps = prompt_tokens / wall
|
||||||
|
print(f" {prompt_tokens} tok in {wall:.1f}s = {tps:.1f} tok/s", flush=True)
|
||||||
|
results.append({"tb": tb, "avg": tps, "std": 0, "samples": 1})
|
||||||
|
else:
|
||||||
|
print(f" FAILED (0 prompt_tokens, wall={wall:.1f}s)", flush=True)
|
||||||
|
results.append({"tb": tb, "avg": None, "std": 0, "samples": 0})
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR: {e}", flush=True)
|
||||||
|
results.append({"tb": tb, "avg": None, "std": 0, "samples": 0})
|
||||||
|
|
||||||
|
srv.terminate()
|
||||||
|
srv.wait()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("SUMMARY")
|
||||||
|
print("=" * 60)
|
||||||
|
for r in results:
|
||||||
|
if r["avg"] is not None:
|
||||||
|
print(f" tb={r['tb']:>3} prefill={r['avg']:.1f} tok/s ({r['samples']} sample)")
|
||||||
|
else:
|
||||||
|
print(f" tb={r['tb']:>3} FAILED")
|
||||||
|
|
||||||
|
best = max((r for r in results if r["avg"] is not None), key=lambda r: r["avg"])
|
||||||
|
if best:
|
||||||
|
print(f"\nBest: threads-batch={best['tb']} ({best['avg']:.1f} tok/s)")
|
||||||
|
|
||||||
|
with open("/tmp/bench_tb_results.txt", "w") as f:
|
||||||
|
for r in results:
|
||||||
|
avg = f"{r['avg']:.1f}" if r["avg"] is not None else "N/A"
|
||||||
|
f.write(f"{r['tb']},{avg},0,{r['samples']}\n")
|
||||||
Executable
+163
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Threads-batch sweep for frank-default-large on dankclaw
|
||||||
|
# Benches prefill throughput at 65536 prompt tokens across threads-batch values.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
MODEL="/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX/Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf"
|
||||||
|
SERVE="/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-server"
|
||||||
|
BENCH="/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-bench"
|
||||||
|
TB_LIST=(16 32 48 64 80 96 128)
|
||||||
|
ENV_FILE="/home/dank/GitPackages/ROCmFPX/build-qwen38/bin"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "THREADS-BATCH SWEEP — frank-default-large"
|
||||||
|
echo "Model: $(basename "$MODEL") ($(du -h "$MODEL" | cut -f1))"
|
||||||
|
echo "Threads-batch values: ${TB_LIST[*]}"
|
||||||
|
echo "Prompt: 65536 tokens, n-gen: 0 (prefill-only)"
|
||||||
|
echo "========================================"
|
||||||
|
echo
|
||||||
|
|
||||||
|
RESULTS=""
|
||||||
|
|
||||||
|
for tb in "${TB_LIST[@]}"; do
|
||||||
|
echo "--- threads-batch=$tb ---"
|
||||||
|
|
||||||
|
# Kill any lingering processes
|
||||||
|
pkill -9 llama-server 2>/dev/null || true
|
||||||
|
pkill -9 llama-bench 2>/dev/null || true
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
echo " Starting server with --threads-batch $tb ..."
|
||||||
|
timeout 300 env HSA_OVERRIDE_GFX_VERSION=11.5.1 HIP_VISIBLE_DEVICES=0 \
|
||||||
|
"$SERVE" \
|
||||||
|
--model "$MODEL" --port 7770 --jinja \
|
||||||
|
--device ROCm0 --gpu-layers 999 --fit off \
|
||||||
|
--threads 16 --threads-batch "$tb" \
|
||||||
|
--parallel 2 --ctx-size 524288 \
|
||||||
|
--batch-size 4096 --ubatch-size 4096 \
|
||||||
|
--flash-attn on \
|
||||||
|
--cache-type-k f16 --cache-type-v f16 \
|
||||||
|
--cache-prompt --cache-idle-slots --metrics \
|
||||||
|
&>/tmp/server_tb_${tb}.log &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Wait for ready
|
||||||
|
READY=false
|
||||||
|
for i in $(seq 1 300); do
|
||||||
|
if curl -sf http://127.0.0.1:7770/health >/dev/null 2>&1; then
|
||||||
|
READY=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if (( i % 30 == 0 )); then
|
||||||
|
echo " Waiting for ready... (${i}s)"
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! $READY; then
|
||||||
|
echo " Server failed to start. Tail of log:"
|
||||||
|
tail -5 /tmp/server_tb_${tb}.log
|
||||||
|
echo " N/A"
|
||||||
|
RESULTS="${RESULTS}${tb},N/A,N/A,?,0\n"
|
||||||
|
echo
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo " Server ready."
|
||||||
|
|
||||||
|
# Run bench (prefill-only: n-gen=0)
|
||||||
|
echo " Running bench..."
|
||||||
|
t0=$(date +%s%N)
|
||||||
|
|
||||||
|
BENCH_OUTPUT=$(env HSA_OVERRIDE_GFX_VERSION=11.5.1 HIP_VISIBLE_DEVICES=0 \
|
||||||
|
"$BENCH" \
|
||||||
|
--model "$MODEL" --threads 16 \
|
||||||
|
--n-prompt 65536 --n-gen 0 \
|
||||||
|
--repetitions 3 --output csv \
|
||||||
|
--flash-attn on --n-gpu-layers 999 2>/tmp/bench_stderr_${tb}.log)
|
||||||
|
bexit=$?
|
||||||
|
t1=$(date +%s%N)
|
||||||
|
elapsed=$(( (t1 - t0) / 1000000000 ))
|
||||||
|
|
||||||
|
# Parse CSV: find prefill row (n_prompt > 0, n_gen == 0)
|
||||||
|
PREFILL="N/A"
|
||||||
|
DECODE="N/A"
|
||||||
|
CONTEXT="?"
|
||||||
|
if [ $bexit -eq 0 ] && [ -n "$BENCH_OUTPUT" ]; then
|
||||||
|
# Extract prefill row: n_prompt > 0 and n_gen == 0
|
||||||
|
PREFILL=$(echo "$BENCH_OUTPUT" | python3 -c "
|
||||||
|
import csv, sys, io
|
||||||
|
reader = csv.reader(io.StringIO(sys.stdin.read()))
|
||||||
|
header = next(reader, None)
|
||||||
|
for row in reader:
|
||||||
|
if len(row) >= 41:
|
||||||
|
try:
|
||||||
|
np_ = int(row[33])
|
||||||
|
ng = int(row[34])
|
||||||
|
avg_ts = float(row[39])
|
||||||
|
if np_ > 0 and ng == 0:
|
||||||
|
print(f'{avg_ts:.1f}')
|
||||||
|
break
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
" 2>/dev/null)
|
||||||
|
CONTEXT=$(echo "$BENCH_OUTPUT" | python3 -c "
|
||||||
|
import csv, sys, io
|
||||||
|
reader = csv.reader(io.StringIO(sys.stdin.read()))
|
||||||
|
header = next(reader, None)
|
||||||
|
for row in reader:
|
||||||
|
if len(row) >= 41:
|
||||||
|
try:
|
||||||
|
np_ = int(row[33])
|
||||||
|
ng = int(row[34])
|
||||||
|
if np_ > 0 and ng == 0:
|
||||||
|
print(row[40].strip() if row[40].strip() else '?')
|
||||||
|
break
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$PREFILL" ]; then
|
||||||
|
PREFILL="N/A"
|
||||||
|
CONTEXT="parse_err"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " Bench failed (rc=$bexit). STDERR:"
|
||||||
|
head -5 /tmp/bench_stderr_${tb}.log
|
||||||
|
echo " N/A"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " prefill=${PREFILL} tok/s decode=${DECODE} ctx=${CONTEXT} time=${elapsed}s"
|
||||||
|
RESULTS="${RESULTS}${tb},${PREFILL},${DECODE},${CONTEXT},${elapsed}\n"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Shutdown server
|
||||||
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
|
wait $SERVER_PID 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "SUMMARY"
|
||||||
|
echo "========================================"
|
||||||
|
echo "tb prefill_tok/s decode_tok/s ctx_util time_s"
|
||||||
|
echo "--------------------------------------------------------"
|
||||||
|
echo -e "$RESULTS" | grep -v "^$" | while IFS=, read -r tb pt dt cu el; do
|
||||||
|
printf "%-6s %-16s %-16s %-12s %-8s\n" "$tb" "$pt" "$dt" "$cu" "${el}s"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Find best
|
||||||
|
BEST=$(echo -e "$RESULTS" | grep -v "^$" | grep -v ",N/A," | sort -t, -k2 -rn | head -1)
|
||||||
|
BEST_TB=$(echo "$BEST" | cut -d, -f1)
|
||||||
|
BEST_PT=$(echo "$BEST" | cut -d, -f2)
|
||||||
|
echo ""
|
||||||
|
echo "Best threads-batch: $BEST_TB (${BEST_PT} tok/s prefill)"
|
||||||
|
|
||||||
|
# Write results
|
||||||
|
echo -e "$RESULTS" | grep -v "^$" > /tmp/bench_tb_results.txt
|
||||||
|
echo ""
|
||||||
|
echo "Results written to /tmp/bench_tb_results.txt"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
pkill -9 llama-server 2>/dev/null || true
|
||||||
|
pkill -9 llama-bench 2>/dev/null || true
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import subprocess, time, os, sys, csv, io
|
||||||
|
|
||||||
|
MODEL = "/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX/Qwen3.8-Flash-Next-Q4_0-ROCmFP4-STRIX.gguf"
|
||||||
|
SERVE = "/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-server"
|
||||||
|
BENCH = "/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-bench"
|
||||||
|
TB_LIST = [16, 32, 48, 64, 80, 96, 128]
|
||||||
|
ENV = {**os.environ, "HSA_OVERRIDE_GFX_VERSION": "11.5.1", "HIP_VISIBLE_DEVICES": "0"}
|
||||||
|
|
||||||
|
def kill_all():
|
||||||
|
subprocess.run(["pkill", "-9", "llama-server"], capture_output=True)
|
||||||
|
subprocess.run(["pkill", "-9", "llama-bench"], capture_output=True)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
def wait_ready():
|
||||||
|
import urllib.request
|
||||||
|
for i in range(600):
|
||||||
|
try:
|
||||||
|
r = urllib.request.urlopen("http://127.0.0.1:7770/health", timeout=2)
|
||||||
|
if r.status == 200:
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def parse_bench_output(stdout, stderr):
|
||||||
|
"""Parse llama-bench CSV output. Columns (0-indexed): n_prompt=33, avg_ts=39"""
|
||||||
|
prefill = decode = None
|
||||||
|
ctx = "?"
|
||||||
|
try:
|
||||||
|
reader = csv.reader(io.StringIO(stdout))
|
||||||
|
header = next(reader, None)
|
||||||
|
if header is None:
|
||||||
|
return None, None, "?"
|
||||||
|
|
||||||
|
# Find prefill row: n_prompt matches our prompt size, n_gen matches our gen size
|
||||||
|
# For n-gen=0, we get prefill only. The prefill row has n_prompt > 0.
|
||||||
|
for row in reader:
|
||||||
|
if len(row) >= 41:
|
||||||
|
try:
|
||||||
|
n_prompt = int(row[33])
|
||||||
|
n_gen = int(row[34])
|
||||||
|
avg_ts = float(row[39]) # avg_tokens_per_second
|
||||||
|
|
||||||
|
if n_prompt > 0 and n_gen == 0:
|
||||||
|
# This is the prefill row
|
||||||
|
prefill = avg_ts
|
||||||
|
ctx = row[40].strip() if row[40].strip() else "?"
|
||||||
|
return prefill, None, ctx
|
||||||
|
elif n_prompt > 0 and n_gen > 0:
|
||||||
|
# Mixed prefill+decode: avg_ts is overall, use prefill row if available
|
||||||
|
if prefill is None:
|
||||||
|
prefill = avg_ts # fallback
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f" CSV parse error: {e}", flush=True)
|
||||||
|
|
||||||
|
return prefill, decode, ctx
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for tb in TB_LIST:
|
||||||
|
print(f"=== threads-batch={tb} ===", flush=True)
|
||||||
|
kill_all()
|
||||||
|
|
||||||
|
srv = subprocess.Popen(
|
||||||
|
[SERVE, "--model", MODEL, "--port", "7770", "--jinja",
|
||||||
|
"--device", "ROCm0", "--gpu-layers", "999", "--fit", "off",
|
||||||
|
"--threads", "16", "--threads-batch", str(tb),
|
||||||
|
"--parallel", "2", "--ctx-size", "524288",
|
||||||
|
"--batch-size", "4096", "--ubatch-size", "4096",
|
||||||
|
"--flash-attn", "on",
|
||||||
|
"--cache-type-k", "f16", "--cache-type-v", "f16",
|
||||||
|
"--cache-prompt", "--cache-idle-slots", "--metrics"],
|
||||||
|
env=ENV, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||||
|
|
||||||
|
ready = wait_ready()
|
||||||
|
if not ready:
|
||||||
|
srv.terminate()
|
||||||
|
srv.wait()
|
||||||
|
out = srv.stdout.read(1000)
|
||||||
|
print(f"Server failed: {out[:300]}")
|
||||||
|
sys.exit(1)
|
||||||
|
print("Server ready, running bench...", flush=True)
|
||||||
|
|
||||||
|
t0 = time.time()
|
||||||
|
try:
|
||||||
|
# Use n-gen=0 to get prefill-only throughput
|
||||||
|
res = subprocess.run(
|
||||||
|
[BENCH, "--model", MODEL, "--threads", "16",
|
||||||
|
"--n-prompt", "65536", "--n-gen", "0",
|
||||||
|
"--repetitions", "3", "--output", "csv",
|
||||||
|
"--flash-attn", "on", "--n-gpu-layers", "999"],
|
||||||
|
env=ENV, capture_output=True, text=True, timeout=600)
|
||||||
|
stdout = res.stdout + res.stderr
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print("Bench timed out!")
|
||||||
|
srv.terminate()
|
||||||
|
results.append({"tb": tb, "prefill": None, "decode": None, "ctx": None, "time": 600})
|
||||||
|
continue
|
||||||
|
|
||||||
|
elapsed = round(time.time() - t0, 1)
|
||||||
|
print(f"Bench rc={res.returncode} stdout_len={len(res.stdout)} stderr_len={len(res.stderr)}", flush=True)
|
||||||
|
if res.returncode != 0:
|
||||||
|
print(f"STDERR: {res.stderr[:500]}", flush=True)
|
||||||
|
if res.stdout.strip():
|
||||||
|
print(f"CSV OUTPUT:\n{res.stdout[:300]}", flush=True)
|
||||||
|
|
||||||
|
prefill, decode, ctx = parse_bench_output(res.stdout, res.stderr)
|
||||||
|
|
||||||
|
row = {"tb": tb, "prefill": prefill, "decode": decode, "ctx": ctx, "time": elapsed}
|
||||||
|
results.append(row)
|
||||||
|
|
||||||
|
pt = f"{prefill:.1f}" if prefill else "N/A"
|
||||||
|
dt = f"{decode:.1f}" if decode else "N/A"
|
||||||
|
print(f" prefill={pt} tok/s decode={dt} tok/s ctx={ctx} time={elapsed}s", flush=True)
|
||||||
|
|
||||||
|
srv.terminate()
|
||||||
|
srv.wait()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
print("\n==========================================", flush=True)
|
||||||
|
print("SUMMARY", flush=True)
|
||||||
|
print("==========================================", flush=True)
|
||||||
|
print(f"{'tb':>5} {'prefill':>12} {'decode':>12} {'ctx':>10} {'time':>8}", flush=True)
|
||||||
|
print("-" * 60, flush=True)
|
||||||
|
for r in results:
|
||||||
|
pt = f"{r['prefill']:.1f}" if r["prefill"] else "N/A"
|
||||||
|
dt = f"{r['decode']:.1f}" if r["decode"] else "N/A"
|
||||||
|
print(f"{r['tb']:>5} {pt:>12} {dt:>12} {str(r['ctx']):>10} {r['time']:>8.0f}", flush=True)
|
||||||
|
|
||||||
|
best = max((r for r in results if r["prefill"]), key=lambda r: r["prefill"])
|
||||||
|
print(f"\nBest: threads-batch={best['tb']} ({best['prefill']:.1f} tok/s)", flush=True)
|
||||||
|
|
||||||
|
with open("/tmp/bench_tb_results.txt", "w") as f:
|
||||||
|
for r in results:
|
||||||
|
pt = str(r["prefill"]) if r["prefill"] else "N/A"
|
||||||
|
dt = str(r["decode"]) if r["decode"] else "N/A"
|
||||||
|
f.write(f"{r['tb']},{pt},{dt},{r['ctx']},{r['time']}\n")
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# Runtime: FLM 1.0.1 on the XDNA2 NPU (50 TOPS). Model: Gemma4-E4B-IT
|
# Runtime: FLM 1.0.1 on the XDNA2 NPU (50 TOPS). Model: Gemma4-E4B-IT
|
||||||
# (8B, Q4_1, vision + audio + reasoning). The NPU allocates from the
|
# (8B, Q4_1, vision + audio + reasoning). The NPU allocates from the
|
||||||
# unified pool shared with frank-default-large (the CIRU llama.cpp
|
# unified pool shared with frank-default-large (the CIRU llama.cpp
|
||||||
# service on the iGPU). See docs/FastFlowLM NPU Inference.md.
|
# service on the iGPU). See serverAppConfigs-docs/FastFlowLM NPU Inference.md.
|
||||||
#
|
#
|
||||||
# 2026-08-20: gemma4-it:e4b deployed. Footprint ~9.1 GB. 64k context.
|
# 2026-08-20: gemma4-it:e4b deployed. Footprint ~9.1 GB. 64k context.
|
||||||
|
|
||||||
|
|||||||
Executable
+408
@@ -0,0 +1,408 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate two ~64k token prompts for n-gram spec benchmarking.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/generate-prompts.py <output_dir>
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
<output_dir>/prompt_code.txt — code-heavy (~64k tokens)
|
||||||
|
<output_dir>/prompt_prose.txt — prose-heavy (~64k tokens)
|
||||||
|
|
||||||
|
The prompts are designed to be structurally different so we can observe
|
||||||
|
whether n-gram speculative decoding helps more on one type than the other.
|
||||||
|
|
||||||
|
Code prompt: structured, algorithmic, repetitive patterns (good for n-gram).
|
||||||
|
Prose prompt: natural language, varied vocabulary (poor match for n-gram).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Rough token estimate: 1 token ≈ 4 chars for English/code
|
||||||
|
TOKEN_CHAR_RATIO = 4.0
|
||||||
|
TARGET_TOKENS = 64000
|
||||||
|
TARGET_CHARS = int(TARGET_TOKENS * TOKEN_CHAR_RATIO)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_code_prompt():
|
||||||
|
"""Generate a code-heavy prompt with structured, repetitive patterns."""
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
# Section 1: Python algorithms (well-structured, good for n-gram matching)
|
||||||
|
algorithms = [
|
||||||
|
("Merge Sort", """
|
||||||
|
def merge_sort(arr):
|
||||||
|
\"\"\"Standard merge sort implementation.\"\"\"
|
||||||
|
if len(arr) <= 1:
|
||||||
|
return arr
|
||||||
|
mid = len(arr) // 2
|
||||||
|
left = merge_sort(arr[:mid])
|
||||||
|
right = merge_sort(arr[mid:])
|
||||||
|
return merge(left, right)
|
||||||
|
|
||||||
|
def merge(left, right):
|
||||||
|
\"\"\"Merge two sorted arrays.\"\"\"
|
||||||
|
result = []
|
||||||
|
i = j = 0
|
||||||
|
while i < len(left) and j < len(right):
|
||||||
|
if left[i] <= right[j]:
|
||||||
|
result.append(left[i])
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
result.append(right[j])
|
||||||
|
j += 1
|
||||||
|
result.extend(left[i:])
|
||||||
|
result.extend(right[j:])
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Test merge sort
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import random
|
||||||
|
test_data = [random.randint(1, 1000) for _ in range(1000)]
|
||||||
|
sorted_data = merge_sort(test_data)
|
||||||
|
assert sorted_data == sorted(test_data)
|
||||||
|
print(f"Merge sort passed: {len(test_data)} items in {len(sorted_data)}")
|
||||||
|
"""),
|
||||||
|
("Binary Search", """
|
||||||
|
def binary_search(arr, target):
|
||||||
|
\"\"\"Binary search in a sorted array.\"\"\"
|
||||||
|
low, high = 0, len(arr) - 1
|
||||||
|
while low <= high:
|
||||||
|
mid = (low + high) // 2
|
||||||
|
if arr[mid] == target:
|
||||||
|
return mid
|
||||||
|
elif arr[mid] < target:
|
||||||
|
low = mid + 1
|
||||||
|
else:
|
||||||
|
high = mid - 1
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# Test binary search
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import random
|
||||||
|
test_data = sorted(random.sample(range(1, 10000), 500))
|
||||||
|
for target in [test_data[0], test_data[-1], test_data[250], -1]:
|
||||||
|
idx = binary_search(test_data, target)
|
||||||
|
if target in test_data:
|
||||||
|
assert test_data[idx] == target
|
||||||
|
else:
|
||||||
|
assert idx == -1
|
||||||
|
print(f"Binary search passed: {len(test_data)} items")
|
||||||
|
"""),
|
||||||
|
("DFS/BFS", """
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
def bfs(graph, start):
|
||||||
|
\"\"\"Breadth-first search.\"\"\"
|
||||||
|
visited = set()
|
||||||
|
queue = deque([start])
|
||||||
|
visited.add(start)
|
||||||
|
order = []
|
||||||
|
while queue:
|
||||||
|
node = queue.popleft()
|
||||||
|
order.append(node)
|
||||||
|
for neighbor in sorted(graph.get(node, [])):
|
||||||
|
if neighbor not in visited:
|
||||||
|
visited.add(neighbor)
|
||||||
|
queue.append(neighbor)
|
||||||
|
return order
|
||||||
|
|
||||||
|
def dfs(graph, start, visited=None):
|
||||||
|
\"\"\"Depth-first search (iterative).\"\"\"
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
stack = [start]
|
||||||
|
order = []
|
||||||
|
while stack:
|
||||||
|
node = stack.pop()
|
||||||
|
if node not in visited:
|
||||||
|
visited.add(node)
|
||||||
|
order.append(node)
|
||||||
|
for neighbor in reversed(sorted(graph.get(node, []))):
|
||||||
|
if neighbor not in visited:
|
||||||
|
stack.append(neighbor)
|
||||||
|
return order
|
||||||
|
|
||||||
|
# Test graph traversal
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import random
|
||||||
|
nodes = list(range(50))
|
||||||
|
graph = {n: random.sample(nodes, random.randint(1, 5)) for n in nodes}
|
||||||
|
bfs_order = bfs(graph, 0)
|
||||||
|
dfs_order = dfs(graph, 0)
|
||||||
|
print(f"BFS visited {len(bfs_order)} nodes, DFS visited {len(dfs_order)} nodes")
|
||||||
|
"""),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, code in algorithms:
|
||||||
|
parts.append(f"# === {name} ===\n")
|
||||||
|
parts.append(code)
|
||||||
|
|
||||||
|
# Section 2: Large data generation (repetitive structure)
|
||||||
|
parts.append("\n# === Data Generation ===\n")
|
||||||
|
parts.append("import json\nimport csv\nimport hashlib\n\n")
|
||||||
|
|
||||||
|
for i in range(200):
|
||||||
|
parts.append(f"# Batch {i}\n")
|
||||||
|
parts.append(f"record_{i} = {{\n")
|
||||||
|
parts.append(f" 'id': {i},\n")
|
||||||
|
parts.append(f" 'name': 'record_{i}',\n")
|
||||||
|
parts.append(f" 'value': {i * 3.14159},\n")
|
||||||
|
parts.append(f" 'hash': hashlib.md5(b'record_{i}').hexdigest(),\n")
|
||||||
|
parts.append(f" 'nested': {{\n")
|
||||||
|
for j in range(10):
|
||||||
|
parts.append(f" 'key_{j}': {i * j},\n")
|
||||||
|
parts.append(f" }}\n")
|
||||||
|
parts.append(f"}}\n")
|
||||||
|
parts.append(f"batch_{i}.append(record_{i})\n")
|
||||||
|
|
||||||
|
# Section 3: Configuration schema (highly repetitive, great for n-gram)
|
||||||
|
parts.append("\n# === Configuration Schema ===\n")
|
||||||
|
for i in range(50):
|
||||||
|
parts.append(f"schema_{i} = {{\n")
|
||||||
|
parts.append(f" 'type': 'object',\n")
|
||||||
|
parts.append(f" 'properties': {{\n")
|
||||||
|
for j in range(20):
|
||||||
|
parts.append(f" 'field_{j}': {{\n")
|
||||||
|
parts.append(f" 'type': 'string' if {j} % 2 else 'integer',\n")
|
||||||
|
parts.append(f" 'required': {j} < 10,\n")
|
||||||
|
parts.append(f" 'default': '{i}_{j}',\n")
|
||||||
|
parts.append(f" 'description': 'Field {j} of schema {i}',\n")
|
||||||
|
parts.append(f" }},\n")
|
||||||
|
parts.append(f" }},\n")
|
||||||
|
parts.append(f" 'required': ['field_0', 'field_1', 'field_2'],\n")
|
||||||
|
parts.append(f" 'additionalProperties': False,\n")
|
||||||
|
parts.append(f"}}\n")
|
||||||
|
|
||||||
|
# Section 4: SQL queries (structured, repetitive)
|
||||||
|
parts.append("\n# === SQL Queries ===\n")
|
||||||
|
for i in range(50):
|
||||||
|
parts.append(f"-- Query batch {i}\n")
|
||||||
|
parts.append(f"SELECT * FROM table_{i}\n")
|
||||||
|
parts.append(f"WHERE col_{i} = {i}\n")
|
||||||
|
parts.append(f"ORDER BY id\n")
|
||||||
|
parts.append(f"LIMIT {i * 100};\n\n")
|
||||||
|
parts.append(f"INSERT INTO table_{i} (id, name, value)\n")
|
||||||
|
parts.append(f"VALUES ({i}, 'record_{i}', {i});\n\n")
|
||||||
|
parts.append(f"UPDATE table_{i}\n")
|
||||||
|
parts.append(f"SET value = {i} * 1.5\n")
|
||||||
|
parts.append(f"WHERE id = {i};\n\n")
|
||||||
|
|
||||||
|
# Section 5: Documentation (structured, repetitive)
|
||||||
|
parts.append("\n# === Documentation ===\n")
|
||||||
|
for i in range(50):
|
||||||
|
parts.append(f"## Section {i}\n\n")
|
||||||
|
parts.append(f"### Overview\n")
|
||||||
|
parts.append(f"This section covers topic {i} in detail. The purpose is to\n")
|
||||||
|
parts.append(f"document the behavior of the system for component {i}.\n\n")
|
||||||
|
parts.append(f"### Implementation\n")
|
||||||
|
parts.append(f"The implementation uses a modular approach with {i} subcomponents.\n")
|
||||||
|
parts.append(f"Each subcomponent handles a specific aspect of the functionality.\n\n")
|
||||||
|
parts.append(f"### Testing\n")
|
||||||
|
parts.append(f"Component {i} is tested with {i * 10} test cases.\n")
|
||||||
|
parts.append(f"All tests pass with 100% coverage.\n\n")
|
||||||
|
parts.append(f"### References\n")
|
||||||
|
parts.append(f"- Reference {i}.1: Component specification\n")
|
||||||
|
parts.append(f"- Reference {i}.2: Implementation guide\n")
|
||||||
|
parts.append(f"- Reference {i}.3: Test documentation\n\n")
|
||||||
|
|
||||||
|
# Fill remaining content with more code blocks
|
||||||
|
while sum(len(p) for p in parts) < TARGET_CHARS:
|
||||||
|
parts.append(f"# Additional block\n")
|
||||||
|
parts.append(f"def extra_{len(parts)}():\n")
|
||||||
|
parts.append(f" \"\"\"Additional function {len(parts)}.\"\"\"\n")
|
||||||
|
parts.append(f" x = {len(parts)}\n")
|
||||||
|
parts.append(f" y = x * 2\n")
|
||||||
|
parts.append(f" return x + y\n\n")
|
||||||
|
|
||||||
|
return "\n".join(parts)[:TARGET_CHARS]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_prose_prompt():
|
||||||
|
"""Generate a prose-heavy prompt with varied vocabulary."""
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
vocabulary = [
|
||||||
|
# Theme 1: Historical narrative
|
||||||
|
("The Renaissance", [
|
||||||
|
"The dawn of the Renaissance brought profound transformation to European civilization.",
|
||||||
|
"Artists like Leonardo da Vinci and Michelangelo pushed the boundaries of human expression.",
|
||||||
|
"The invention of the printing press by Gutenberg revolutionized the spread of knowledge.",
|
||||||
|
"Scholars in Florence and Venice debated philosophy, science, and the nature of the cosmos.",
|
||||||
|
"The Medici family funded countless artists, scholars, and architects.",
|
||||||
|
"The rediscovery of classical Greek and Roman texts ignited intellectual renewal.",
|
||||||
|
"Architecture shifted from the vertical aspiration of Gothic to the proportional harmony of classical orders.",
|
||||||
|
"Patronage became a symbol of civic virtue and political power.",
|
||||||
|
"The humanist movement placed human experience and reason at the center of intellectual inquiry.",
|
||||||
|
"Mathematics, anatomy, and natural philosophy advanced through empirical observation.",
|
||||||
|
]),
|
||||||
|
# Theme 2: Scientific exposition
|
||||||
|
("Quantum Mechanics", [
|
||||||
|
"Quantum mechanics describes the behavior of matter and energy at atomic scales.",
|
||||||
|
"The Schrödinger equation governs the evolution of quantum states over time.",
|
||||||
|
"Wave-particle duality reveals that light and matter exhibit both wave and particle properties.",
|
||||||
|
"The uncertainty principle limits the precision with which conjugate variables can be measured.",
|
||||||
|
"Quantum entanglement creates correlations between particles that persist across any distance.",
|
||||||
|
"The Copenhagen interpretation posits that measurement collapses the wave function.",
|
||||||
|
"Superposition allows quantum systems to exist in multiple states simultaneously until measured.",
|
||||||
|
"Quantum tunneling enables particles to traverse classically forbidden energy barriers.",
|
||||||
|
"The double-slit experiment demonstrates interference patterns from single particles.",
|
||||||
|
"Quantum computing leverages superposition and entanglement for computational advantage.",
|
||||||
|
]),
|
||||||
|
# Theme 3: Philosophical discourse
|
||||||
|
("Ethics and Moral Reasoning", [
|
||||||
|
"Moral philosophy grapples with questions of right and wrong, virtue and vice.",
|
||||||
|
"Utilitarianism judges actions by their consequences and the happiness they produce.",
|
||||||
|
"Deontological ethics emphasizes duties and rules rather than outcomes.",
|
||||||
|
"Virtue ethics focuses on character development and flourishing rather than specific actions.",
|
||||||
|
"The social contract theory posits that moral obligations arise from collective agreement.",
|
||||||
|
"Moral skepticism challenges the possibility of objective moral knowledge.",
|
||||||
|
"The problem of evil questions the compatibility of an omnipotent, benevolent deity with suffering.",
|
||||||
|
"Free will debates center on whether human choices are genuinely open or determined.",
|
||||||
|
"Moral relativism holds that ethical standards are culture-dependent rather than universal.",
|
||||||
|
"The trolley dilemma illustrates tensions between utilitarian and deontological reasoning.",
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
for theme_name, sentences in vocabulary:
|
||||||
|
parts.append(f"## {theme_name}\n\n")
|
||||||
|
for sentence in sentences:
|
||||||
|
parts.append(f"{sentence} ")
|
||||||
|
parts.append("\n\n")
|
||||||
|
|
||||||
|
# Section 2: Extended narrative with varied styles
|
||||||
|
parts.append("## Extended Narrative\n\n")
|
||||||
|
|
||||||
|
narratives = [
|
||||||
|
"The city stretched endlessly beneath a sky the color of television tuned to a dead channel.",
|
||||||
|
"Mountains rose like ancient sentinels, their peaks lost in clouds that swirled and danced.",
|
||||||
|
"The ocean whispered secrets to the shore, each wave carrying fragments of distant lands.",
|
||||||
|
"In the quiet library, dust motes drifted through shafts of amber light filtering through stained glass.",
|
||||||
|
"The market bustled with vendors calling out prices, the air thick with spices and laughter.",
|
||||||
|
"Silent forests stretched for miles, their canopies forming a green cathedral overhead.",
|
||||||
|
"The old lighthouse stood solitary against the storm, its beam cutting through the darkness.",
|
||||||
|
"Rivers carved paths through valleys over millennia, patient and relentless.",
|
||||||
|
"The desert shimmered in the heat, mirages dancing on the horizon like phantom cities.",
|
||||||
|
"Snow fell silently on the rooftops, transforming the town into a winter wonderland.",
|
||||||
|
"The ancient ruins whispered stories of a civilization long forgotten by the modern world.",
|
||||||
|
"Stars scattered across the night sky like diamonds on velvet, billions of them.",
|
||||||
|
"The forest floor was carpeted with ferns and moss, damp and fragrant with earth.",
|
||||||
|
"Clocks ticked away in the old tower, each chime echoing through the narrow streets.",
|
||||||
|
"The garden bloomed in an explosion of color, roses, tulips, and lavender in perfect arrangement.",
|
||||||
|
"The train rattled through the countryside, passengers gazing at fields of golden wheat.",
|
||||||
|
"Mountains echoed with the calls of eagles circling on thermal currents above.",
|
||||||
|
"The village square hosted a weekly market where farmers sold their harvest and artisans displayed their crafts.",
|
||||||
|
"A lone wolf howled in the distance, its mournful cry carrying across the valley.",
|
||||||
|
"The cathedral vaulted overhead, ribbed arches converging at a central keystone.",
|
||||||
|
]
|
||||||
|
|
||||||
|
for narrative in narratives:
|
||||||
|
parts.append(f"{narrative} ")
|
||||||
|
|
||||||
|
# Section 3: Generate more content by varying the narratives
|
||||||
|
extensions = [
|
||||||
|
" Scholars have debated the implications of this phenomenon for decades, producing conflicting interpretations.",
|
||||||
|
" Local legends claim that those who listen carefully can hear voices from centuries past.",
|
||||||
|
" Researchers continue to study the effects, though evidence remains inconclusive.",
|
||||||
|
" The architecture reflects a synthesis of influences from multiple cultures and time periods.",
|
||||||
|
" Travelers who venture far enough often lose their sense of direction and time.",
|
||||||
|
" The tradition has been passed down through generations, each adding their own interpretation.",
|
||||||
|
" Modern technology has made it possible to study these phenomena with unprecedented precision.",
|
||||||
|
" The natural beauty of the area attracts visitors from around the world seeking tranquility.",
|
||||||
|
" Archaeologists uncovered artifacts dating back thousands of years, revealing a rich history.",
|
||||||
|
" The climate has changed significantly over the centuries, altering the landscape dramatically.",
|
||||||
|
" Scientists proposed multiple hypotheses to explain the observed patterns, each with varying degrees of support.",
|
||||||
|
" The cultural significance of the site extends far beyond its physical boundaries.",
|
||||||
|
" Visitors report feeling a profound sense of peace and connection to something greater.",
|
||||||
|
" The geological formation resulted from a combination of volcanic activity and erosion over millions of years.",
|
||||||
|
" The literature from this period provides insight into the social and political conditions of the time.",
|
||||||
|
" Artists found inspiration in the dramatic landscapes, producing works that captured their essence.",
|
||||||
|
" The discovery challenged established theories and prompted a reevaluation of fundamental assumptions.",
|
||||||
|
" The community worked together to preserve the site, recognizing its importance to their heritage.",
|
||||||
|
" Researchers used advanced imaging techniques to reveal hidden structures beneath the surface.",
|
||||||
|
" The story has been retold in countless forms, from oral traditions to modern cinema.",
|
||||||
|
]
|
||||||
|
|
||||||
|
for extension in extensions:
|
||||||
|
parts.append(extension + " ")
|
||||||
|
|
||||||
|
# Section 4: More narrative variations
|
||||||
|
parts.append("\n\n## Additional Observations\n\n")
|
||||||
|
|
||||||
|
observations = [
|
||||||
|
"The interplay between light and shadow creates a dynamic visual experience that changes throughout the day.",
|
||||||
|
"Historical records indicate that this practice dates back to the early medieval period.",
|
||||||
|
"The geological composition of the region provides valuable insights into planetary formation processes.",
|
||||||
|
"Cultural traditions in the area reflect a complex interweaving of indigenous and immigrant influences.",
|
||||||
|
"The ecological balance of the ecosystem depends on a delicate network of symbiotic relationships.",
|
||||||
|
"Technological advances have dramatically altered the way we understand and interact with the natural world.",
|
||||||
|
"The linguistic diversity of the region is remarkable, with dozens of languages and dialects spoken.",
|
||||||
|
"Astronomical observations from this location have contributed significantly to our understanding of the cosmos.",
|
||||||
|
"The artistic movement that originated here influenced creative expression across multiple continents.",
|
||||||
|
"Economic factors played a crucial role in shaping the political landscape of the era.",
|
||||||
|
]
|
||||||
|
|
||||||
|
for obs in observations:
|
||||||
|
parts.append(f"{obs} ")
|
||||||
|
|
||||||
|
# Section 5: Extended descriptions with varied sentence structures
|
||||||
|
parts.append("\n\n## Detailed Descriptions\n\n")
|
||||||
|
|
||||||
|
descriptions = [
|
||||||
|
("The coastal town of Portmore sat nestled between steep cliffs and a sheltered bay, its whitewashed houses cascading down to a harbor filled with fishing boats and pleasure craft. The smell of salt and fish hung in the air, mingling with the aroma of fresh bread from the bakery on Main Street. Every morning at dawn, the fishermen would set out, their boats cutting through the glassy water, returning by midday with their catch. The market would be alive with the sounds of haggling, the glint of silver scales in the sunlight, and the colorful piles of fish laid out on ice. In the afternoons, tourists would wander the narrow streets, browsing artisan shops and sampling local wines at small tavernas. The old fortification on the headland offered panoramic views of the coastline, stretching for miles in both directions, with the open sea beyond.",
|
||||||
|
"Nestled in the foothills of the mountain range, the village of Eldermere had remained largely unchanged for centuries. Its cobblestone streets wound between timber-framed houses with thatched roofs, their gardens overflowing with herbs and flowers. The village center was dominated by a medieval church with a stone tower that had been added during the fourteenth century. The parish priest, Father Thomas, had served the community for over three decades, and the villagers trusted him implicitly. On Sundays, the church would be filled to capacity, and the choir would sing hymns that echoed through the vaulted ceiling. Outside, the hills were covered in pastures where sheep grazed contentedly, tended by shepherds who knew every animal by name. The seasonal rhythms of the village followed the agricultural calendar: planting in spring, harvest in autumn, and a brief period of rest in winter. Local festivals celebrated these transitions with music, dance, and feasting that brought the community together."),
|
||||||
|
]
|
||||||
|
|
||||||
|
for desc in descriptions:
|
||||||
|
parts.append(f"{desc} ")
|
||||||
|
|
||||||
|
# Fill remaining to reach target
|
||||||
|
while sum(len(p) for p in parts) < TARGET_CHARS:
|
||||||
|
parts.append(f"{random.choice([
|
||||||
|
"The morning sun cast long shadows across the landscape, illuminating every detail with warm, golden light.",
|
||||||
|
"In the silence that followed, the weight of what had been said hung heavy in the air between them.",
|
||||||
|
"The old manuscript had survived centuries of conflict, fire, and neglect, its pages yellowed but still legible.",
|
||||||
|
"Scientists had been studying the phenomenon for years, yet its underlying mechanisms remained poorly understood.",
|
||||||
|
"The city's architecture told the story of its evolution, from medieval fortifications to modern skyscrapers.",
|
||||||
|
"Every season brought its own character to the valley, transforming the landscape in subtle but dramatic ways.",
|
||||||
|
"The debate raged on, scholars on all sides presenting arguments and counterarguments with fierce conviction.",
|
||||||
|
"The ancient tree stood as a testament to endurance, its gnarled branches reaching skyward after thousands of years.",
|
||||||
|
"Cultural exchange between neighboring regions had enriched both societies, leading to innovations in art and technology.",
|
||||||
|
"The discovery of the artifact reshaped our understanding of the civilization that had created it.",
|
||||||
|
])} ")
|
||||||
|
|
||||||
|
return "\n".join(parts)[:TARGET_CHARS]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} <output_dir>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
output_dir = sys.argv[1]
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"Generating prompts...")
|
||||||
|
|
||||||
|
code_prompt = generate_code_prompt()
|
||||||
|
prose_prompt = generate_prose_prompt()
|
||||||
|
|
||||||
|
code_path = os.path.join(output_dir, "prompt_code.txt")
|
||||||
|
prose_path = os.path.join(output_dir, "prompt_prose.txt")
|
||||||
|
|
||||||
|
with open(code_path, "w") as f:
|
||||||
|
f.write(code_prompt)
|
||||||
|
with open(prose_path, "w") as f:
|
||||||
|
f.write(prose_prompt)
|
||||||
|
|
||||||
|
# Estimate token counts
|
||||||
|
code_tokens = len(code_prompt) // TOKEN_CHAR_RATIO
|
||||||
|
prose_tokens = len(prose_prompt) // TOKEN_CHAR_RATIO
|
||||||
|
|
||||||
|
print(f"Code prompt: {code_tokens:.0f} estimated tokens ({len(code_prompt):,} bytes) → {code_path}")
|
||||||
|
print(f"Prose prompt: {prose_tokens:.0f} estimated tokens ({len(prose_prompt):,} bytes) → {prose_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# (XDNA2, 50 TOPS) is a single-lane device: generations serialize in FIFO
|
# (XDNA2, 50 TOPS) is a single-lane device: generations serialize in FIFO
|
||||||
# order, so this is a background/triage endpoint. The NPU shares the ~30 GiB
|
# order, so this is a background/triage endpoint. The NPU shares the ~30 GiB
|
||||||
# unified pool with frank-default-large (the CIRU llama.cpp service on the
|
# unified pool with frank-default-large (the CIRU llama.cpp service on the
|
||||||
# iGPU); see docs/FastFlowLM NPU Inference.md for the memory budget.
|
# iGPU); see serverAppConfigs-docs/FastFlowLM NPU Inference.md for the memory budget.
|
||||||
#
|
#
|
||||||
# NOTE: FLM requires unlimited memlock. The unit sets LimitMEMLOCK=infinity.
|
# NOTE: FLM requires unlimited memlock. The unit sets LimitMEMLOCK=infinity.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|||||||
+10
-1
@@ -164,7 +164,7 @@ for path in "${CHANGED[@]}"; do
|
|||||||
scripts/*)
|
scripts/*)
|
||||||
add_test tests/scripts_syntax.bats
|
add_test tests/scripts_syntax.bats
|
||||||
;;
|
;;
|
||||||
docs/*)
|
serverAppConfigs-docs/*)
|
||||||
add_test tests/docs_completeness.bats
|
add_test tests/docs_completeness.bats
|
||||||
add_test tests/deployment_scoping.bats
|
add_test tests/deployment_scoping.bats
|
||||||
;;
|
;;
|
||||||
@@ -180,6 +180,15 @@ for path in "${CHANGED[@]}"; do
|
|||||||
.AGENTTRASH/*)
|
.AGENTTRASH/*)
|
||||||
# Trash moves are not testable; skip.
|
# Trash moves are not testable; skip.
|
||||||
;;
|
;;
|
||||||
|
.gitignore|.env.example)
|
||||||
|
# Ignore/ignore-list changes change nothing at runtime; not testable.
|
||||||
|
;;
|
||||||
|
old/scripts/*|old/scripts/**)
|
||||||
|
add_test tests/scripts_syntax.bats
|
||||||
|
;;
|
||||||
|
report/*)
|
||||||
|
# Frozen audit reports are point-in-time artifacts; not testable.
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
die "no focused-test mapping for '$path'; add one before committing"
|
die "no focused-test mapping for '$path'; add one before committing"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ graph LR
|
|||||||
|
|
||||||
The last two rows are bare-metal systemd services, not containers. This
|
The last two rows are bare-metal systemd services, not containers. This
|
||||||
repo owns their scripts, units, and knowledge base (see
|
repo owns their scripts, units, and knowledge base (see
|
||||||
[docs/LLM/index.md](../LLM/index.md)). Current mapping (2026-08-22):
|
[serverAppConfigs-docs/LLM/index.md](../LLM/index.md)). Current mapping (2026-08-22):
|
||||||
`frank-default-large` runs the CIRU ROCmFPX llama.cpp on Dankclaw;
|
`frank-default-large` runs the CIRU ROCmFPX llama.cpp on Dankclaw;
|
||||||
`frank-default-small` runs the upstream llama.cpp on Dankserver.
|
`frank-default-small` runs the upstream llama.cpp on Dankserver.
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ The containers share the private bridge network `systemd-gitea`. The quadlet net
|
|||||||
| `dank/serverAppConfigs` | this repo — Podman stacks + LLM fleet |
|
| `dank/serverAppConfigs` | this repo — Podman stacks + LLM fleet |
|
||||||
| `dank/vllmScripts` | retired 2026-08-22 — merged into `dank/serverAppConfigs` (history preserved) |
|
| `dank/vllmScripts` | retired 2026-08-22 — merged into `dank/serverAppConfigs` (history preserved) |
|
||||||
|
|
||||||
The LLM fleet knowledge base: [docs/LLM/index.md](../LLM/index.md)
|
The LLM fleet knowledge base: [serverAppConfigs-docs/LLM/index.md](../LLM/index.md)
|
||||||
|
|
||||||
## Management
|
## Management
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ The build is **full TDD** (Q36–Q38). Tests exist before the code: the checker
|
|||||||
|
|
||||||
## Per-service docs (separate workstream)
|
## Per-service docs (separate workstream)
|
||||||
|
|
||||||
Each service gets a docs page at `/docs/<svc>/` (Q30). The content is a **how-to for a regular user** — no maintenance details (Q29). The pages are password-protected by the same auth as the homepage section they belong to (Q31). They are built as each service goes active (Q32). The `docs` link on a service points to its page. The existing reference docs in `docs/` are not linked from the homepage. They may contain secrets (Q22).
|
Each service gets a docs page at `/docs/<svc>/` (Q30). The content is a **how-to for a regular user** — no maintenance details (Q29). The pages are password-protected by the same auth as the homepage section they belong to (Q31). They are built as each service goes active (Q32). The `docs` link on a service points to its page. The existing reference docs in `serverAppConfigs-docs/` are not linked from the homepage. They may contain secrets (Q22).
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
+9
-9
@@ -81,7 +81,7 @@ llm = LLM(
|
|||||||
|
|
||||||
Both `MultiprocExecutor` and `RayDistributedExecutor` report `supports_pp: True` (`vllm/v1/executor/multiproc_executor.py` line ~88, `vllm/v1/executor/ray_executor.py` line ~67).
|
Both `MultiprocExecutor` and `RayDistributedExecutor` report `supports_pp: True` (`vllm/v1/executor/multiproc_executor.py` line ~88, `vllm/v1/executor/ray_executor.py` line ~67).
|
||||||
|
|
||||||
**Ray cluster setup** (`docs/examples/ray_serving/multi-node-serving.sh`):
|
**Ray cluster setup** (`serverAppConfigs-docs/examples/ray_serving/multi-node-serving.sh`):
|
||||||
```bash
|
```bash
|
||||||
# Head node:
|
# Head node:
|
||||||
ray start --head --port=6379
|
ray start --head --port=6379
|
||||||
@@ -89,7 +89,7 @@ ray start --head --port=6379
|
|||||||
ray start --address='<HEAD_IP>:6379'
|
ray start --address='<HEAD_IP>:6379'
|
||||||
```
|
```
|
||||||
|
|
||||||
**Source:** `docs/serving/parallelism_scaling.md`, `vllm/config/parallel.py`, `vllm/engine/arg_utils.py`
|
**Source:** `serverAppConfigs-docs/serving/parallelism_scaling.md`, `vllm/config/parallel.py`, `vllm/engine/arg_utils.py`
|
||||||
|
|
||||||
### 2. Pipeline Parallelism Split Mechanics
|
### 2. Pipeline Parallelism Split Mechanics
|
||||||
|
|
||||||
@@ -113,11 +113,11 @@ ray start --address='<HEAD_IP>:6379'
|
|||||||
|
|
||||||
**Cross-node PP data:** Hidden-state tensors (batch × seq_len × hidden_dim) are sent point-to-point between stages. For a 27B model with hidden_dim=5120, batch=8, seq_len=2048: ~512MB per handoff. This happens once per forward pass per stage boundary.
|
**Cross-node PP data:** Hidden-state tensors (batch × seq_len × hidden_dim) are sent point-to-point between stages. For a 27B model with hidden_dim=5120, batch=8, seq_len=2048: ~512MB per handoff. This happens once per forward pass per stage boundary.
|
||||||
|
|
||||||
**Source:** `docs/serving/parallelism_scaling.md`, `docs/features/pipeline_parallel.md`, `vllm/v1/executor/multiproc_executor.py`, PR #33960
|
**Source:** `serverAppConfigs-docs/serving/parallelism_scaling.md`, `serverAppConfigs-docs/features/pipeline_parallel.md`, `vllm/v1/executor/multiproc_executor.py`, PR #33960
|
||||||
|
|
||||||
### 3. Interconnect Requirements
|
### 3. Interconnect Requirements
|
||||||
|
|
||||||
**Official vLLM guidance** (from `docs/serving/parallelism_scaling.md` and the blog):
|
**Official vLLM guidance** (from `serverAppConfigs-docs/serving/parallelism_scaling.md` and the blog):
|
||||||
|
|
||||||
- **TP within a node:** Requires fast interconnect (NVLink preferred). Over PCIe/Ethernet, TP AllReduce dominates and throughput collapses.
|
- **TP within a node:** Requires fast interconnect (NVLink preferred). Over PCIe/Ethernet, TP AllReduce dominates and throughput collapses.
|
||||||
- **PP across nodes:** Tolerates slow links. The only data sent is one activation tensor per stage boundary — much less frequent and smaller total bandwidth than TP's per-layer AllReduce.
|
- **PP across nodes:** Tolerates slow links. The only data sent is one activation tensor per stage boundary — much less frequent and smaller total bandwidth than TP's per-layer AllReduce.
|
||||||
@@ -131,7 +131,7 @@ ray start --address='<HEAD_IP>:6379'
|
|||||||
|
|
||||||
**Key difference from TP:** TP does an AllReduce after every attention block and every MLP block — ~160 syncs per forward pass for an 80-layer model. PP does one send/recv per stage per forward pass. A 10GbE link handles ~1.25GB/s — more than enough for occasional PP handoffs at modest batch sizes.
|
**Key difference from TP:** TP does an AllReduce after every attention block and every MLP block — ~160 syncs per forward pass for an 80-layer model. PP does one send/recv per stage per forward pass. A 10GbE link handles ~1.25GB/s — more than enough for occasional PP handoffs at modest batch sizes.
|
||||||
|
|
||||||
**Source:** `docs/serving/parallelism_scaling.md`, vLLM Blog (vllm.ai/blog/2025-02-17-distributed-inference), pkhamdee.blog/2026/06/30
|
**Source:** `serverAppConfigs-docs/serving/parallelism_scaling.md`, vLLM Blog (vllm.ai/blog/2025-02-17-distributed-inference), pkhamdee.blog/2026/06/30
|
||||||
|
|
||||||
### 4. Real Benchmarks
|
### 4. Real Benchmarks
|
||||||
|
|
||||||
@@ -185,11 +185,11 @@ ray start --address='<HEAD_IP>:6379'
|
|||||||
1. KV cache is split across stages (each stage has less memory than if the full model were on one GPU)
|
1. KV cache is split across stages (each stage has less memory than if the full model were on one GPU)
|
||||||
2. PP bubble reduces throughput at low batch sizes
|
2. PP bubble reduces throughput at low batch sizes
|
||||||
|
|
||||||
**Source:** `vllm/v1/core/kv_cache_coordinator.py`, `docs/serving/parallelism_scaling.md`
|
**Source:** `vllm/v1/core/kv_cache_coordinator.py`, `serverAppConfigs-docs/serving/parallelism_scaling.md`
|
||||||
|
|
||||||
### 6. Speculative Decoding with PP
|
### 6. Speculative Decoding with PP
|
||||||
|
|
||||||
**Known incompatibility** (`docs/features/speculative_decoding/README.md`):
|
**Known incompatibility** (`serverAppConfigs-docs/features/speculative_decoding/README.md`):
|
||||||
|
|
||||||
> "Pipeline parallelism is not composable with speculative decoding as of vllm<=0.15.0"
|
> "Pipeline parallelism is not composable with speculative decoding as of vllm<=0.15.0"
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ ray start --address='<HEAD_IP>:6379'
|
|||||||
- DFlash (retired) was also an external drafter
|
- DFlash (retired) was also an external drafter
|
||||||
- n-gram internal speculation may work with PP since it doesn't need a separate model
|
- n-gram internal speculation may work with PP since it doesn't need a separate model
|
||||||
|
|
||||||
**Source:** `docs/features/speculative_decoding/README.md`, `vllm/config/speculative.py`
|
**Source:** `serverAppConfigs-docs/features/speculative_decoding/README.md`, `vllm/config/speculative.py`
|
||||||
|
|
||||||
## Verdict: 2-Node vLLM PP over Ethernet for 27B Model
|
## Verdict: 2-Node vLLM PP over Ethernet for 27B Model
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ ray start --address='<HEAD_IP>:6379'
|
|||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
- **Kept:** Parallelism and Scaling docs — vLLM official guidance on TP/PP strategy, `docs/serving/parallelism_scaling.md` — explains the canonical recipe (TP=gpus/node, PP=nodes), interconnect requirements, multi-node setup with Ray and mp
|
- **Kept:** Parallelism and Scaling docs — vLLM official guidance on TP/PP strategy, `serverAppConfigs-docs/serving/parallelism_scaling.md` — explains the canonical recipe (TP=gpus/node, PP=nodes), interconnect requirements, multi-node setup with Ray and mp
|
||||||
- **Kept:** vLLM Blog — Distributed Inference — explains TP/PP mechanisms, super-linear throughput from KV cache effects, PP lower communication overhead than TP
|
- **Kept:** vLLM Blog — Distributed Inference — explains TP/PP mechanisms, super-linear throughput from KV cache effects, PP lower communication overhead than TP
|
||||||
- **Kept:** pKhamdee.blog — Scaling LLM Inference DP/PP/TP with vLLM — detailed decision tree, benchmark comparisons, practical gotchas, TP vs PP communication costs
|
- **Kept:** pKhamdee.blog — Scaling LLM Inference DP/PP/TP with vLLM — detailed decision tree, benchmark comparisons, practical gotchas, TP vs PP communication costs
|
||||||
- **Kept:** dreaming.press — Tensor Parallelism vs Pipeline Parallelism — clear explanation of interconnect as the decision, NVLink vs Ethernet distinction
|
- **Kept:** dreaming.press — Tensor Parallelism vs Pipeline Parallelism — clear explanation of interconnect as the decision, NVLink vs Ethernet distinction
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user