Use when implementing an L4 model_arch under simulator/src/arch/ after its L3 worklets build — the full-model wire file for one worker type that forwards ModelCfg into worklet configs, compiles the per-iter CostTree once, and exposes IterwiseUnifiedModel. Covers the live code contract: build_configs/resolve_configs/ build + cost_tree/eval_iter + the Scale{n} layer fold + the config.rs selector. Follows the code, not design.md's retired dry_run/JitPlan/Describe. NOT for L3 worklets (impl-compose-worklet).
Install
npx skillscat add syfi-servingstudio/servingstudiosim/impl-compose-arch Install via the SkillsCat registry.
Impl Compose Arch
You implement one L4 model_arch under simulator/src/arch/, the full-model wire
for one worker type. Its L3 worklets must already build. This is the top of the
bottom-up build; after it, register the arch selector so the launcher can run it.
Code is the contract: the live arch
uses build_configs / resolve_configs / build + cost_tree +IterwiseUnifiedModel over the CostTree — NOT the design doc's dry_run →JitPlan or Describe (retired; dry-run is now build against a dry-run bridge,
and the label render lives in CostNode::Labeled).
Read First
simulator/src/arch/README.md— the L4↔L5 contract, the build shape, theScale{n}fold.simulator/src/arch/llama3_dense.rs— the reference dense-local arch (every
member below);arch/llama3_dense_tp.rsfor a TP variant.simulator/src/arch/contract.rs—IterwiseUnifiedModel/UnifiedArchInput.simulator/src/arch/config.rs+model_cfg.rs— the arch selector enum +ModelCfg.
File & scope
arch/<family>.rs (or <family>_{attn,ffn}.rs for split worker types),
re-exported via mod.rs. You also register the selector in arch/config.rs
(IterArchSel tagged enum) so the launcher/deployment can pick it. Different
backends/algorithms = different arch files (no in-file if/match polymorphism).
Contract (mirror llama3_dense.rs)
- a per-arch numeric parallel struct (e.g.
DenseParallel { gpu_name }) — each
arch owns the parallel dims it needs (the sharedParallelCfgunion is retired). *Configsaggregate — one field per worklet/op*Config+num_layers.*Resolvedaggregate — one field per worklet*Resolved(atomic ops pass their*KernelConfigstraight through).*Model—name+ the built worklets/ops +cost_flat: Vec<FlatCostNode>+n_slots(the CostTree compiled once at build).build_configs(&ModelCfg, &Parallel) -> *Configs— forward dims 1:1, bakegpu_name, pick each role'sbackends. No bridge.resolve_configs(&Configs) -> *Resolved— call each worklet::resolve_config(&cfgs.<field>); nothing else.build(name, resolved, bridge) -> Result<*Model, BuildError>—Op::new/*Worklet::buildeach member withformat!("{name}.<slot>"), then compile the
tree once:let tree = model.cost_tree(); model.cost_flat = tree.flatten(); model.n_slots = tree.n_slots();.cost_tree(&self) -> CostTree— build withCostTreeBuilder; wrap the
homogeneous decoder layer inCostNode::Scale { n: num_layers, child }(leaves
minted once, the fold supplies×num_layers); aLabeledroot;b.finish(root).eval_into(&self, batch, ev)— the single eval body, streaming leaves in exactcost_treeorder (the cursor must filln_slots).impl IterwiseUnifiedModel—eval_iter(fillslots,CostTree::aggregate),eval_iter_with_inputs(same viaEvaluator::with_inputsfor cost_log),cost_log_manifest,kv_cache_layout,gpus_per_replica.- an arch-specific
kv_cache_layout(resolved) -> KvCacheLayout(KV layout is
arch knowledge, so it lives here, not onModelCfg). Build it fromKvCacheParts (arch/kv_layout.rs), one per kind of cached state:per_tokenfor GQA/MLA KV,compressed(name, tokens_per_entry, …)for a
DSA indexer or compressed cache,sliding_windowfor SWA or a short-conv
window,per_requestfor a GDN/KDA state or ring buffer.- Bytes are summed over the part's layers and every rank of one KV
partition (one attention TP group). For GQA usegqa_kv_bytes_per_token,
which counts heads replicated when TP exceedsnum_kv_heads. A state every
rank holds whole (MLA latent, recurrent state) is described for one rank
and finished with.held_on_each_rank(attn_tp). bytes_per_entryis the model's own state. Put what vLLM allocates on top
in the overhead, never inbytes_per_entry:.padded_to(…)(padded head,
mamba page, packed group page),.with_copies(…)(speculative blocks), andEngineOverhead { block_tokens, fixed_device_bytes }(large hybrid block,
null block). Do not add align mode's extra state copy; L5 adds it.- Mark state a prefix hit cannot resume from with
.not_prefix_reusable()
(bounded-replay SWA, a buffer vLLM never caches). - When a vLLM capture exists, pin the layout with a unit test that resolves it
at the logged KV budget and checksKvAccounting::concurrency_at(max_model_len)
against the logged "Maximum concurrency" (pattern:kv_cache_layout_reproduces_vllms_packed_groupsindeepseek_v4_vllm.rs/deepseek_v41_vllm.rs, 0.5% tolerance). A larger miss usually means a
missing part or overhead term. - Read the result instead of recomputing it:
launcher <preset> --kv-report
prints the layout per GPU, one request's per-part charge and the
concurrency, andsimulator cost-treescarries the arch'skv_cache
(skilldev-calculate-kv-cache-capacity).
- Bytes are summed over the part's layers and every rank of one KV
Rules
build_configsnever touches the bridge (M2);resolve_configsonly calls each
worklet'sresolve_config(M3).- Dry-run =
buildagainst a dry-run bridge (it tallies missing specs); there is no
separate traversal /JitPlan. Scale{num_layers}mints per-layer leaves once;eval_intostreams in exactly
the ordercost_treeminted them.gpu_nameis the single source of truth threaded into every kernel lookup.- Multi-rank fan-out uses
CostNode::Max/LeafMetricswith overlap_factor1.0
at L4 — real <1.0 overlap only lives inside an L3 worklet. Concurrent streams
on one device (an auxiliary-stream branch) useCostNode::Parallel, neverMax: Optimality folds everyMaxto its mean and reports the gap as imbalance. - Preserve each worklet's source barriers and common spine. L4 may fold
homogeneous layer repetition withScale, but must not clone phase-specific
trees, expand ranks/chunks into slots, or move children across a join. A
serial-stream architecture variant changes composition only; it keeps the
same worklets, leaves, inputs, and operation boundaries.
Tests And Smoke
- Unit-test
build_configs(dims +gpu_namethreaded) andresolve_configs
(worklet shapes baked) with no bridge (asllama3_dense.rs). just test-cpu.- Real smoke: run the launcher dry-run so
buildruns against a dry-run bridge —uv run python -m launcher <preset>.json --dry-run(the arch must be registered
inconfig.rsand reachable from a preset). It prints the compiled cost-tree
manifest + the missing-spec tally.
Next — hand off to impl-wire-new-arch. This skill's proof is the no-bridge
unit tests (the arch builds in isolation). Making the arch reachable from a
config/predictor and a worker — the arch/build.rs builder + itsbuild_{iter,attn,ffn}_model predictor dispatch, and the deployment arm — is the
wiring phase. The launcher-reachable dry-run / timing-predict smoke above only
passes once that wiring exists.
Report Back
Return: files changed (arch file + config.rs selector); the *Configs /*Resolved aggregates; the cost_tree structure (Sum/Scale shape) + slot count;
the IterwiseUnifiedModel methods implemented; cargo test + launcher dry-run
output; and any unsupported gpu/dtype the build rejects.