"Use when planning or estimating a new ServingStudio Sim worker combination, or when implementing, composing, reviewing, renaming, moving, or tidying production L5 worker code under `simulator/src/worker/`: KV stores and capability traits, admission lifecycle/selection policies, execution adapters, cadence shells, family directories, `build_*_worker` recipes, and L6 wiring. Enforces the canonical four-axis ownership, share-vs-new and difficulty analysis, pending-membership rule, shell discriminator, construction/file boundaries, complete changed-file coverage, and behavior-preserving validation. Not for L4 kernel/cost-model implementation or an unrelated general file review."
Install
npx skillscat add syfi-servingstudio/servingstudiosim/dev-compose-worker Install via the SkillsCat registry.
Compose Worker
Apply these rules to production L5 worker changes. Treatdoc/detailed_design/L5.md as the canonical design andsimulator/src/worker/README.md as the production tree guide. Git history may
explain behavior only when the canonical design and production code leave a
detail unclear; it never overrides them.
This skill specializes dev-file-design-review for L5. Use the stricter rule
when the two overlap.
Required reading
Before editing:
- Read
doc/detailed_design/L5.mdcompletely. - Read
simulator/src/worker/README.md. - Read
simulator/src/worker/iter_worker.rs. - Read the target component and concrete family shell completely.
- Read every
build_*_worker.rsrecipe and L6 caller affected by the change.
Do not infer a new name, directory, trait surface, or ownership rule without
support from these sources. Stop for a design decision if the canonical L5
contract does not cover the proposed shape.
1. Classify the change before editing
Use the production equation:
Worker = <KV, Admission<Selection>, Execution> × Shell(cadence)Write down which owner changes:
| Owner | Owns | Must not own |
|---|---|---|
| KV | footprint, capacity facts, reserve/resident/held ledgers, sticky partition ownership, advance/release, KV sampling | selection, model input, shell phase |
| Admission lifecycle | fresh-request state transitions and gates | a shadow pending queue, KV byte arithmetic, model input |
| Selection policy | the sole not-yet-started pending membership and stable order | resident/active cadence state |
| Execution | L4 model, reusable input/cost buffers, state-to-ArchInput lowering, model evaluation |
request ordering, transfer overlap, completion cadence |
| Shell | message protocol, FSM/cadence, grouping, transfer overlap, wakeups, completion ownership | duplicated KV/admission/accounting or model math |
Identify the cadence family separately:
- whole iteration →
workers/iter/IterBatchWorker; - pull plus decode →
workers/pd_decode/PullDecodeWorker; - layer-slot attention →
workers/afd_attention/SlotAttentionWorkerplus its
privateAttentionSlotPipeline; - buffered FFN →
workers/afd_ffn/BufferedFfnWorker.
Reuse a shell unless the change introduces a different asynchronous timeline,
event ordering, synchronization protocol, overlap model, or completion owner.
Batch arithmetic, token counts, selection order, and capacity policy do not by
themselves justify a new shell.
Plan a new composition
Dissect a proposed worker before estimating or implementing it:
- Shell: name the closest production cadence and any new timeline, ordering,
synchronization, overlap, or completion-owner requirement. - KV: list the resource lifecycle and observation capabilities required.
- Admission lifecycle: write the fresh-to-terminal state path; identify
whether one existing lifecycle covers it or multiple behaviors must merge. - Selection: name the ordering facts and algorithm independently from the
lifecycle. - Execution: name the L4 input/cost shape and every KV or shell fact it must
read. - Construction/L6: name the concrete recipe, selector, and protocol caller;
L6 may choose the recipe but must not assemble the axes.
Produce this share-vs-new table:
| Part | Reuse by default | Write only when |
|---|---|---|
| Shell | nearest cadence family | the shell discriminator above fires |
| KV | KvCacheStore plus existing capabilities; a new kind of cached state is a KvCachePart in the arch's layout, not a new store |
resource lifecycle or required observation is absent |
| Lifecycle | nearest family lifecycle | required state transitions cannot be expressed by one existing lifecycle |
| Policy | existing PendingOrderPolicy |
ordering needs new frozen facts or a new algorithm |
| Execution | matching production adapter | input lowering or cost evaluation needs new facts/shape |
| Recipe/L6 | sibling builder and caller pattern | a new concrete composition must be selectable |
Capabilities are additive refinements; lifecycle behavior is single-select. A
combination of capabilities should normally be bounds over one KV owner. If two
lifecycle behaviors are both required, merge them once in one lifecycle rather
than stacking lifecycle objects or copying queues.
Estimate difficulty by the dominant semantic change, not the number of files:
- LOW: recipe/alias, policy swap, config value, or implementation of an
already-defined capability; - MODERATE: one new lifecycle, resource-owning KV leaf/wrapper, execution
adapter, or bounded trait-seam extension; - HIGH: a new cadence shell/protocol, changed completion ownership, or a
challenge to the axis split.
Tag every new part with the smallest fixability class that describes it:
- A — pure add: add a leaf, policy, adapter, or recipe without changing
existing bodies; - B — seam extension: expose one missing capability through a trait bound,
associated type, or narrow method; - C — mechanical reshape: uniformly change signatures while preserving
bodies and ownership; - D — new sub-axis or shell: add a genuinely new capability family or
cadence; - E — re-decide the axes: the ownership equation cannot express the worker.
A–C extend the abstraction. D is substantial but bounded. E is a design-doc
decision: stop before implementation and make the ownership contradiction
explicit.
2. Enforce axis ownership
KV
Keep KvStore as the common resource lifecycle:
footprint → fits → reserve → commit_resident → advance → releaseExpose family observations through the narrow capability traitsIterWorkerKv, SlotPipelineKv, and HandoffKv. Add a capability bound to the
concrete composition instead of adding a role enum or optional method toKvStore.
Keep these invariants:
- one request stays on one KV partition for its KV lifetime;
- an AFD cadence slot is not a KV partition;
- the KV implementation is the only strict capacity/accounting source;
- charges come from the resolved
KvAccountingin KV units; the store never
reads layout parts, andcurrent_kvstays context tokens for the L4 input; - live checkpoints and retained prefixes are the soft tier: evictable, never in
fitsorkv_usage; - input builders consume borrowed visitors/slices instead of cloning live
membership; KvStorenever constructs an L4 input.
When adding or changing a ledger, trace one request through every lifecycle hop.
At each hop name exactly one owner for the quantity. No owner means
over-admission; two owners mean double accounting.
Do not delegate fits or pressure accounting through a wrapper that owns an
additional occupancy ledger. Delegation is sound only for wrappers that keep
configuration/routing while placing the full resource quantity in the inner
footprint and ledgers. A resource-owning wrapper must extend the capacity seam
so its occupancy participates at every lifecycle hop.
Admission and selection
Keep not-yet-started pending membership entirely in PendingOrderPolicy.
Lifecycle implementations must not maintain a second pending container.
Freeze selection facts in AdmissionCandidate at enqueue. Preserve stable tie
behavior and update membership, queued-count, and queued-KV observations
together for push, pop, remove, and cancellation.
Do not confuse a selection queue with active cadence state:
- fresh requests waiting to start → selection policy;
- PD landed/in-transit work →
PullDecodeWorker; - AFD slotted work → slot pipeline;
- FFN tasks →
BufferedFfnWorker.
Use the family-specific admission surface. SlotPipelineAdmission is not anIterAdmission with missing callbacks.
For future chunked prefill, keep fresh requests in the policy and at most one
partially processed active_chunk per partition in lifecycle state. Never pop
and push an active chunk through the policy: that rotates order and changes the
scheduling semantics.
Do not turn alignment observations into worker policy. A trace may supply
request lengths and arrivals, but observed DP rank, equal-shaped adjacency, or
profiler-created chunks are realized outcomes, not admission constraints.
Implement generic chunk lifecycle over the original request stream; keep any
observed-placement replay as an explicitly conditioned experiment outside the
predictive worker.
Execution
Keep model/input ownership in the matching sibling trait:
IterModelExecution<K>;AttentionLayerExecution;FfnTaskExecution.
Build UnifiedArchInput, AttnArchInput, or FfnArchInput only here. Reuse
input and cost buffers. Keep transfer submission in the shell because overlap is
a cadence fact.
When another axis needs a KV fact, carry it through the narrowest production
seam:
- an iteration lifecycle places the capability bound on its
IterAdmission<K>implementation; - an iteration execution adapter places it on its
IterModelExecution<K>implementation; - a family trait that cannot express the needed fact requires a Class B seam
extension, not a shell-side downcast or duplicated calculation.
Check method-generic family traits such as AttentionLayerExecution explicitly:
if the required capability cannot be named at the implementation boundary,
report that as real seam work rather than claiming the tuple already composes.
Shell
Keep the shell concrete and behavior-readable. It owns the local fixpoint loop,
wakeup calculation, protocol messages, and terminal event timing. Do not hide
these behind a universal FSM trait or optional callbacks.
Missing capabilities should fail at the concrete recipe through trait bounds.
Do not repair a composition with runtime role checks, nullable components, or a
shared mega-trait.
3. Use compatibility as a semantic test
Reject a combination only for a semantic contradiction, such as a no-KV FFN
worker exposing decoder-KV behavior. “Not implemented”, “no preset”, “expensive”,
or “the current selector has another name” are not design contradictions.
An unknown variant fits when it needs:
- one new leaf component or necessary thin adapter;
- one declarative build recipe/registration;
- focused contract and behavior tests;
- no copied tick loop, pending ledger, KV ledger, or
ArchInputlowering.
If the proposal requires copied shared machinery, identify the misplaced owner
before coding. Add a new shell only when the cadence discriminator in section 1
fires.
Classify a variant as config only when changing the value does not add a state
transition, ownership rule, event-order branch, or timeline. A behavioral fork
needs a distinct lifecycle, adapter, capability, or shell as appropriate.
Prefer parameter names that describe the mechanism rather than one use case,
and prefer composable with_* configuration over multiplying constructors.
4. Preserve construction and file boundaries
Use workers/<family>/ as the shell-family directory.
- Put each concrete composition in its own
build_*_worker.rs. - Make the recipe visibly import and select KV, lifecycle, policy, execution,
and shell types. - Keep compatibility aliases as names for concrete generic compositions, not
wrapper runtimes. - Let deployments/controllers call recipes; do not make L6 assemble L5 axes.
- Keep construction off
IterWorker.
Move repeated mechanics into a narrowly named essentials helper only when they
are truly mechanical: GPU allocation, capacity derivation/registration, sampler
creation, cost buffers, communication-group registration, and shared context.
An essentials helper must not select:
- a KV implementation;
- an admission lifecycle;
- a selection policy;
- an execution adapter;
- a shell or ingress behavior.
If a helper makes one of those choices, move that choice back into the concrete
recipe.
5. Make shell source read in execution order
For a non-trivial shell or private pipeline, make the module //! block state:
- what the file owns and explicitly does not own;
- the cadence/timelines it runs;
- its accounting, membership, and completion invariants;
- an explicit reading order.
Lay the file out:
- module contract;
- constants and state types;
- worker/pipeline struct;
- construction;
IterWorkerand capability impls;- message handlers;
- tick/fixpoint path;
- helpers immediately after their first caller;
- tests.
Order helpers by the call path, not by categories such as “all getters”.
For a shell-specific message enum, keep enqueue as pure dispatch and map each
variant 1:1 to on_msg_<variant>. Reserve on_msg_* for external message entry
points. A generic shell such as IterBatchWorker may delegate its opaqueA::Msg directly to admission; it must not inspect lifecycle-specific variants.
An inner protocol object may use on_msg_* only when the outer handler delegates
the same external message to it. Do not use on_* for an internal lifecycle
transition.
6. Apply naming-quality checks
Run every touched name through these tests:
- Make the name earn its keep. Keep a single-use helper when its name and doc
encode a real concept or invariant; inline a purely mechanical expression. - Avoid false quantifiers. Do not use
all_,any_, orcountwhen the
value is one flag or one item. - Name the transition. Prefer
begin_decode,commit_resident, orrelease_slotted_requestoverprocess,handle,update, orfinalize. - Qualify thin wrappers. State whose fact is queried:
request_*,slot_*,partition_*, orkv_*. - Use domain names at call sites. Prefer
reserved_requests,classified_specs, orpulling_taskover vagueitems,data, orstate.
Keep an old-reference symbol as a (ref old_name) anchor only when it materially
helps compare behavior with the still-relevant reference implementation. Remove
anchors to deleted local abstractions or superseded design; canonical L5 names
win.
For a rename, update the definition, call sites, tests, comments, docs, and
re-exports together. Use rg before and after, and distinguish same-spelled
methods on sibling types.
7. Require complete change-surface coverage
“Coverage” here means coverage of the changed design/code surface, not only test
line coverage.
Start with git status --short, git diff --name-status, and the relevant
commit/branch diff. Build a file inventory before reviewing or handing off.
Every materially changed file must appear in the review inventory. Do not hide
files under “miscellaneous” or review only the most interesting shell.
For each changed file, record:
| File | Axis/family | Behavior or ownership changed | Callers/consumers checked | Test/doc evidence |
|---|
Expand the review radius according to the owner:
- Shared KV change: inspect every implemented capability, all recipes using
the store, cancellation/release, capacity registration, sampling, and both
iteration/slot consumers. - Admission/policy change: inspect every lifecycle using the policy,
cancellation, queued status/KV, stable ties, and the absence of a shadow
pending queue. - Execution change: inspect every input builder, reusable buffer, cost-log
path, model-layout accessor, and shell call site. - Shell/protocol change: inspect message/event enums, wakeups, empty-work
behavior, status counts, transfer overlap, terminal ownership, the L6
controller, and the end-to-end sim action. - Recipe/essentials change: inspect every sibling recipe, re-export,
deployment factory call, GPU/KV/communication registration, logging setup,
and constructor test. - Shared type or rename: inspect all families, L6 callers, docs/README,
lessons if they are in scope, and stale deleted paths.
For considerable worker work, explicitly cover all production kinds in the
inventory:
- barebone;
- HP unified;
- PD prefill;
- PD decode;
- AFD attention;
- AFD FFN.
Mark a kind “not affected” only after checking its composition/caller. Do not
silently omit it.
Also verify observational surfaces: WorkerStatus, request stages, event
ordering, KV snapshots/capacity metadata, cost logs/manifests, transfer logs, and
summary fields. An abstraction-preserving refactor may still be wrong if one of
these drifts.
Workflow
- Read the required sources and record the dirty-worktree boundary.
- Classify the change by axis and cadence; for a new combination, complete the
share-vs-new table, difficulty estimate, and A–E fixability tags. - Write the concrete proposed composition type and list every required trait
bound before adding bodies. - Inventory the full change surface and affected production kinds.
- Trace the behavior end to end:
enqueue → policy/lifecycle → KV → shell → execution → event/L6. - Implement the smallest ownership-correct change.
- Update module docs,
worker/README.md, canonical L5, and neighboring L4/L6/L7
docs when the contract, construction, or wiring materially changes. - Run the complete change-surface review from section 7; do not stop after the
primary file builds. - Validate and report every skipped or unavailable check.
Validation
Always run:
git diff --check
uv run cargo check -p simulator --lib
uv run cargo test -p simulator --libCheck formatting only for changed Rust files:
rustfmt --edition 2021 --config skip_children=true --check <changed-file.rs> ...Do not use cargo fmt --all for a scoped worker change.
Also run:
rgfor stale file paths, old type names, removed helpers, and incomplete
rename call sites;- focused tests for each changed axis and affected family;
- local Markdown-link and terminology checks when docs/README change.
For a new composition, prove the production tuple rather than adding a sketch
census. Add a focused compile-time trait assertion beside the closest shell and
a constructor test for its build_*_worker recipe. Treat an unsatisfied bound as
the exact additive worklist; do not bypass it with runtime role checks.
When cadence, admission, KV accounting, execution lowering, L6 wiring, or event
timing changes, compare representative unified, HP/MoE, PD, and AFD simulations
as applicable. Use the same resolved preset, workload, model JSON, and filled
profile database on both sides. Compare deterministic modeled outputs and event
semantics. Do not treat wall_s or derived realtime_x as equivalence fields.
Output
For a planning-only request, lead with:
- Difficulty: LOW/MODERATE/HIGH plus the dominant cost;
- the share-vs-new table and A–E tag for every new part;
- the proposed concrete
Shell<K, A, E>shape, omitting axes the family does not
own; - capability-escalation seams and the smallest additive worklist;
- compile/behavior evidence required before implementation is complete.
For an implementation or review, lead with the outcome and ownership verdict.
Include:
- the axis/family classification;
- the complete changed-file coverage table, with every material file accounted
for; - design and naming rules applied;
- affected production kinds and why any are not affected;
- validation commands and results;
- remaining semantic risks, skipped checks, or follow-up work.