Reproducible Docker/vLLM deployment, tuning, and benchmarks for DeepSeek V4.1 Flash on a switchless three-node DGX Spark fabric.
Python
21
690 commits
updated Oct 18, 2026
Reproducible Docker/vLLM deployment, tuning, and benchmarks for DeepSeek V4.1 Flash on a switchless three-node DGX Spark fabric, and the same deployment on a four-node ring.
The promoted kernel family is TileLang (r6): TileLang kernels for SM121,
with DeepSeek's TileKernels and the one-shot RoCE collectives of
sparknet, in place
of B12X's attention, linear, MoE, mHC, vocabulary and collective kernels
(kernel_backend: tilelang, see scripts/kernel_backend.py). B12X stays in
the image as the alternative backend; a configuration without
kernel_backend means B12X. The r6 promotion record
has both recipes and their benchmarks, and the
decode-kernel experiment
compares the two families kernel by kernel.
docs/fundamentals.md explains the model itself: GEMM
shapes, number formats, one decoder layer, attention, mHC, Engram, DSpark, and
how TP3 and TP4 split the layers.
The deployment tools also generate and validate a four-node switchless ring profile. The TP4 1M profile serves the checkpoint's full 1,048,576-token context with 16 sequences on the same image as the three-node deployment, and was benchmarked at those limits (see Performance). The promoted configuration below is the three-node deployment.
Named TP3/TP4 transport tuning profiles
keep B12X's measured RoCEnante limits and NCCL settings together.
bin/spark tuning show tp4 shows the B12X TP4 recipe; tuning create
generates a complete B12X configuration for a site node map, with launch
disabled. The promoted TileLang profiles are config/cluster.json
(TP3) and config/cluster-tp4.json (TP4).
This repository is being promoted from a forensic capture of the running cluster into its only operational source of truth. Until the transition checklist is complete, files are explicit about whether they describe observed state, desired state, or an experiment.
You need three DGX Sparks cabled as a triangle, or four cabled as a ring, on
DGX OS 26.09.2 or later. Each node needs about 480 GiB of free disk, Docker
with the NVIDIA runtime, and passwordless SSH and sudo from the head node.
Two Sparks are not supported. docs/setup.md is the full
guide: what each stage changes on your machines, how to check it, and how to
undo it.
| Sparks | Profile | Context | Kernel | Memory saver |
|---|---|---|---|---|
| 3 | config/cluster-4k.json | 262,144 | stock 4 KiB | not needed |
| 3 | config/cluster.json | 524,288 | 64 KiB | required |
| 4 | config/cluster-tp4.json | 1,048,576 | 64 KiB | required |
The 4 KiB profile needs no kernel change, so it is the shortest path to a running service.
| Stage | Do | Check |
|---|---|---|
| 1 | Host baseline: scripts/host-recovery apply, headless boot, nvidia-drm mode setting, reboot | scripts/host-recovery check |
| 2 | 64 KiB kernel and memory saver (64 KiB profiles only) | getconf PAGESIZE prints 65536 |
| 3 | Fabric: cable, then address with sparknet topology discover | 9000-byte pings on every cable path |
| 4 | Site configuration: node map, head address, interfaces | bin/spark doctor |
| 5 | Model: the pinned checkpoint on every node | |
| 6 | Image: bin/spark build prepare, bin/spark build image --apply, copy to every node | one image ID everywhere |
| 7 | First start: bin/spark cluster start --apply | bin/spark doctor --live |
| 8 | Verify: bin/spark bench | quality 5/5, no regressions |
Use --cluster-config <profile> before the subcommand for any profile other
than config/cluster.json. The repository never changes a host by itself:
bin/spark doctor only reports, and prints each fix as a command for you to
run (docs/doctor.md). cluster start refuses to launch
until the profile's kernel, fabric and image checks pass.
The active baseline is recorded in manifests/baselines/2026-10-05-karmic-kraken-r6.json (promotion record):
7.0.0-1019-nvidia-64k (kho=off), signed memory-saver DKMS
0.2.0 with UVM leaf-table packing enabled, no desktop, and
vm.watermark_boost_factor=0;integration/karmic-kraken-beta vLLM (plus Engram
projection sharding, asynchronous Engram rows, and two tool-call and
image-cache fixes) with the TileLang kernel family:
--no-enable-flashinfer-autotune): only the
sampler uses FlashInfer, and the pass saved no configs.One content-addressed image runs on all three nodes and passes the LRU coherence gate 5/5; see Performance.
The machine-readable desired configuration is config/cluster.json.
The named 4 KiB and 64 KiB profiles retain the
previous capacity as a fallback and select the larger 64 KiB profile by default;
config/cluster-tp4.json is the four-node TP4 1M
profile on the same image (--cluster-config config/cluster-tp4.json).
To reproduce the deployment on your own Sparks, follow
docs/setup.md.
The r6 acceptance benchmark (2026-10-05, bin/spark bench from dgx1:
quality, then prose and code prompts with reasoning on, temperature 0, 256
output tokens, three samples per decode point, and real source text for
prefill, two repeats) against r5p's, one boot each at the recipe's limits.
Each value is the mean ± its 95% confidence interval; a family is ahead only
where the two intervals do not overlap (bold).
Three nodes (TP3, eight sequences at 512K):
| Workload | B12X r5p | TileLang r6 |
|---|---|---|
| Quality gate (fixed LRU task, 5 repeats) | 5/5 | 5/5 |
| Prose, 1 stream (aggregate tok/s) | 50.4 ± 12.2% | 52.2 ± 0.7% |
| Prose, 2 streams | 75.9 ± 10.6% | 84.4 ± 0.6% |
| Prose, 4 streams | 118.8 ± 12.9% | 125.7 ± 0.5% |
| Prose, 8 streams | 173.3 ± 8.4% | 198.6 ± 0.4% |
| Code, 1 stream | 60.1 ± 2.0% | 63.0 ± 0.5% |
| Code, 2 streams | 90.8 ± 5.0% | 97.3 ± 8.5% |
| Code, 4 streams | 136.3 ± 6.3% | 159.2 ± 44.2% |
| Code, 8 streams | 192.3 ± 3.2% | 240.3 ± 1.0% |
| One-stream prose / code step time | 41.92 / 46.44 ms | 38.21 / 42.43 ms |
| Source-text prefill, 32K (tok/s) | 3,778 ± 14.9% | 4,306 ± 20.9% |
| Source-text prefill, 256K | 3,632 ± 2.2% | 4,034 ± 0.3% |
| Source-text prefill, 500K | 3,432 ± 0.6% | 3,743 ± 0.8% |
| Lowest MemAvailable, dgx1 | 5.93 GiB | 5.99 GiB |
Four nodes (TP4 1M recipe, 16 sequences at 1,048,576 tokens):
| Workload | B12X r5p | TileLang r6 |
|---|---|---|
| Prose, 1 / 2 / 4 streams | 62.1 ± 3.8% / 92.7 ± 8.0% / 141.6 ± 7.9% | 62.4 ± 1.2% / 100.2 ± 2.7% / 147.1 ± 10.2% |
| Prose, 8 / 16 streams | 215.7 ± 7.5% / 297.3 ± 1.2% | 222.1 ± 0.3% / 318.4 ± 1.6% |
| Code, 1 / 2 / 4 streams | 72.5 ± 7.5% / 111.3 ± 5.4% / 168.3 ± 9.6% | 76.0 ± 0.6% / 116.8 ± 18.9% / 174.9 ± 0.6% |
| Code, 8 / 16 streams | 245.7 ± 1.3% / 323.5 ± 1.5% | 264.3 ± 0.6% / 349.8 ± 2.5% |
| One-stream prose / code step time | 34.00 / 37.72 ms | 31.79 / 34.75 ms |
| Source-text prefill, 32K / 256K (tok/s) | 5,117 ± 1.7% / 4,873 ± 2.5% | 5,836 ± 4.2% / 5,471 ± 3.1% |
| Source-text prefill, 500K / 1M | 4,645 ± 1.7% / 4,026 ± 0.9% | 5,072 ± 1.3% / 4,370 ± 0.6% |
| KV capacity | 8,580,566 tokens in 10.5 GiB per rank | same |
| Lowest MemAvailable | 20.4 GiB (dgx1) | 19.35 GiB (dgx1) |
TileLang is never behind: it is ahead on single-stream step time, at eight and sixteen streams and on every prefill point except TP3's 32K, and level elsewhere. B12X's intervals are wider because its text changes from sample to sample (DSpark acceptance moves with it); TileLang generates the same text every sample, so its intervals are tight except where two- and four-stream scheduling interleaves differently between samples. The intervals cover sample-to-sample spread on one text; across 13 texts at TP3, DSpark acceptance per text varied about ±5% while the two families' means were level (TP3 benchmark). Step time, which does not depend on acceptance, is the kernel comparison. The B12X TP4 500K point is from its own run.
The native benchmark reports (TP3, TP4, and r5p's TP3 and TP4) record intervals, prompts, memory and thermal results. The 64 KiB deployment record covers the TP3 profile's long-context admission (four simultaneous 485K-token contexts) and near-limit retrieval (3/3 at 519,142 tokens), measured on r5o.
The tables below retain the historical 4 KiB r5l reference (r5o added the
top-k tie rule and TMA stage-release fences). These were measured with
bin/spark bench from dgx1: prose and code
prompts, temperature 0, 256 output tokens. With reasoning on (the server
default) every measured token is reasoning text:
| Prompt | Streams | Aggregate tok/s | Per-stream decode tok/s | First token |
|---|---|---|---|---|
| prose | 1 | 50.3 | 52.2 | 0.20 s |
| prose | 2 | 76.1 | 41.4 | 0.33 s |
| prose | 4 | 111.8 | 31.0 | 0.42 s |
| prose | 8 | 162.0 | 22.4 | 0.53 s |
| code | 1 | 59.7 | 62.8 | 0.23 s |
| code | 2 | 90.4 | 49.6 | 0.35 s |
| code | 4 | 131.4 | 36.6 | 0.45 s |
| code | 8 | 187.4 | 26.6 | 0.55 s |
With reasoning off (the answer itself), aggregate tok/s at 1/2/4/8 streams:
| Prompt | 1 | 2 | 4 | 8 |
|---|---|---|---|---|
| prose | 59.1 | 84.8 | 124.9 | 174.5 |
| code | 80.6 | 118.2 | 165.1 | 233.0 |
| JSON | 77.0 | 109.7 | 165.2 | 234.3 |
| Other measurements | |
|---|---|
| Quality gate (fixed LRU task, 5 repeats) | 5/5 |
| Long-context retrieval (phrase at 10%, 50%, 90% depth) | 3/3 at 152,914 tokens |
| Single-stream decode step | about 42 ms on prose and 47 ms on code; accepted drafts per step 1.2 (prose), 1.9 (code), 3.2 (code answers) |
| Cold prefill, repeated filler | 2K 4.4k, 32K 4.9k, 64K 4.9k, 131K 4.7k tok/s |
| Cold prefill, real text (Python source) | 4K 3.9k, 16K 3.8k, 32K 3.8k, 64K 3.8k, 131K 3.8k, 200K 3.8k tok/s |
| Prefix-cache replay, 32K prompt | 6.47 s cold, 0.24 s warm |
| Four concurrent 64K contexts | all admitted without preemption, peak KV use 20%, 13.1 tok/s per stream |
| Four concurrent 180K contexts (r5k) | all admitted without preemption, peak KV use 36%, 11.9 tok/s per stream |
| KV capacity | 1,348,708 tokens in 2.2 GiB per rank (5.1 full 256K contexts) |
| Host memory headroom | dgx1 at least 6.38 GiB MemAvailable under load (3 GiB guard); startup passes the 5 GiB guard |
The quick default takes three or four samples per decode point, about ±2-12% at 95% confidence; temperature-0 outputs differ between identical requests (the indexer's radix top-k keeps an arbitrary subset of positions tied at its threshold), which moves acceptance from sample to sample. Report: decode, prefill, prefix cache, and admission. See Benchmarking to reproduce them.
The project was spark3-vllm-ds41f until 2026-10-05. The former GitHub name
now holds a pointer, and deployment checks accept configurations that name
it. The command is bin/spark, and experiment scripts call it; dated
experiment records keep bin/spark3 as they ran it.
Names built into images or installed on the hosts keep spark3 until they
are rebuilt or reinstalled:
SPARK3_* environment variables that the vLLM patches read;local.spark3.* image labels and the /opt/spark3 paths inside the
image;spark3-kernel-memory service and RoCE marker.Lab windows also honour holds and requests written as ~/spark3-hold.json
and ~/spark3-request.json.
There are three deliberately separate kinds of state:
manifests/baselines/ contains immutable observations and benchmark identity.config/ contains the promoted desired state used to render launches.experiments/ contains isolated candidates and evidence before promotion.An experiment never becomes the baseline merely because it is running. Promotion requires reproducible measurements, an explicit decision, and a commit updating the desired configuration and baseline record together.
All commands are run from the repository root.
bin/spark doctor
bin/spark doctor --live
bin/spark status
bin/spark render dgx1
bin/spark cluster sync
bin/spark cluster start --replace
bin/spark cluster stop
bin/spark bench
bin/spark upstream list
bin/spark upstream prepare vllm
bin/spark upstream prepare b12x
bin/spark build prepare
bin/spark build check
bin/spark build image
bin/spark build image --apply
bin/spark build smoke
scripts/host-recovery check
scripts/host-recovery apply
doctor, status, render, and every cluster command without --apply are
read-only. bench only sends API requests; see Benchmarking.
doctor --live also compares the nodes' running kernel, base page size, driver
and memory policy, and verifies the staged 64 KiB kernel against
config/kernel-trial.json. It checks candidate
modules, headers, initramfs presence and the retained GRUB fallback; it never
selects a boot entry or applies corrections. Preparation is distinct from a
successful trial boot. The boot trial passed compatibility checks. Matched memory profiling is in progress before deciding the default; both kernels and their matching swap files remain installed.
doctor --live checks the configured checkpoint's file fragmentation on every
node. It reports every shard whose current extent count exceeds ext4's best
count, even if e4defrag assigns it a severity score of zero; ideal layouts
remain quiet. Large files can have a best count greater than one. Run
bin/spark doctor --fragmentation-commands to print exact correction commands
for affected blob files, grouped by node. This only prints commands: any actual
defragmentation belongs in a coordinated maintenance window. The check reads
the checkpoint index and extent metadata using sudo -n e4defrag -c, resolves
shard symlinks, and neither scans tensor payloads nor flushes caches. Missing
permissions, unsupported filesystems or incomplete reports are warnings, never
reported as clean layouts. Fragmentation warnings do not block startup and do
not establish a loading slowdown. Plain doctor remains local.
build prepare writes only under ignored .work/build/; see
docker/README.md. cluster sync fetches a published commit and detaches every clean node
checkout at that exact revision; it never copies a working tree or ignored files.
A commit counts as published when a branch on origin contains it. The
promoted configuration sets deployment.branch: main, so production deploys
only from main; experiment configurations omit the field, so they stay
deployable after their branch is merged and deleted.
The only cleanliness exception is a repository-local writable runtime mount
declared in cluster.json (currently cache/), which is preserved in place and
never enters Git.
cluster start preflights all three ranks, pre-arms a protected host-local
startup memory guard, waits for its first successful memory sample, and only
then starts workers before the head. Readiness fails if a guard exits or
available host memory crosses its threshold. Only a healthy API switches every
node to the lower steady-state guard. Mutating operations require an explicit
--apply; replacing an existing
service additionally requires --replace, and an experiment with
deployment.launch_enabled=false refuses mutation locally.
The promoted configuration is launch-enabled on main. Experiment
configurations set launch_enabled for themselves.
scripts/ holds the memory guard that cluster start installs on each node
and host recovery, which has not yet moved into bin/spark. Experiments keep
their own scripts in their directories.
Host management-plane recovery is versioned separately under host/recovery/.
It arms the existing hardware watchdog and protects SSH/Tailscale without
restarting Docker or the inference service. The incident evidence and exact
policy boundary are documented in docs/recovery.md.
bin/spark bench measures the live service from the head node and writes one
self-contained report to results/private/bench/<UTC time>/bench.json
(--output to change). By default it is a quick check of about six minutes:
the quality gate and every decode point with three or four samples each.
bin/spark bench --full runs every suite below and samples each decode
point to its precision target, about 35 minutes; use it for experiments that
need to resolve small differences. Before sending anything it runs the doctor --live
comparison and refuses a cluster that differs from its configuration. It also
records the configuration hash, each node's image ID and checkout, and the
client commit.
Suites of the full run, in order (--suites selects a subset):
quality: the fixed LRU request five times at temperature 0; all five must
pass, or the run stops before measuring anything.
compliance (explicit --suites compliance; not part of --full until
constrained decoding beside DSpark is qualified): structured-output
correctness on natural completion. Eight record prompts, each asking for a
complete array of twelve objects with 4,096 tokens of room, run prompt-only
and then constrained by the same array schema through response_format. A
request passes when it stopped on its own (finish_reason), parsed as
exactly twelve objects with no fence or commentary, and every object
conforms to its schema (python-jsonschema). Outputs are saved under
outputs/compliance-<arm>/. Every request receipt in every suite now
records finish_reason.
decode: prose and code prompts at concurrency 1, 2, 4, and 8, 256 output
tokens, reasoning on, temperature 0, and the same prompts and metric
(aggregate completion tokens per wall second) as every published baseline.
Output text still differs from run to run, so each point is a random
draw. Points are sampled in shuffled rounds after a discarded warmup round.
With --full, each point keeps sampling until its 95% confidence interval
is within --precision (2%) of the mean, between --min-samples (6) and
--max-samples (40) samples; the quick default takes 3-4.
Single boots of one configuration differ by about 3%, because adaptive
verification profiles its costs at startup, so effects smaller than that
need several boots per arm.
--decode-cases selects other cases. prose-nothink, code-nothink and
json-nothink turn reasoning off to measure the answer itself. The
portable group (count, explain, tasks, rows, math, chat,
essay, story, chat-sampled) follows the protocol public DGX Spark
benchmarks use, so figures line up with theirs:
min_tokens with ignore_eos);tasks;chat-sampled at temperature 0.7.Every decode point also reports decode_window_tps: the tokens after each
stream's first, over the span from the earliest first token to the latest
last token. It leaves out prefill and the start stagger of a concurrent
wave.
Three primary numbers accompany every point: step_ms (time per engine
step; exact for one stream, a decode-window estimate for several),
accepted_per_verified (accepted tokens per verified draft token) and
tokens_per_step (one plus accepted drafts per step). Throughput is
reported twice: measured (tps and decode_window_tps) and derived_tps,
what the primaries imply (tokens_per_step times streams over step_ms).
Temperature-0 output text does not repeat between samples, and draft
acceptance follows the text, so accepted_per_verified, tokens_per_step
and tok/s all swing between samples of the same prompt; step_ms stays
within about 1%. Gate kernel, transport and host changes on step_ms, and
use the acceptance numbers to explain why tok/s moved.
The precision group (prose-1k, code-1k, json-1k) resolves small
differences between arms: 1,024 output tokens, prompts whose natural answer
exceeds that budget so nothing is forced to continue past its end, at least
five and at most twelve samples per point, and convergence on step_ms
rather than tps. --converge-on overrides the convergence metric for
every point. A WARN line names outputs that stopped before the budget;
lengthen the prompt when it appears. json-1k gives each stream a distinct
record type whose prompt is generated from a JSON schema, and scores every
output against that schema: completed objects, whether the truncated prefix
parses once closed, and the share of objects that conform (json_schema_valid,
with the first violation quoted). Each output is also written under
outputs/<point>/ in the report directory, as the raw text and, when it
parses, the closed prefix as .json, so jq can be run on it afterwards.
Validation uses python-jsonschema (Draft 2020-12): sudo apt install python3-jsonschema on the dgx hosts, or the hashed
requirements/bench-client.txt elsewhere. json-schema-1k requests the
same through the server's structured output (response_format with a JSON
schema). It is opt-in and
outside the group, because constrained decoding changes the sampler path and
needs its own qualification beside speculative decoding before its numbers
mean anything.
sampled: DSpark accepted drafts per step at temperature 1.0 from the
engine counters, 32 requests per case at concurrency 1 and 4.
prefill: cold prefill at 2K, 32K, 64K, and 128K tokens, three unique
uncached prompts each. --prefill-text chooses the text:
filler (the default) repeats 18 words, so its Engram rows stay cached;novel uses random pseudo-words, whose rows are read from disk;source uses real text, the Python standard library's source and
docstrings.prefix: a 32K prompt followed by two identical replays, reporting cold and
warm TTFT and the cache hit rate.
admission: four concurrent 64K-token contexts; all four must run at once
without preemption.
Measurements stay clean and safe:
The report is still written if the run stops early.
With a reference run, each decode and prefill point shows its percent change
with a Welch 95% interval. The default reference is the profile's
benchmark_reference when it names one, as
config/cluster-tp4.json does for its four-node run,
and otherwise manifests/benchmarks/<promoted_baseline>.json. A run is only
comparable with a reference of the same node count, transport and kernel
backend. The bench refuses a default reference that differs in any of them;
--compare PATH names a reference explicitly and only warns about a
difference, and --compare none skips the comparison. The command exits
non-zero on a failed quality or admission check, any failed request, an early
stop, or a point that is significantly slower by more than --tolerance (3%).
A promotion adds a reference run to manifests/benchmarks/ for each topology
it promotes: <baseline>.json, plus a suffixed run such as
<baseline>-tp4.json that the other topology's profile names.
Canonical upstreams, contribution forks, tracking branches, pinned commits, and
applied upstream fixes live in upstreams.lock.json. Local
changes are kept as ordered patch series rather than edits to copied source
trees. upstream always means the canonical project; origin is Christopher's
fork when one exists; this deployment repository is neither. See
docs/upstreams.md. Repositories that are useful for ideas
but are not build inputs are kept separately in
docs/inspiration.md.
bin/spark build builds the promoted image from the pinned sources in
upstreams.lock.json and the patch series (docker/README.md),
and one content digest runs on all three nodes.
Canonical vLLM main is parked; the serving sources are Local Inference Lab's
integration/karmic-kraken-beta vLLM and B12X, TileLang, DeepSeek's
TileKernels and sparknet, with the local patch series
(docs/upstreams.md).
Reproducible Docker/vLLM deployment, tuning, and benchmarks for DeepSeek V4.1 Flash on a switchless three-node DGX Spark fabric.
Python
21
690 commits
updated Oct 18, 2026
Reproducible Docker/vLLM deployment, tuning, and benchmarks for DeepSeek V4.1 Flash on a switchless three-node DGX Spark fabric, and the same deployment on a four-node ring.
The promoted kernel family is TileLang (r6): TileLang kernels for SM121,
with DeepSeek's TileKernels and the one-shot RoCE collectives of
sparknet, in place
of B12X's attention, linear, MoE, mHC, vocabulary and collective kernels
(kernel_backend: tilelang, see scripts/kernel_backend.py). B12X stays in
the image as the alternative backend; a configuration without
kernel_backend means B12X. The r6 promotion record
has both recipes and their benchmarks, and the
decode-kernel experiment
compares the two families kernel by kernel.
docs/fundamentals.md explains the model itself: GEMM
shapes, number formats, one decoder layer, attention, mHC, Engram, DSpark, and
how TP3 and TP4 split the layers.
The deployment tools also generate and validate a four-node switchless ring profile. The TP4 1M profile serves the checkpoint's full 1,048,576-token context with 16 sequences on the same image as the three-node deployment, and was benchmarked at those limits (see Performance). The promoted configuration below is the three-node deployment.
Named TP3/TP4 transport tuning profiles
keep B12X's measured RoCEnante limits and NCCL settings together.
bin/spark tuning show tp4 shows the B12X TP4 recipe; tuning create
generates a complete B12X configuration for a site node map, with launch
disabled. The promoted TileLang profiles are config/cluster.json
(TP3) and config/cluster-tp4.json (TP4).
This repository is being promoted from a forensic capture of the running cluster into its only operational source of truth. Until the transition checklist is complete, files are explicit about whether they describe observed state, desired state, or an experiment.
You need three DGX Sparks cabled as a triangle, or four cabled as a ring, on
DGX OS 26.09.2 or later. Each node needs about 480 GiB of free disk, Docker
with the NVIDIA runtime, and passwordless SSH and sudo from the head node.
Two Sparks are not supported. docs/setup.md is the full
guide: what each stage changes on your machines, how to check it, and how to
undo it.
| Sparks | Profile | Context | Kernel | Memory saver |
|---|---|---|---|---|
| 3 | config/cluster-4k.json | 262,144 | stock 4 KiB | not needed |
| 3 | config/cluster.json | 524,288 | 64 KiB | required |
| 4 | config/cluster-tp4.json | 1,048,576 | 64 KiB | required |
The 4 KiB profile needs no kernel change, so it is the shortest path to a running service.
| Stage | Do | Check |
|---|---|---|
| 1 | Host baseline: scripts/host-recovery apply, headless boot, nvidia-drm mode setting, reboot | scripts/host-recovery check |
| 2 | 64 KiB kernel and memory saver (64 KiB profiles only) | getconf PAGESIZE prints 65536 |
| 3 | Fabric: cable, then address with sparknet topology discover | 9000-byte pings on every cable path |
| 4 | Site configuration: node map, head address, interfaces | bin/spark doctor |
| 5 | Model: the pinned checkpoint on every node | |
| 6 | Image: bin/spark build prepare, bin/spark build image --apply, copy to every node | one image ID everywhere |
| 7 | First start: bin/spark cluster start --apply | bin/spark doctor --live |
| 8 | Verify: bin/spark bench | quality 5/5, no regressions |
Use --cluster-config <profile> before the subcommand for any profile other
than config/cluster.json. The repository never changes a host by itself:
bin/spark doctor only reports, and prints each fix as a command for you to
run (docs/doctor.md). cluster start refuses to launch
until the profile's kernel, fabric and image checks pass.
The active baseline is recorded in manifests/baselines/2026-10-05-karmic-kraken-r6.json (promotion record):
7.0.0-1019-nvidia-64k (kho=off), signed memory-saver DKMS
0.2.0 with UVM leaf-table packing enabled, no desktop, and
vm.watermark_boost_factor=0;integration/karmic-kraken-beta vLLM (plus Engram
projection sharding, asynchronous Engram rows, and two tool-call and
image-cache fixes) with the TileLang kernel family:
--no-enable-flashinfer-autotune): only the
sampler uses FlashInfer, and the pass saved no configs.One content-addressed image runs on all three nodes and passes the LRU coherence gate 5/5; see Performance.
The machine-readable desired configuration is config/cluster.json.
The named 4 KiB and 64 KiB profiles retain the
previous capacity as a fallback and select the larger 64 KiB profile by default;
config/cluster-tp4.json is the four-node TP4 1M
profile on the same image (--cluster-config config/cluster-tp4.json).
To reproduce the deployment on your own Sparks, follow
docs/setup.md.
The r6 acceptance benchmark (2026-10-05, bin/spark bench from dgx1:
quality, then prose and code prompts with reasoning on, temperature 0, 256
output tokens, three samples per decode point, and real source text for
prefill, two repeats) against r5p's, one boot each at the recipe's limits.
Each value is the mean ± its 95% confidence interval; a family is ahead only
where the two intervals do not overlap (bold).
Three nodes (TP3, eight sequences at 512K):
| Workload | B12X r5p | TileLang r6 |
|---|---|---|
| Quality gate (fixed LRU task, 5 repeats) | 5/5 | 5/5 |
| Prose, 1 stream (aggregate tok/s) | 50.4 ± 12.2% | 52.2 ± 0.7% |
| Prose, 2 streams | 75.9 ± 10.6% | 84.4 ± 0.6% |
| Prose, 4 streams | 118.8 ± 12.9% | 125.7 ± 0.5% |
| Prose, 8 streams | 173.3 ± 8.4% | 198.6 ± 0.4% |
| Code, 1 stream | 60.1 ± 2.0% | 63.0 ± 0.5% |
| Code, 2 streams | 90.8 ± 5.0% | 97.3 ± 8.5% |
| Code, 4 streams | 136.3 ± 6.3% | 159.2 ± 44.2% |
| Code, 8 streams | 192.3 ± 3.2% | 240.3 ± 1.0% |
| One-stream prose / code step time | 41.92 / 46.44 ms | 38.21 / 42.43 ms |
| Source-text prefill, 32K (tok/s) | 3,778 ± 14.9% | 4,306 ± 20.9% |
| Source-text prefill, 256K | 3,632 ± 2.2% | 4,034 ± 0.3% |
| Source-text prefill, 500K | 3,432 ± 0.6% | 3,743 ± 0.8% |
| Lowest MemAvailable, dgx1 | 5.93 GiB | 5.99 GiB |
Four nodes (TP4 1M recipe, 16 sequences at 1,048,576 tokens):
| Workload | B12X r5p | TileLang r6 |
|---|---|---|
| Prose, 1 / 2 / 4 streams | 62.1 ± 3.8% / 92.7 ± 8.0% / 141.6 ± 7.9% | 62.4 ± 1.2% / 100.2 ± 2.7% / 147.1 ± 10.2% |
| Prose, 8 / 16 streams | 215.7 ± 7.5% / 297.3 ± 1.2% | 222.1 ± 0.3% / 318.4 ± 1.6% |
| Code, 1 / 2 / 4 streams | 72.5 ± 7.5% / 111.3 ± 5.4% / 168.3 ± 9.6% | 76.0 ± 0.6% / 116.8 ± 18.9% / 174.9 ± 0.6% |
| Code, 8 / 16 streams | 245.7 ± 1.3% / 323.5 ± 1.5% | 264.3 ± 0.6% / 349.8 ± 2.5% |
| One-stream prose / code step time | 34.00 / 37.72 ms | 31.79 / 34.75 ms |
| Source-text prefill, 32K / 256K (tok/s) | 5,117 ± 1.7% / 4,873 ± 2.5% | 5,836 ± 4.2% / 5,471 ± 3.1% |
| Source-text prefill, 500K / 1M | 4,645 ± 1.7% / 4,026 ± 0.9% | 5,072 ± 1.3% / 4,370 ± 0.6% |
| KV capacity | 8,580,566 tokens in 10.5 GiB per rank | same |
| Lowest MemAvailable | 20.4 GiB (dgx1) | 19.35 GiB (dgx1) |
TileLang is never behind: it is ahead on single-stream step time, at eight and sixteen streams and on every prefill point except TP3's 32K, and level elsewhere. B12X's intervals are wider because its text changes from sample to sample (DSpark acceptance moves with it); TileLang generates the same text every sample, so its intervals are tight except where two- and four-stream scheduling interleaves differently between samples. The intervals cover sample-to-sample spread on one text; across 13 texts at TP3, DSpark acceptance per text varied about ±5% while the two families' means were level (TP3 benchmark). Step time, which does not depend on acceptance, is the kernel comparison. The B12X TP4 500K point is from its own run.
The native benchmark reports (TP3, TP4, and r5p's TP3 and TP4) record intervals, prompts, memory and thermal results. The 64 KiB deployment record covers the TP3 profile's long-context admission (four simultaneous 485K-token contexts) and near-limit retrieval (3/3 at 519,142 tokens), measured on r5o.
The tables below retain the historical 4 KiB r5l reference (r5o added the
top-k tie rule and TMA stage-release fences). These were measured with
bin/spark bench from dgx1: prose and code
prompts, temperature 0, 256 output tokens. With reasoning on (the server
default) every measured token is reasoning text:
| Prompt | Streams | Aggregate tok/s | Per-stream decode tok/s | First token |
|---|---|---|---|---|
| prose | 1 | 50.3 | 52.2 | 0.20 s |
| prose | 2 | 76.1 | 41.4 | 0.33 s |
| prose | 4 | 111.8 | 31.0 | 0.42 s |
| prose | 8 | 162.0 | 22.4 | 0.53 s |
| code | 1 | 59.7 | 62.8 | 0.23 s |
| code | 2 | 90.4 | 49.6 | 0.35 s |
| code | 4 | 131.4 | 36.6 | 0.45 s |
| code | 8 | 187.4 | 26.6 | 0.55 s |
With reasoning off (the answer itself), aggregate tok/s at 1/2/4/8 streams:
| Prompt | 1 | 2 | 4 | 8 |
|---|---|---|---|---|
| prose | 59.1 | 84.8 | 124.9 | 174.5 |
| code | 80.6 | 118.2 | 165.1 | 233.0 |
| JSON | 77.0 | 109.7 | 165.2 | 234.3 |
| Other measurements | |
|---|---|
| Quality gate (fixed LRU task, 5 repeats) | 5/5 |
| Long-context retrieval (phrase at 10%, 50%, 90% depth) | 3/3 at 152,914 tokens |
| Single-stream decode step | about 42 ms on prose and 47 ms on code; accepted drafts per step 1.2 (prose), 1.9 (code), 3.2 (code answers) |
| Cold prefill, repeated filler | 2K 4.4k, 32K 4.9k, 64K 4.9k, 131K 4.7k tok/s |
| Cold prefill, real text (Python source) | 4K 3.9k, 16K 3.8k, 32K 3.8k, 64K 3.8k, 131K 3.8k, 200K 3.8k tok/s |
| Prefix-cache replay, 32K prompt | 6.47 s cold, 0.24 s warm |
| Four concurrent 64K contexts | all admitted without preemption, peak KV use 20%, 13.1 tok/s per stream |
| Four concurrent 180K contexts (r5k) | all admitted without preemption, peak KV use 36%, 11.9 tok/s per stream |
| KV capacity | 1,348,708 tokens in 2.2 GiB per rank (5.1 full 256K contexts) |
| Host memory headroom | dgx1 at least 6.38 GiB MemAvailable under load (3 GiB guard); startup passes the 5 GiB guard |
The quick default takes three or four samples per decode point, about ±2-12% at 95% confidence; temperature-0 outputs differ between identical requests (the indexer's radix top-k keeps an arbitrary subset of positions tied at its threshold), which moves acceptance from sample to sample. Report: decode, prefill, prefix cache, and admission. See Benchmarking to reproduce them.
The project was spark3-vllm-ds41f until 2026-10-05. The former GitHub name
now holds a pointer, and deployment checks accept configurations that name
it. The command is bin/spark, and experiment scripts call it; dated
experiment records keep bin/spark3 as they ran it.
Names built into images or installed on the hosts keep spark3 until they
are rebuilt or reinstalled:
SPARK3_* environment variables that the vLLM patches read;local.spark3.* image labels and the /opt/spark3 paths inside the
image;spark3-kernel-memory service and RoCE marker.Lab windows also honour holds and requests written as ~/spark3-hold.json
and ~/spark3-request.json.
There are three deliberately separate kinds of state:
manifests/baselines/ contains immutable observations and benchmark identity.config/ contains the promoted desired state used to render launches.experiments/ contains isolated candidates and evidence before promotion.An experiment never becomes the baseline merely because it is running. Promotion requires reproducible measurements, an explicit decision, and a commit updating the desired configuration and baseline record together.
All commands are run from the repository root.
bin/spark doctor
bin/spark doctor --live
bin/spark status
bin/spark render dgx1
bin/spark cluster sync
bin/spark cluster start --replace
bin/spark cluster stop
bin/spark bench
bin/spark upstream list
bin/spark upstream prepare vllm
bin/spark upstream prepare b12x
bin/spark build prepare
bin/spark build check
bin/spark build image
bin/spark build image --apply
bin/spark build smoke
scripts/host-recovery check
scripts/host-recovery apply
doctor, status, render, and every cluster command without --apply are
read-only. bench only sends API requests; see Benchmarking.
doctor --live also compares the nodes' running kernel, base page size, driver
and memory policy, and verifies the staged 64 KiB kernel against
config/kernel-trial.json. It checks candidate
modules, headers, initramfs presence and the retained GRUB fallback; it never
selects a boot entry or applies corrections. Preparation is distinct from a
successful trial boot. The boot trial passed compatibility checks. Matched memory profiling is in progress before deciding the default; both kernels and their matching swap files remain installed.
doctor --live checks the configured checkpoint's file fragmentation on every
node. It reports every shard whose current extent count exceeds ext4's best
count, even if e4defrag assigns it a severity score of zero; ideal layouts
remain quiet. Large files can have a best count greater than one. Run
bin/spark doctor --fragmentation-commands to print exact correction commands
for affected blob files, grouped by node. This only prints commands: any actual
defragmentation belongs in a coordinated maintenance window. The check reads
the checkpoint index and extent metadata using sudo -n e4defrag -c, resolves
shard symlinks, and neither scans tensor payloads nor flushes caches. Missing
permissions, unsupported filesystems or incomplete reports are warnings, never
reported as clean layouts. Fragmentation warnings do not block startup and do
not establish a loading slowdown. Plain doctor remains local.
build prepare writes only under ignored .work/build/; see
docker/README.md. cluster sync fetches a published commit and detaches every clean node
checkout at that exact revision; it never copies a working tree or ignored files.
A commit counts as published when a branch on origin contains it. The
promoted configuration sets deployment.branch: main, so production deploys
only from main; experiment configurations omit the field, so they stay
deployable after their branch is merged and deleted.
The only cleanliness exception is a repository-local writable runtime mount
declared in cluster.json (currently cache/), which is preserved in place and
never enters Git.
cluster start preflights all three ranks, pre-arms a protected host-local
startup memory guard, waits for its first successful memory sample, and only
then starts workers before the head. Readiness fails if a guard exits or
available host memory crosses its threshold. Only a healthy API switches every
node to the lower steady-state guard. Mutating operations require an explicit
--apply; replacing an existing
service additionally requires --replace, and an experiment with
deployment.launch_enabled=false refuses mutation locally.
The promoted configuration is launch-enabled on main. Experiment
configurations set launch_enabled for themselves.
scripts/ holds the memory guard that cluster start installs on each node
and host recovery, which has not yet moved into bin/spark. Experiments keep
their own scripts in their directories.
Host management-plane recovery is versioned separately under host/recovery/.
It arms the existing hardware watchdog and protects SSH/Tailscale without
restarting Docker or the inference service. The incident evidence and exact
policy boundary are documented in docs/recovery.md.
bin/spark bench measures the live service from the head node and writes one
self-contained report to results/private/bench/<UTC time>/bench.json
(--output to change). By default it is a quick check of about six minutes:
the quality gate and every decode point with three or four samples each.
bin/spark bench --full runs every suite below and samples each decode
point to its precision target, about 35 minutes; use it for experiments that
need to resolve small differences. Before sending anything it runs the doctor --live
comparison and refuses a cluster that differs from its configuration. It also
records the configuration hash, each node's image ID and checkout, and the
client commit.
Suites of the full run, in order (--suites selects a subset):
quality: the fixed LRU request five times at temperature 0; all five must
pass, or the run stops before measuring anything.
compliance (explicit --suites compliance; not part of --full until
constrained decoding beside DSpark is qualified): structured-output
correctness on natural completion. Eight record prompts, each asking for a
complete array of twelve objects with 4,096 tokens of room, run prompt-only
and then constrained by the same array schema through response_format. A
request passes when it stopped on its own (finish_reason), parsed as
exactly twelve objects with no fence or commentary, and every object
conforms to its schema (python-jsonschema). Outputs are saved under
outputs/compliance-<arm>/. Every request receipt in every suite now
records finish_reason.
decode: prose and code prompts at concurrency 1, 2, 4, and 8, 256 output
tokens, reasoning on, temperature 0, and the same prompts and metric
(aggregate completion tokens per wall second) as every published baseline.
Output text still differs from run to run, so each point is a random
draw. Points are sampled in shuffled rounds after a discarded warmup round.
With --full, each point keeps sampling until its 95% confidence interval
is within --precision (2%) of the mean, between --min-samples (6) and
--max-samples (40) samples; the quick default takes 3-4.
Single boots of one configuration differ by about 3%, because adaptive
verification profiles its costs at startup, so effects smaller than that
need several boots per arm.
--decode-cases selects other cases. prose-nothink, code-nothink and
json-nothink turn reasoning off to measure the answer itself. The
portable group (count, explain, tasks, rows, math, chat,
essay, story, chat-sampled) follows the protocol public DGX Spark
benchmarks use, so figures line up with theirs:
min_tokens with ignore_eos);tasks;chat-sampled at temperature 0.7.Every decode point also reports decode_window_tps: the tokens after each
stream's first, over the span from the earliest first token to the latest
last token. It leaves out prefill and the start stagger of a concurrent
wave.
Three primary numbers accompany every point: step_ms (time per engine
step; exact for one stream, a decode-window estimate for several),
accepted_per_verified (accepted tokens per verified draft token) and
tokens_per_step (one plus accepted drafts per step). Throughput is
reported twice: measured (tps and decode_window_tps) and derived_tps,
what the primaries imply (tokens_per_step times streams over step_ms).
Temperature-0 output text does not repeat between samples, and draft
acceptance follows the text, so accepted_per_verified, tokens_per_step
and tok/s all swing between samples of the same prompt; step_ms stays
within about 1%. Gate kernel, transport and host changes on step_ms, and
use the acceptance numbers to explain why tok/s moved.
The precision group (prose-1k, code-1k, json-1k) resolves small
differences between arms: 1,024 output tokens, prompts whose natural answer
exceeds that budget so nothing is forced to continue past its end, at least
five and at most twelve samples per point, and convergence on step_ms
rather than tps. --converge-on overrides the convergence metric for
every point. A WARN line names outputs that stopped before the budget;
lengthen the prompt when it appears. json-1k gives each stream a distinct
record type whose prompt is generated from a JSON schema, and scores every
output against that schema: completed objects, whether the truncated prefix
parses once closed, and the share of objects that conform (json_schema_valid,
with the first violation quoted). Each output is also written under
outputs/<point>/ in the report directory, as the raw text and, when it
parses, the closed prefix as .json, so jq can be run on it afterwards.
Validation uses python-jsonschema (Draft 2020-12): sudo apt install python3-jsonschema on the dgx hosts, or the hashed
requirements/bench-client.txt elsewhere. json-schema-1k requests the
same through the server's structured output (response_format with a JSON
schema). It is opt-in and
outside the group, because constrained decoding changes the sampler path and
needs its own qualification beside speculative decoding before its numbers
mean anything.
sampled: DSpark accepted drafts per step at temperature 1.0 from the
engine counters, 32 requests per case at concurrency 1 and 4.
prefill: cold prefill at 2K, 32K, 64K, and 128K tokens, three unique
uncached prompts each. --prefill-text chooses the text:
filler (the default) repeats 18 words, so its Engram rows stay cached;novel uses random pseudo-words, whose rows are read from disk;source uses real text, the Python standard library's source and
docstrings.prefix: a 32K prompt followed by two identical replays, reporting cold and
warm TTFT and the cache hit rate.
admission: four concurrent 64K-token contexts; all four must run at once
without preemption.
Measurements stay clean and safe:
The report is still written if the run stops early.
With a reference run, each decode and prefill point shows its percent change
with a Welch 95% interval. The default reference is the profile's
benchmark_reference when it names one, as
config/cluster-tp4.json does for its four-node run,
and otherwise manifests/benchmarks/<promoted_baseline>.json. A run is only
comparable with a reference of the same node count, transport and kernel
backend. The bench refuses a default reference that differs in any of them;
--compare PATH names a reference explicitly and only warns about a
difference, and --compare none skips the comparison. The command exits
non-zero on a failed quality or admission check, any failed request, an early
stop, or a point that is significantly slower by more than --tolerance (3%).
A promotion adds a reference run to manifests/benchmarks/ for each topology
it promotes: <baseline>.json, plus a suffixed run such as
<baseline>-tp4.json that the other topology's profile names.
Canonical upstreams, contribution forks, tracking branches, pinned commits, and
applied upstream fixes live in upstreams.lock.json. Local
changes are kept as ordered patch series rather than edits to copied source
trees. upstream always means the canonical project; origin is Christopher's
fork when one exists; this deployment repository is neither. See
docs/upstreams.md. Repositories that are useful for ideas
but are not build inputs are kept separately in
docs/inspiration.md.
bin/spark build builds the promoted image from the pinned sources in
upstreams.lock.json and the patch series (docker/README.md),
and one content digest runs on all three nodes.
Canonical vLLM main is parked; the serving sources are Local Inference Lab's
integration/karmic-kraken-beta vLLM and B12X, TileLang, DeepSeek's
TileKernels and sparknet, with the local patch series
(docs/upstreams.md).