Skip to content

config: resolution reads the declarations, not the fields - #36253

Merged
ch-wan merged 7 commits into
mainfrom
cheng/gc-p4-resolution-declarations
Aug 26, 2026
Merged

config: resolution reads the declarations, not the fields#36253
ch-wan merged 7 commits into
mainfrom
cheng/gc-p4-resolution-declarations

Conversation

@ch-wan

@ch-wan ch-wan commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Resolution is a chain: one resolver decides a field, the next one reads that decision.
Today that works only because declare_resolution writes the field as a side effect, so
the record doubles as the scratchpad for a half-finished resolution. That side effect is
what keeps ServerArgs from being what it should be — the raw user input — and it makes
"who decided this value" unanswerable after the fact.

This PR moves every read that happens during resolution onto the declaration seam, so
the field write is no longer the channel the chain communicates through. It changes no
values: the view answers exactly what the field held.

What changes

  • resolving_view(server_args) is a live read view: every attribute read walks the
    declaration stash newest-first and falls through to the raw field. It is what code
    running inside resolution should use. (resolved_view stays for post-resolution
    passes, which want a snapshot.)
  • The arg_groups hooks, the ~779 field reads inside the ServerArgs resolution handlers
    (cfg = resolving_view(self)), the helpers resolution calls with the record, the
    record's own member functions, and the type-based dispatcher all read through the view or
    through resolution_result(server_args, field).
  • The assertions and the tests that inspect a mid-resolution value read the resolution
    result rather than the field.
  • test_resolution_reads_the_declarations.py pins it: functions under arg_groups/* that
    take a config, and every dispatcher-reachable ServerArgs handler, must have zero direct
    field reads. That guard lands with the flip, so at this boundary the sweep is unpinned.

Verified by A/B: for every launch shape in the reproducibility suite, the resolved
projection before and after this PR is identical. Two launch shapes are deliberately not
identical
, both because a pass declares without writing at this point in the stack, so a
read downstream of the pass's slot now sees the declaration instead of the pristine field.
Both restore what the pre-declaration monolith did, and neither is reachable from the CPU
suite:

  • SM100 + EmbeddingGemma: _attention_backend_default declares trtllm_mha, so the
    prefill-only no-KV path is no longer entered on a None read.
  • DSpark + DP attention + waterfill: _a2a_backend_overrides declares deepep, so the
    invalid combination is now rejected at startup instead of admitted and then run on
    deepep.

Three mid-resolution readers stay on the record here and convert with the flip in the last
PR of the stack — get_attention_backends, describe_kv_events_publisher, and
compute_world_size. They are value-identical at this boundary.

How to verify

export PYTHONPATH=$PWD/python
python -m pytest -q test/registered/unit/server_args/test_resolution_is_reproducible.py
python -m pytest -q test/registered/unit/server_args/test_resolution_declarations.py
python -m pytest -q test/registered/unit/server_args/test_resolution_reads_no_bag.py
python -m pytest -q test/registered/unit/server_args/test_server_args.py

🤖 Generated with Claude Code


CI States

Latest PR Test (Base): ❌ Run #32966532779
Latest PR Test (Extra): ✅ Run #32966710903
Latest PR Test (AMD ROCm 7.2): ❌ Run #32966532866

@ch-wan
ch-wan force-pushed the cheng/gc-p4-resolution-declarations branch 2 times, most recently from faac6ba to f17b3c4 Compare August 26, 2026 05:39
@ch-wan
ch-wan force-pushed the cheng/gc-p3-drop-unread-record branch from e744e0d to c976856 Compare August 26, 2026 06:48
@ch-wan
ch-wan force-pushed the cheng/gc-p4-resolution-declarations branch from f17b3c4 to f3f4dae Compare August 26, 2026 06:48
@ch-wan
ch-wan force-pushed the cheng/gc-p3-drop-unread-record branch from c976856 to 3ca3f1b Compare August 26, 2026 07:22
@ch-wan
ch-wan force-pushed the cheng/gc-p4-resolution-declarations branch 2 times, most recently from 71043dd to 0eb2984 Compare August 26, 2026 07:32
@ch-wan
ch-wan force-pushed the cheng/gc-p3-drop-unread-record branch from 3ca3f1b to 1725254 Compare August 26, 2026 08:12
@ch-wan
ch-wan force-pushed the cheng/gc-p4-resolution-declarations branch from 0eb2984 to 6e6947d Compare August 26, 2026 08:12
@ch-wan
ch-wan force-pushed the cheng/gc-p3-drop-unread-record branch from 1725254 to f63fd27 Compare August 26, 2026 08:21
@ch-wan
ch-wan force-pushed the cheng/gc-p4-resolution-declarations branch 2 times, most recently from 444d916 to 4b1f9c3 Compare August 26, 2026 08:59
@ch-wan
ch-wan force-pushed the cheng/gc-p3-drop-unread-record branch from f63fd27 to 02afaaa Compare August 26, 2026 09:37
@ch-wan
ch-wan force-pushed the cheng/gc-p4-resolution-declarations branch from 4b1f9c3 to e82e791 Compare August 26, 2026 09:37
@ch-wan
ch-wan force-pushed the cheng/gc-p3-drop-unread-record branch from 02afaaa to 981bebc Compare August 26, 2026 11:52
ch-wan added 7 commits August 26, 2026 12:02
`declare_resolution` writes the field as it declares, so a resolver that reads
the field afterwards sees the declared value -- and every one of the 1130
mid-resolution field reads in this tree depends on that write. Removing it (the
last step of making `ServerArgs` hold only raw input) currently breaks
resolution outright: 20 of 20 launch shapes die on `AttributeError: 'NoneType'
object has no attribute 'prefill'`.

This is the first half of that: the readers move to `resolving_view`, a live
view that answers from the declaration stash and falls through to the field.
While the immediate write is still there the two agree, so this is a no-op --
which is exactly what makes it checkable: 20 launch shapes, every field of the
resolution result compared against the previous commit, zero differences.

337 reads in `arg_groups`: the speculative hook (146), the override passes
(103), the DeepSeek-V4 and PD-disaggregation hooks, and the three small model
hooks. `ResolvedView` stays for the post-process passes, which want the state
snapshotted at their slot; `ResolvingConfig` is for a reader that outlives a
declaration.
…ields

The other half of the readers: 779 `self.<field>` reads across the 88
resolution handlers reachable from the dispatcher now go through
`resolving_view(self)`. Same argument as the hooks -- while
`declare_resolution` still writes the field as it declares, the view and the
field agree, so this changes nothing and can be checked exactly: 20 launch
shapes, every field of the resolution result compared against the previous
commit, zero differences.

What it buys is that the pipeline no longer depends on that write to see its
own decisions. Removing it -- so the record holds the raw input and nothing
else -- needs the readers outside `arg_groups` and `ServerArgs` that resolution
calls with the record (the platform defaults, the spec-algo hook, `ModelConfig`,
the CP strategy) to move as well; those are next.
The last of the mid-resolution readers outside the pipeline's own modules:
`ModelConfig.from_server_args` (23 reads), the spec-algo hook, the adaptive-spec
support check, the CP strategy binder and the BCG predicate. They are called
*by* resolution with the record in hand, so they have the same problem as the
handlers -- a declaration-only resolver leaves the field holding the raw input.

`resolving_view` is imported inside the function at these sites: they sit under
`configs/`, `layers/` and `speculative/`, and a module-level import of
`arg_groups.overrides` there would be a new import edge into the resolution
pipeline.

`test_model_config_reads_resolved_input` learns the spelling: a local bound to
`resolving_view(sa)` / `resolved_view(sa)` / `sa._resolved()` is the record for
scanning purposes, so its two pins keep describing the reads they were written
for. 20 launch shapes, zero differences in the resolution result.
`ServerArgs`' public members read their fields, which is the same problem the
handlers had: a declaration-only resolver leaves the field holding the raw
input, so `max_speculative_num_draft_tokens`, `is_ep_joiner`,
`is_startup_weight_load_overlap`, the expert-balancedness predicates and
`describe_kv_events_publisher` could answer for what was typed instead of what
resolution decided.

They read through the view now, and the file settles on one spelling for it:
`cfg = resolving_view(self)`, replacing the `resolved = resolved_view(self)` /
`resolved = self._resolved()` mix this file had accumulated. One exception keeps
its own name -- `describe_kv_events_publisher` already binds `cfg` to a
`KVEventsConfig`, and the view has to not shadow it.
A census over the modules the pipeline actually reaches -- the registered passes
and providers plus the import map, the same derivation
`test_resolution_reads_no_bag` uses -- left 26 field reads outside `arg_groups`
and `ServerArgs`: the dLLM config builder, the experimental Marlin LoRA
validator, and the NPU platform defaults.

The NPU one is the reason to bother. `set_default_server_args` asks "did anyone
decide `page_size` yet?" before declaring its own default, and that question
has to be asked of the declarations: reading the field would answer "no" for a
size an earlier pass had already declared, and the hook would overwrite it. No
A/B on a CUDA host can catch that, which is why the census is the check here.

`configure_logger`'s single read stays: `log_level` is raw input, and that
function is called with stand-ins.
Two reads in the dispatcher itself, one of which matters: `get_device_memory_capacity(self.device)` runs right after the platform defaults declare `device`, so a field read there would size memory for `auto`. The dummy-model boundary check moves with it for uniformity.

The tests follow the same rule as the golden model-override ones: what they assert is what resolution decided, so they read `resolution_result` rather than the field -- the CPU-EAGLE overlap constraint, the dSpark draft-path default, the media-domain normalization, the multimodal piecewise-graph gates, the encoder transfer backend, and the spec-registry algorithm name. The multimodal processor fixture seeds the worker counts through `override_server_args` instead of a MagicMock, because the processor reads them from `get_mm()` now.

All no-ops today (the declaration still writes the field as it declares); they are what the flip needs in place first.
Everything that will change hands when the declarations stop writing the field,
moved ahead of the flip so it can be checked while both still agree.

`test_server_args` is the bulk of it (101 reads): what those cases assert is
what resolution decided, and `resolution_result` answers that whether or not
the declaration was written back. Four assertions go the *other* way and now
read the field on purpose -- the FA4 page-size and waterfill cases exist to show
the field staying pristine while the declaration wins, so they keep reading it
and say so.

`_comparable` in the reproducibility suite reads the projection too: comparing
fields would have stopped covering the decisions a resolution leak would shift.
The multimodal and Kimi processor fixtures seed their worker counts and cache
budget through `override_server_args` instead of a stand-in, because the
processor reads them from `get_mm()` / `get_serving()` now -- that also fixes
`test_kimi_processor_workers_clone_the_gpu_wrapper`, which the processor
conversion broke (the cache came out enabled and the fingerprint path ran into a
`SimpleNamespace` hf_config).

The supplied-instance exposure pin drops 19 entries: the model-config,
dLLM, CP, Marlin-LoRA, adaptive-spec and spec-registry reads all go through the
view now.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deepseek Multi-modal multi-modal language model npu ready-to-merge The PR is ready to merge after the CI is green. speculative-decoding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant