Recipes
Launch configurations that were run and measured on named hardware, and how local overrides layer on top.
Goal
Running a model should start from a curated recipe we have actually run, not from a HuggingFace search plus autodetection. Search stays, clearly labelled as the escape hatch for models we have no recipe for.
Source authority
Two upstreams, each authoritative for a different thing. Mixing this up is how we
shipped a flag the image rejects (--attention-backend B12X_MLA_SPARSE).
| source | authoritative for | licence |
|---|---|---|
vllm-project/recipes |
model facts: repo ids, variants, min_vllm_version, the hardware: verified map |
Apache-2.0 |
eugr/spark-vllm-docker |
image facts: backend names, loader, mods, build_args, anything describing its own image |
MIT |
eugr is the higher-signal source for DGX Spark / GB10 because its recipes are written for that hardware and shipped with the image they target. vllm-project is broader but hardware-blind for SM121.
eugr is MIT (Copyright 2026 Eugene Rakhmatulin); derived data must carry attribution.
Three roles, kept separate
Conflating these is what made ingestion look hard.
- Launch path — our tested recipes only. Digest-pinned, measured on our own
hardware,
internal/recipe/tested.json. This is the product. - Search path — HF search, annotated. "eugr has a recipe for this", "vllm-project claims gb200-verified". Signal without a claim that it runs here.
- Dev path — a diff tool, not an importer. Parses both upstreams and reports where our tested set disagrees. Dev-only: it never ships in the binary and cannot break a launch.
The critical consequence: we never execute eugr's command: template, only compare
against it. Comparison tolerates a partial parse; execution does not. That removes
every integration risk below.
Why not ingest eugr directly
command:is a templated shell string, not an arg list: line continuations,{placeholder}substitution,{{...}}escaping literal JSON.container,build_argsandmodsreference eugr's build pipeline. We consume the published image by digest, so for us those fields are advisory.- The repo moves fast. Anything mechanically ingested becomes a moving dependency.
Why not drop the upstreams entirely
Without a systematic diff there is no way to learn we have fallen behind. That is not hypothetical: we ran DeepSeek for a week with no speculative decoding because the recipe was transcribed from a log instead of from eugr's YAML.
Upstream schemas
eugr, recipes/*.yaml, recipe_version: "1" (uniform across all recipes, documented in
their docs/AGENT_DEVELOPMENT.md, and used by run-recipe.py for feature checks):
model (HF repo, the join key), container, build_args[], cluster_only, mods[],
defaults{...}, env{}, command: |.
vllm-project, models/<org>/<Model>.yaml: model.model_id (same join key),
docker_image, min_vllm_version, base_args, meta.hardware, variants,
compatible_strategies. Only entries listed under hardware: are verified; everything
else is a fail-open default, and we surface that distinction rather than inventing tiers.
Provenance
Every tested recipe is authored in this repo but derived from an upstream, so each entry
carries a structured source:
"source": {
"repo": "eugr/spark-vllm-docker",
"path": "recipes/deepseek-v4-flash-0731.yaml",
"commit": "1761c49ad71ca9c8ca50772941ee4d06dcc40dc1",
"license": "MIT"
}
Structured rather than a comment for three reasons: JSON has no comments; recipediff
needs it to know which upstream file and commit to compare an entry against; and MIT and
Apache-2.0 both want attribution. recipes show renders it as a "derived from" line, and
TestTestedRecipesCiteAPinnedSource rejects an entry without a repo, a full 40-character
commit and a licence.
The pinned commit is also the staleness signal: when it trails upstream HEAD,
recipediff has something to report.
Declared deviations
Where an entry departs from its source, source.deviations records it as data:
{
"kind": "flag",
"setting": "max_num_seqs",
"upstream": "8",
"ours": "32",
"why": "measured ..."
}
kind is flag, env or mod, and it says where to look for the claim: the args, the
entry's env, or upstream's mods list. It exists because the two guards used to
contradict each other. The entry-side test built "--" + setting for every declaration,
so an env key became the flag --VLLM-MOE-SKIP-PADDING and could never be found, while
recipediff keyed env findings on the raw key -- which made two of the open differences
impossible to declare at all. With the kind recorded, each guard checks the place the
setting actually lives.
A declaration has to be falsifiable, so recipediff reads the values and not just the
key: the direction must agree (a drop cannot be declared as a change), ours must match
the entry's own value exactly, and upstream must match exactly unless upstream's value
is longer than 32 characters -- its --speculative-config and --reasoning-config JSON
blobs cannot usefully be restated, so prose is allowed there. A declaration that no longer
fits reads as MISDECLARED and still counts against the exit code; keying on the name
alone let a stale claim go on hiding a difference that had moved underneath it. An entry
whose source has no parser is reported as NOT CHECKED and counted too, because an entry
nobody can compare is an entry whose differences nobody has seen.
An empty ours means we dropped the setting. setting must name the thing the way
upstream does — the flag without its dashes (max_model_len), the env key, or the mod
path — because that is the key recipediff matches on, and
TestDeviationsMatchTheArgsTheyDescribe checks the claim against the entry itself. A
dropped setting leaves no trace to check, so the test also holds the name to the shape its
kind implies: a mod path has a slash, an env key is upper case, a flag is neither. Prose
in notes cannot do any of this.
Running the tool against both entries found differences that were never decisions, only
transcription. Four of them turned out to be no decision at all once the pinned image was
read: --max-cudagraph-capture-size 64 is exactly what vLLM computes when the flag is
absent at our max_num_seqs, --reasoning-config is built from --reasoning-parser
with the same empty delimiters upstream passes, and both
--default-chat-template-kwargs restate what the deepseek_v4 renderer already does.
Those are declared with the code that proves it.
Two per entry remain open and are deliberately left undeclared so the report keeps raising
them, because both change behaviour rather than restating it:
VLLM_USE_MEGA_AOT_ARTIFACT 1 to -1 (anything but "1" disables the mega AOT artifact)
and VLLM_MOE_SKIP_PADDING dropped (the image defaults it to on, upstream turns it off
for this model). Each needs a measurement or a reason, not a sentence invented after the
fact.
Local overrides
A tested recipe is the base layer; the profile of the same name is the override
layer. recipes launch merges them last-wins per flag, which is the precedence vLLM
itself documents for a command line over a config file.
Editing is allowed and honoured. The earlier design refused edits to a recipe profile, but
the profile is not inert: the Coder sync reads MAX_MODEL_LEN and DISPLAY_NAME from it, while
the launcher built argv from the recipe alone. An edit therefore did half of something — it
moved what Coder advertised without moving what the server served. Merging removes that
asymmetry by construction.
What changes when overrides exist is the claim, not the permission:
recipes list,recipes show,listand the launch output report the entry as the recipe plus local overrides, andrecipes showstops presenting the verified numbers as if they described the running server.- A future
recipe verifymust refuse to re-stamp measurements taken from an overridden run, because "we ran this" would then be false.
What is still refused is a wrong command, not an edit: native start on a container
recipe, which would run vLLM on the local machine while the cluster serves. switch to a
recipe is allowed, but only once that recipe has been launched at least once, because the
launch is what records its node topology. That precondition is checked ahead of
WriteActiveEnv: activation notifies Coder before restarting, so a late failure would
leave Coder advertising a model that is not serving.
Overriding any flag is allowed, including --attention-backend and --load-format. The
forbiddenArgs check still fails closed on combinations known to abort on this arch. A
profile that merely restates a recipe value is not an override, so a freshly generated
profile reports none. Changing MODEL is refused: that is a different model, which the
recipe's flags and measurements do not describe.
Worked example: speculative decoding is a single-user override
dspark speculative decoding is the clearest case for the override layer rather than the
recipe. Measured on the two-Spark pair at 262K context, single stream:
| workload | recipe | plus dspark |
|---|---|---|
| repetitive/structured | 28.8 tok/s | 80 tok/s |
| code | 28.8 tok/s | 44-56 tok/s |
| prose | 28.8 tok/s | 40 tok/s |
It is not in the recipe because the cost lands on everyone else: KV cache falls from 681,894 to 420,192 tokens, concurrency from 2.60x to 1.60x, and load time from 150s to 195s. Speculation also spends spare compute between tokens, and on a busy server that compute is already carrying other requests, so the single-stream numbers above are an upper bound rather than what a shared box delivers. Time-to-first-token gets slightly worse, which every waiting request pays for.
Enable it per-deployment when the box has one user:
vllm-helper set deepseek-v4-flash-2spark EXTRA_ARGS='--speculative-config {"method":"dspark","num_speculative_tokens":5,"draft_sample_method":"probabilistic","attention_backend":"B12X","draft_load_config":{"load_format":"safetensors","safetensors_load_strategy":"lazy"}} --max-cudagraph-capture-size 48'
vllm-helper recipes launch deepseek-v4-flash-2spark
num_speculative_tokens must be a multiple of the checkpoint's dspark_block_size (5 for
DeepSeek V4 Flash 0731); 3 truncates the block and 6 is rejected at boot. The capture size
is max_num_seqs x (k + 1). Confirm acceptance from the server's own SpecDecoding metrics log lines rather than from client-side tokens per second, because under
speculation one streamed chunk carries several tokens.
Shipped recipes
Both run on the same pinned image, sharded TP=2 across two DGX Sparks, at 262144 context.
The numbers are each entry's verified block in internal/recipe/tested.json.
| id | source | KV tokens | concurrency | load |
|---|---|---|---|---|
deepseek-v4-flash-2spark |
eugr/spark-vllm-docker@1761c49a |
664,817 | 2.53x | 161 s |
deepseek-v4-flash-vision-2spark |
eugr/spark-vllm-docker@798528a2 |
670,498 | 2.56x | 165 s |
Every difference from upstream is declared in source.deviations except two per entry,
which stay open on purpose (see Declared deviations):
VLLM_USE_MEGA_AOT_ARTIFACT (1 upstream, -1 here) and VLLM_MOE_SKIP_PADDING (0
upstream, unset here). recipediff reports exactly those four findings until each is
measured or explained.
Deliberate divergences to keep: port 8001 (matches PORT, fronted by the router on 8011) and an explicit --max-model-len rather than auto, so the served window and
verified.context stay pinned together.
Invariants
- Images pinned by
@sha256:, never a moving tag. eugr's:latestadvanced under us mid-session, and a moved image is how a valid backend name becomes invalid. - Tested means tested: strict list, no aspirational entries.
- Separate ids per topology rather than one entry with a topology switch.
--max-model-lenmust equalverified.context, enforced byTestVerifiedContextMatchesMaxModelLen.- Extend upstream, do not fork it.