plans: config API design for the training and inference suites - #349
Draft
atnair-amd wants to merge 3 commits into
Draft
plans: config API design for the training and inference suites#349atnair-amd wants to merge 3 commits into
atnair-amd wants to merge 3 commits into
Conversation
Design for a shared block library, suite registry, and load-time rule set covering the six training and inference suites (vllm, atom, sglang, megatron, torchtitan, jaxmaxtext). Documents the current divergence measured against the tree: four cell-key signatures for one operation, env split across four locations, and three suites whose config files must change because they carry no paths block.
The doc preserved the existing sweep shapes behind a SweepEntry contract. Add the alternative -- one flat runs block with declared names and arbitrary overrides -- with its costs, the zero-churn migration lever, and the open decisions. Correct three stale claims while there: the inventory is five shapes not three (megatron/torchtitan do have sweeps in their current configs, sglang has a select-one block, and 13 configs have no sweep construct at all), the MatrixSweep key is derived rather than the declared name, and the per-shape counts are 42/12/3/5/13.
Companion to the config API refactor doc, covering how a config file comes to exist at all. cvs copy-config is generation today and it is shutil.copyfile -- 75 of 145 shipped configs carry 354 <changeme> placeholders that survive the copy. The cluster file half is already generated; the config half is not. Grounds the argument in the vllm llama31-70b pair, whose single and distributed configs differ in nine fields, none of them knowledge: five are derivable, one is a static port, one is cosmetic, and the two sweep differences are a name prefix that cell_key never reads. 23 files across 11 topology sets repeat that pattern in every suite. Adds four candidate sources of truth for generated content, nine design ideas, the pipeline showing where generation meets the schema, and the sequencing risk against the sweep departure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two design docs, under
plans/. Nothing incvs/changes.cvs-config-api-refactor.md— the config schema and theautoresolution phase.cvs-config-generation.md— how a config file comes to exist at all. Downstream of the first.Both are reproduced in full below.
Part 1 —
plans/cvs-config-api-refactor.mdOne config API for the training and inference suites
Scope: vllm, atom, sglang, megatron, torchtitan, jaxmaxtext.
Health / rccl / ibperf / platform / mori come later.
The problem
Six suites, six config loaders, six sets of conventions.
Each suite invented its own answer to the same questions: where do paths live, how is
a container declared, what is a sweep, what happens when the config is wrong. An
engineer who knows vLLM cold still cannot read an atom config without reading atom's
loader first.
The idea
Blocks — typed, reusable config fragments. A suite composes the ones it needs.
Registry — maps the suite name from
cvs runto its schema. One load path.Rules — the checks no single block can do: sweep-vs-thresholds, config-vs-cluster.
What a suite must implement
Today this contract is implicit, and every suite guessed differently. Making it
explicit is most of the value.
sweep.entries()+cell_key(entry)cells(), coverage rule, parametrize IDs, report lookupcontainerblockthresholdsblockrules— cross-block and cluster checksfixcontractTwo methods. Everything else a suite writes today —
load_variant,validate_sweep_selector,validate_thresholds_cover_sweep,_check_no_changeme,expected_cells,orchestrator_container_from_variant— moves into the framework.See Appendix B for exactly what each suite implements today and what survives.
The sweep problem, solved once
What "cell key" means
Every sweep produces cells, and each cell's key is a string that must exactly match
a key in the threshold file. Nothing checks that the two agree in format — only that
they agree in content.
Today that string is built three different ways, with three different signatures:
Four distinct signatures for one conceptual operation —
(isl, osl, concurrency),(combo_key),(), and no method at all.No shared code can call that. Which is why every suite reimplements coverage
checking, parametrize ID generation, and report cell lookup on top of it.
The fix: split enumeration from formatting
entries()is where the shapes differ.cells()is where they stop differing.cell_keystays suite-owned — atom's driver branching is real and cannot begenericized — but it now has one signature across all six, so framework code can
call it.
What an entry carries
The shapes share no dimensions at all —
isl/osl/concurrencyvsgbs/mbs/precisionvs arbitrary maxtext overrides — soSweepEntrycannot havetyped dimension fields. It has three:
dimsis whatcell_keyformats.payloadis what the job consumes. Nothing else.vllm —
sequence_combinations[{name, isl, osl, goodput_slo}]+runs[{combo, concurrency}]:megatron / torchtitan —
combinations{id: {...}}+runs[id]:jaxmaxtext —
training.sweeps[]+training.enabled_sweep_list[]:cell_keythen readsdimsand nothing else:The drift this closes
jaxmaxtext is the case that shows why deriving the key matters. Today its dimensions
exist only inside the name string, which is also the threshold key. The config's own
comment states
GBS = per_device_batch_size * total GPUs, and the arithmetic holds inall three shipped configs:
So the name is derivable but hand-maintained. Change
per_device_batch_sizeto 4and forget to rename, and the key still resolves against the threshold file — because
the name is the key — while advertising a GBS that never ran. Nothing catches it.
Declaring dims and deriving the name closes that. The migration does not require
rewriting configs first: keep the literal
name, and have load assert derived ==declared, reporting a
Problemon mismatch. That is the drift check today and thedeletion path tomorrow.
What that buys
Everything downstream stops caring which sweep shape it got:
cells()cells()cells()And a new sweep shape costs one
entries()method — nothing else changes.Five shapes, one contract
ComboSweepsweep.sequence_combinations[]+sweep.runs[{combo, concurrency}]MatrixSweepsweep.combinations{id: …}+sweep.runs[id]nameNamedSweeptraining.sweeps[]+training.enabled_sweep_list[]nameBenchmarkSweep{active_benchmark, benchmark_params}— select-onemodel_paramslookup, no sweep constructNote jax's sweep is nested under
training, not at the top level like the others —one more thing a newcomer has to discover by reading a loader.
MatrixSweepcarries three names per entry — the dict id, an innername, and thederived threshold key — and no two are equal. See the departure section below.
sglang's
BenchmarkSweepselects one benchmark rather than enumerating several, and itderives cells from the threshold file, inverting the direction of truth. That's an
open decision, not a solved case.
The 13 no-sweep configs cannot express a second run at all; they need a synthesized
single entry under any scheme.
Departure: one
runsblockThe section above keeps the shapes and hides them behind a contract. The alternative is
to not keep them: one shape, declared names, every key function deleted. This is a
departure from how sweeps work in every suite today, so it is written out in full.
The inventory is five shapes, not three
Counted across all shipped config files:
sweep.{sequence_combinations[], runs[{combo, concurrency}]}cell_key(isl, osl, concurrency)sweep.{combinations{id: …}, runs[id]}cell_key(combo_key)training.{sweeps[], enabled_sweep_list[]}nameis the key{active_benchmark, benchmark_params}perf_cell_key(bp_dict)model_paramslookupShape 4 reaches beyond the six suites in scope:
pytorch_xdituses it too, in 2 moreconfig files. Widening the scope adds shapes rather than reusing them.
Two corrections to what this doc said earlier. Megatron and torchtitan do have a
sweep construct — shape 2 — in their current configs; it is only their legacy files
(3 megatron, 6 torchtitan) that fall into shape 5. And sglang's configs are shape 4,
not "no sweep block."
Shape 5 matters: 13 config files cannot express a second run at all. Those suites
are mid-migration already, which is the strongest argument that this is convergence
rather than imposition — four different teams reached for a sweep block and each
invented a different one.
The key functions
8 definitions across 6 modules in 4 signatures, plus
expected_cells()in 6:cell_key(self, isl, osl, concurrency)atom_config_loader.py:147,vllm_config_loader.py:226,inferencing_config_loader.py:187,sglang_config_loader.py:326cell_key(self, combo_key)megatron/.../training_config_loader.py:147,torchtitan/training_config_loader.py:139perf_cell_key(bp_dict)sglang_config_loader.py:97(module-level)perf_cell_key(self)sglang_config_loader.py:331Three names for one run
A megatron combination declares an id, a
name, and a threshold key — and no two areequal:
To find the threshold for a run, a reader has to know which of the three the lookup
uses and then reconstruct it from a formatter in Python.
Declare-then-select is dead weight
Three of the five shapes declare entries in one list and enable them in another. In the
shipped single-node DeepSeek vllm config, 3 combos are declared and 1 is run — the
other two are inert text the reader must recognise as inert. jaxmaxtext's two lists are
character-for-character identical, so its selector selects everything.
The two selector implementations also disagree on strictness: vllm's
validate_sweep_selectorraises on an unknown reference, while jaxmaxtext'swarnings.warns and silently runs a wider sweep than asked for.The proposal
Four changes, in increasing order of departure:
expected_cells()delete. The key is in the file.
--runs a,b). JSON hasno comments, so an in-file selector is the only way to disable an entry today — a
CLI flag removes the need.
Today a vllm sweep can vary ISL, OSL and concurrency and nothing else; megatron can
vary GBS, MBS and precision and nothing else. Sweeping any other axis means editing
Python. Overrides let a run vary any field the schema has.
result_dictlifts out to the siblingthreshold file, where every other suite already keeps it.
What it costs
Denormalization. vllm declares ISL/OSL once and references it from N runs; a flat
list repeats them. Mitigated by inheritance — the config body holds the base and each
run declares only its delta — but a wide sweep is more lines than today.
Names can lie.
GBS=32in a declared name is hand-written and unchecked. Forjaxmaxtext that is already true; for vllm and megatron it is a regression from a key
that is currently computed. This is the one place the departure is strictly worse, and
it is the open decision below.
13 files need a synthesized single run to move off shape 5.
Migration is a script, not a re-calibration
cell_key()is deterministic. Set each declarednameto exactly the stringcell_key()emits today and no threshold file changes at all — 42 shape-1 configsand 12 shape-2 configs convert mechanically, thresholds untouched. The departure can
land without a single re-measured number.
The cross-product question reopens
This doc argued against an implicit
matrix:cross product because generated namesdrift against hand-written threshold keys. That objection dies once thresholds are
emitted from the run rather than typed: the name and the key come from one source
and cannot disagree. So the sequencing is — threshold emitter first, then
matrix:becomes safe sugar that expands to named
runsat load.Open decisions
rendered display string; or long names plus a validator recomputing the derivable
dimensions. Anything auto-resolved cannot appear in a name computed at collection
time, which pushes toward opaque.
truth. Converting it is a restructure of all 5 configs, not a rename.
matrix:sugar — after the emitter, or not at all.extra="forbid"— a typo'd key otherwise yields a run thatlooks swept and isn't. Merge must be deep;
_deep_mergecurrently exists in sixAST-identical copies.
What changes, per suite
Paths,ModelSpec,ContainerConfig; all 19 params typedstr; drops unknown keys on load; 4 validatorsComboSweep; real types; unknown keys rejectedSweep,validate_sweep_selectorandvalidate_thresholds_cover_sweepsideways out of vllm's module; 6 validators; ownexpand_sweep,orchestrator_container_from_variantComboSweepfrom the library; no cross-suite import; sweep expansion and orchestrator handoff move to framework_is_legacy_root→ legacy vs unified), but all 5 shipped configs are legacy — noschema_version, noframework, top-levelconfig/benchmark_params; 526-line conftest; two key functions (cell_key+perf_cell_key); derives cells from thresholds;orchsubsets cluster hostspathsblock — paths live in aconfiggrab-bag with NCCL, topology and run flags; ownMatrixSweep,validate_sweep_selector,validate_thresholds_cover_sweep,_check_no_changeme; unknown run refs warn and skippaths;MatrixSweepfrom library; those four functions become framework; unknown refs are errorspathsblock; byte-identical copies of megatron's sweep block and all four functions — differs only in class namespaths; the duplicate file shrinks to a schemaNamedSweepwith name-as-key (already the right pattern); ownvalidate_thresholds_cover_training; a bad enable-list entry silently widens the runNamedSweepfrom library; coverage becomes the shared rule; enable-list mismatches are errorsThe shared fixtures are not equally shared
"One
orch, onehf_token" hides very different amounts of work. Measured bycomparing the six implementations directly:
_deep_mergecluster_dictorchroles.server.env— the one-env-block change erases that. sglang is genuinely different.hf_tokenSo the dedup is cheaper than it looks for
_deep_mergeandorch, and more expensivethan it looks for
hf_token, which is not a copy-paste problem but a behaviourdisagreement:
Identical cluster, identical pre-staged model: vllm runs, atom skips. Which
you get depends only on which suite you picked. Unifying the fixture forces that
question to be answered once — that is a decision, not a refactor.
Config files do change — for three suites, not one
schema_version, noframework, top-levelconfig/benchmark_paramspathsblock; split theconfiggrab-bag17 of the 62 config files across these six suites — not 5. (The 57 "envelope"
configs counted elsewhere in this doc exclude sglang's 5 entirely, because they carry
no
schema_version. That omission is itself the finding.)megatron and torchtitan have no
pathsblock at all —they keep paths inside a
configcatch-all that mixes four concerns:That is why megatron's
hf_tokenreadsvariant_config.config['hf_token_file']whileevery inference suite reads
variant_config.paths.hf_token_file.The payoff is concentrated in sglang. 138 of its 453 loader lines (30%) exist only
to reverse-engineer a missing
pathsblock —_infer_models_dirrecoversmodels_dirfrom
container_config.volume_dict,_infer_shared_fsrecoversshared_fsbystring-slicing
log_dir, plus_legacy_server_env,legacy_container_block_from_inference,legacy_paths_from_inference,_is_legacy_root,_load_legacy_variant. Give sglang areal
pathsblock and all 138 lines delete.What genuinely does not change
variant_configstill arrives as a typed object. Raw dict access intest and job code is already near zero — 0–2 sites per suite — so the port does not
reach into job internals.
container.model_dump()keeps its current contract.One capability the framework must decide on
sglang's
orchrewrites the cluster dict to scope the orchestrator to a subset ofhosts, branching three ways (single / distributed / disaggregated) on which suite was
selected. No other suite subsets hosts. Either the framework supports host-subsetting
as a first-class concept or sglang keeps a suite-owned
orch. That is the one item onthis list that is an architecture question rather than a port.
What a suite author writes
Then the conftest calls
load_config()instead ofload_variant(). That is the port.What the user sees when it's wrong
Today: one error, raised at the first problem, sometimes mid-run.
After:
Every problem at once, before anything launches, each naming the next action. The
fixline is mandatory on every rule — if you can't say what the user should do, thecheck isn't ready.
Threshold ergonomics
Two questions, answered separately:
enforce_thresholdsfalsefalsetruetrueFlip one boolean to move between gated and ungated, file left in place. Today a
threshold file is mandatory even when nothing is being gated.
Why this is cheap
Most of it already exists in
cvs/lib/utils/config_loader.py— "framework-agnosticconfig machinery shared by every CVS suite."
substitute_configBaseVariantConfig(atom, sglang, jaxmaxtext)Paths,ModelSpec,ContainerSpec,RuntimeSpecalready live thereThe genuinely new code is the registry (~30 lines), the
Problemtype, theentries()contract, and the threshold-optional change. The rest is moving blocks that exist into
a library, and deleting the copies.
Phasing
The six do not cost the same, so they do not go in one bucket.
Problem+ 3 shared rules. No suite changes.pathsblock. One of each kind.Phase 2 is deliberately one of each kind so both paths are proven before Phase 3
replays them. sglang is last because it is the only suite whose port is also an
architecture decision.
Unregistered suites keep their current loader throughout. Nothing is deleted until
its replacement is proven.
The ask
cells()as the single producer means eitherregenerating existing threshold files or keeping per-suite formatters for
compatibility.
a record-only run? Without it, "flip to enforce" means hand-authoring the file.
Appendix A — Block reference
Every block, what it holds, and who declares it.
Paths/ModelPathsSplit by whether the suite pulls models. All six training/inference suites use
ModelPaths; the later suites usePaths.min_length=1is load-bearing — a present-but-empty path currently satisfiesstrand fails later, mid-run.
Participates in three-pass substitution:
{user-id}from the cluster file, thenself-reference (
{shared_fs}), then cross-block ({paths.log_dir}).ModelSpecOwns the
remote=1 not implementedguard. The guard lives here — not on the enclosingconfig — so a suite composing
ModelSpecwithout the shared envelope still gets it.ContainerSpec/RuntimeSpec/RuntimeArgsContainerSpecis closed — four members.RuntimeArgsis open because runtime flagsare the runtime's vocabulary, but the members CVS reads are declared so they're typed
and discoverable.
container.model_dump()must keep producing whatOrchestratorConfigconsumes.env— one blockEnvironment variables are set once for a run. One block, at the top level — which is
what the code already does, as below.
Today there are four places they live, plus a fifth that is declared and unused:
roles.server.envtraining.env_varsmegatron_lib.py:540-560TORCH_NCCL_ASYNC_ERROR_HANDLING,NCCL_IB_*torchtitan_lib.py:389-393HSA_FORCE_FINE_GRAIN_PCIE,PYTORCH_HIP_ALLOC_CONFcontainer.runtime.args.envThe per-role scoping the current schema allows is not merely unused — the code
actively collapses it. vllm writes one env script and has both processes source it:
So
roles.server.envalready reaches the client. The name says otherwise, which isexactly the kind of thing a newcomer has to read the job code to discover.
The rest of the evidence agrees:
rolesis exactly{server}in 42 of 42 configs that have it.(
mi30x_sglang_deepseek_r1_0528_disaggregated.json) splits node lists, ports, andpolicies — but not env.
The two hardcoded sets matter most for a newcomer. There is no config field for them,
so tuning NCCL on megatron means finding
megatron_lib.py:551and editing librarycode. Folding them into
envwith defaults makes them visible and overridable.If disaggregated serving later needs a split,
roles.<role>.envmerging over thebase block is purely additive — existing configs keep working, no migration. Of the 8
env keys in the corpus most are cluster-wide (
HF_HUB_OFFLINE,TRANSFORMERS_OFFLINE,GPU_ARCHS), butVLLM_ROCM_USE_AITERandAMDGCN_USE_BUFFER_OPSare kernel-selectionflags that could plausibly differ between a compute-bound prefill node and a
memory-bound decode node. That is the trigger to build it — base plus override, not two
parallel blocks as today. Deferring until then costs nothing.
Redaction is framework-owned against one declared secret-key set, and applies wherever
the block is rendered. Suites do not write their own: a per-suite regex is a per-suite
chance to get the escaping wrong, and the failure is silent.
ThresholdSpecA tagged union on
kind. Seven kinds, and they do not share a field set.infominvaluemaxvaluemax_msvaluemin_tok_svaluewithinvalue,tolerance_pctmin_ratiovalue,referenceA union is right here and wrong at the top level: this set is closed and owned by one
evaluator that already switches on exactly these seven strings. Suite schemas are
open-ended, which is why the registry keys on a plain string instead.
A misspelled
kindcurrently reaches the evaluator and is reported per-metric at runtime. Here it fails at load.
The sweep blocks
Each implements
entries() -> list[SweepEntry]. See the sweep section above for theentry shape and the implementations. sglang's select-one
BenchmarkSweepand the 13no-sweep configs are not modelled here — both are open decisions.
Two rules apply to all of them:
runs/enabled_sweep_listis an error. A sweepthat drops a bad reference reports green for a run that never happened — megatron
warns and skips today, and jax silently widens the run.
BaseVariantConfig— preset assemblyA convenience, not a requirement. It carries no validation of its own — every
guard lives on the block it belongs to. A suite needing a different set composes
blocks directly and loads through the same registry entry.
The membership is not a guess. Across the 57 envelope configs on disk:
frameworkandgpu_archare in every config and declared separately in everysuite schema. Six declarations of a universal field is exactly the duplication this
removes.
Appendix B — Per-suite implementation inventory
What each suite writes today. ✓ = own implementation, ↗ = imported from another
suite, — = not present.
BaseVariantConfigload_variantcell_keycell_keysignatureexpected_cellsvalidate_sweep_selectorvalidate_thresholds_cover_*_check_no_changemeexpand_sweeporchestrator_container_from_variant@model_validator@field_validatorAfter: every row above except
cell_key, the suite schema, and suite-specificrules moves into the framework.
cell_keystays, with one signature.The megatron / torchtitan columns are identical because the files are — their sweep
blocks and all four helper functions differ only in class names.
Part 2 —
plans/cvs-config-generation.mdCVS config generation
Companion to
cvs-config-api-refactor.md. That docdefines the config schema and the
autoresolution phase. This one is about getting aconfig file to exist in the first place.
It is downstream of the refactor and cannot be read as freestanding. Generation
emits whatever shape the schema defines; if the schema's sweep departure lands after a
generator ships, every generated file is immediately legacy. Sequencing is treated
explicitly at the end.
Scope is the same six suites: vllm, atom, sglang, megatron, torchtitan, jaxmaxtext.
The problem this solves
cvs copy-configis generation today, and it isshutil.copyfile(
copy_config_plugin.py:129). It hands the user a template with<changeme>still init. Across
cvs/input/config_file/that is 75 files carrying 354 occurrences, outof 145 configs total. Authoring the rest by hand is the friction.
The asymmetry is the tell:
cvs generate cluster_jsontakes--hosts(or a hosts file), expands192.168.1.10-20andhost[1-10], renders a Jinja template.So CVS already believes in generation — it just stopped at the easier of the two files.
Generation is not resolution. Resolution (
"auto", in the companion doc) fillsvalues in memory at run time: zero files, always current, nothing for the user to see or
tune. Generation produces an artifact. Both consume the same probes, so the probe layer
gets built once with two consumers — but they are not substitutes, because a config the
user cannot see is a config the user cannot tune.
The extension point already exists
GeneratorPlugin(cli_plugins/generate_plugin.py:43-92) is an ABC with dynamicdiscovery over
cvs/input/generate/andcvs/reports/generate/. Addingcvs generate configis dropping one file into a directory — no CLI plumbing, noregistry edit. Jinja2 is already a dependency and templates already have a home at
cvs/input/templates/<kind>/.The probes exist too, all currently unwired for this purpose:
lib/utils/ib_discovery.pyautoin atom/vllm fabric resolutionparse_mem_usage,lib/utils/gpu.py_du_bytes,tests/inference/vllm/vllm.py:142get_model_from_rocm_smi_outputNothing here needs inventing. It needs wiring and a front door.
Case study: vllm llama3.1-70B
vllm ships exactly two configs for this model —
mi300x_vllm_llama31-70b_fp8_single.json(83 lines) and
..._distributed.json(87). A field-level diff of the two returns ninedifferences, and not one of them is knowledge:
params.nnodes"1"→"2"len(orch.hosts)params.master_addr<changeme>orch.hosts[0]params.pipeline_parallel_size"1"→"2"= nnodeson the mp backendroles.server.ib_netdev<changeme>ip -4 -o addr showon the host's own IProles.server.ib_hca_devices"auto"params.master_port"29501"container.namew1_…→w2_…sweep.sequence_combinationsw1_isl=1000_osl=1000→w2_…sweep.runscombo: "w1_…"→"w2_…"The last two are worth dwelling on. The sweep entries are otherwise identical — same
isl, sameosl, samegoodput_slo, same concurrency. Andcell_key(
lib/inference/utils/vllm_config_loader.py:226-238) formats the threshold key fromisl,osl,tp,ppandconcurrency: the combonamenever enters the key.So the
w1_/w2_prefix changes no behavior, no lookup, and no result. It is adistinction that distinguishes nothing.
Strip the cosmetics and an entire 87-line file exists to express five derivable facts
and one port number.
It is not a vllm quirk
Counting files whose names differ only by a topology suffix (
_single,_distributed,_disaggregated,_multinode):23 files in 11 topology sets, spanning all six suites plus jax. 12 of them are the
redundant members. Every suite independently decided topology is a filename axis.
And the easy case isn't easy either
The single-node file still carries two
<changeme>:threshold_jsonandcontainer.image. Neither is cluster-specific. Both are knowable. So even thezero-multinode path is not a zero-edit path today.
Possible solutions
The design question is what generation reads from.
num_prompts: 320is not a default, it is somebody's measured result.(model, cluster)at run timeRecommendation: S1 now, S2 as the migration target. S1 ships against the corpus that
already exists and is honest about where its numbers came from. S2 is what S1 becomes
once generation has revealed which fields actually vary across models — that information
does not exist yet, and guessing it now is how the wrong recipe format gets locked in.
S3 and S4 are coherent positions, but each surrenders something already asked for.
Independent of S1–S4: topology stops being a file axis
One config;
nnodesfrom the cluster file;ppderived. The 12 redundant files abovestop needing to exist. This is the largest concrete win in the generation story and it
does not depend on which source-of-truth option wins.
The nine ideas
G1 — Clone-and-retarget, not synthesize
The 145 shipped configs are accumulated tuning knowledge.
num_prompts: 320,client_poll_count: 90, theserve_args— none are schema defaults; they are per-modelresults someone measured. Generation picks the nearest shipped config as a base and
re-targets it. Pydantic can dump structure; it cannot dump knowledge.
Falls out for free: the missing-architecture problem. There is no MI325X vllm config
anywhere — but generating from the MI300X one and swapping the threshold reference
produces a usable starting point instead of nothing.
G2 — Invert the front door: cluster-first
Today the flow is pick a config, hope it fits the cluster. Flip it:
Same fit arithmetic as the preflight gate, run as a generator rather than a
validator. Minimum input stays node IPs, plus the two questions nothing can answer for
the user.
G3 — Freeze the tuning surface,
autothe hardwareThe split rule for what gets written as a literal versus left as
auto:num_prompts, concurrency,max-model-len, sweep shaperocm_dir,gpu_archautonnodes,master_addrThe failure mode to avoid is freezing everything — a fully concrete config generated on
cluster A silently misdescribes cluster B.
G4 — Generate the pair, in record mode
Emit the config and its threshold file together, zeros throughout,
enforce_thresholds: false. That is exactly the intended lifecycle: the first run ismeant to be an easy pass that records.
G5 — Every generated value carries its provenance
"Why is
num_prompts320?" should be answerable from the file. The sources are a smallclosed set:
probe,cluster-file,recipe:<name>,default,you. CVS already hasthe
_comment_Xconvention to carry it.Generation also gets to fix an existing problem: the
_example_keys in themegatron and jax configs ship real foreign-cluster values (
bnxt_re0-7,ens51f1np1,rocep28s0). AGENTS.md forbids shipping cluster-specific values precisely because userscopy them. A generator replaces those with what was discovered on their cluster.
G6 — Stamp it, diff it, never clobber it
CSPs will commit these files. Stamp the source recipe and its version;
--diffshowswhat moved upstream since; refuse to overwrite a file the user has edited.
Generated-then-owned is the normal lifecycle, not an edge case.
G7 — Write outside the package
The harness runs from a
site-packagescopy, not the git source. Generated configs mustland in a user-owned path — writing into
cvs/input/config_file/puts them where thenext
pip installerases them.G8 — Starter sweeps, not a blank
runsblockOffer smoke / qualification / full envelope and populate
runsaccordingly. Thisis where "set up sweeps across any axis" actually gets delivered: most users want a good
default sweep, and the ones who don't will edit it — which is the point of writing a
file rather than resolving in memory.
G9 — The round-trip test is the honesty gate
generate → load → validatemust pass with zero edits, for every (suite, model) thecatalog claims to support. One test in CI, and generation cannot rot as schemas move.
It is also the only mechanical acceptance test for "a user with only node IPs can run a
suite." Without it, that goal is an aspiration that cannot fail.
Where this fits
Generation owns the head of the pipe. The refactor owns everything from
loadonward.They meet at exactly one contract — the schema.
Two consequences worth stating plainly.
Generation is the refactor's acceptance test. G9 is what proves the schema is
actually usable from a standing start, rather than merely well typed.
Sequencing is a real risk. The refactor proposes replacing five sweep shapes with one
flat
runsblock. A generator shipped before that lands writes files in the old shape,and the migration then has two producers to fix instead of one. Either generation
targets the post-departure shape from day one, or it ships after the departure.
Preference: target the post-departure shape, and let the generator be the first
consumer that proves it works.
What generation cannot do
Three things stay the user's, and the design should stop pretending otherwise:
Two questions plus node IPs is the floor. Everything else is derivable, probeable, or
already shipped.
Open questions
that only running S1 produces.
both, wizard when a TTY is present.
generation (
--arch mi355x --nodes 2) is weaker but works from a laptop. Supportingboth means offline emits more
auto.model naming in the companion doc.
cvs copy-configsurvive? If generation covers the same ground with betteroutput, keeping both means two ways to get a config and one of them ships
<changeme>.