Skip to content

config: resolution declares, and nothing writes a field - #36618

Merged
ch-wan merged 1 commit into
mainfrom
cheng/gc-p1
Aug 27, 2026
Merged

config: resolution declares, and nothing writes a field#36618
ch-wan merged 1 commit into
mainfrom
cheng/gc-p1

Conversation

@ch-wan

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

Copy link
Copy Markdown
Collaborator

Motivation

PR 1 of a five-PR series on top of the raw-input ServerArgs work (#36250#36255), based on f775db03aaa. Each builds on the previous one; review them in order.

  1. cheng/gc-p1 — config: resolution declares, and nothing writes a field ← this PR
  2. cheng/gc-p2 — config: every handler declares its cuda-graph decisions
  3. cheng/gc-p3 — config: a parallel leaf with no live counterpart is read bare
  4. cheng/gc-p4 — config: a parallel size has one spelling; a patched scope declares its own
  5. cheng/gc-p5 — config: the record is not an object that gets passed around

They are grouped by how they have to be read, not by topic: PR 3 is 118 files of one mechanical rewrite, reviewed by checking the rule and sampling; PR 4 is the design change that rewrite made possible, and its production files each need reading.

CI for the whole series runs on a separate vehicle PR, whose branch sits one placeholder commit above PR 5: #36623.

The landed stack made ServerArgs hold the operator's input and moved
resolution's answers into the declaration stash. Two channels could still write
a field afterwards, and nothing asserted the result. This closes both and
asserts it.

Modifications

``ef10c68ee45 · 5 files (1 production, 3 test, 1 skill doc) · +323 / −37

The guard

test_record_holds_the_raw_input.py states the whole contract in one place:
after resolve_once(), every field still equals the _raw_input snapshot taken
before any handler ran. Fifteen launch shapes.

Two directions, because they are independent and neither catches the other:

  • a field must not be rebound — the snapshot holds what the caller passed
    and getattr must still answer with it;
  • an operator-supplied mutable must not be edited in place — the record
    points at the caller's own dict or list, so a handler that reaches into one
    changes a value the caller still holds, and a field-by-field comparison
    cannot see it (the snapshot stores the reference).

Both were verified by injection: a setattr inside resolution fails the first
and not the second; cuda_graph_config.setdefault(...) fails the second and not
the first.

The write-through

run_post_process_pass called _apply_fields when a pass ran after resolution
finished. The declaration is already in the stash at that point and publish
projects from the stash, so the write was redundant for the projection and only
served field readers — which the record no longer has. This is the last
production caller.
The helper itself stays where it is, with one caller left:
RuntimeContext.override_server_args, the test stand-in, which publishes a
context for tests that need bags. It lives in arg_groups/, the module the
mutation ratchet exempts by module, and #36622 explains why closing that last
one is a separate pass.

I first made this an error, and that was wrong: check_server_args hosts such a
slot (_hisparse_validation), so the error fired in production. Instrumenting
run_post_process_pass across five launch shapes showed that slot is the only
one that runs after resolution, and it declares nothing. Dropping the write and
keeping the stash append is both correct and smaller.

The registry hole

_a2a_fusion_adjustments and _hrm_text_attention_force were invoked from a
slot but carried no @register_post_process, so every check that enumerates
POST_PROCESS_PASSES — the chain-read ratchet, the declaration scan — walked
past them. Both are registered now, and a static scan of every
run_post_process_pass(sa, fn) call site pins the registry against them.

A runtime spy found only the first: _hrm_text_attention_force runs for one
model family, so a fixture that resolves a Llama config never reaches it.

While there: the list's comment claimed to be the end-state execution order.
Measured, it is not — _hisparse_validation is registered sixteenth and always
runs last, because check_server_args is a later stage than __post_init__.
The comment now says the list is a registry and names the stage split.

The doc

SKILL.md said publish "snapshots the resolved field values into the config
bags". It projects them from the declarations over the raw fields. A reader who
believed the old sentence would look for resolved values on the record.

Accuracy Tests

No model-output change: this series moves where a configuration value is read
from, not what resolution decides. The equivalent check for that claim is a
resolution dump — every field's resolved value for 24 launch shapes (plain, tp2,
tp4_pp2, dp2, EAGLE, NEXTN, page32, page64_chunk2k, cuda-graph knobs,
disaggregation, deterministic, hierarchical cache, symmetric memory, …) — taken
in both trees and compared field by field:

0 differences across 24 shapes × 478 shared fields, against f775db03aaa.

The one field the series has and the base does not is grpc_worker_threads: on
main it is a public non-field slot assigned in _handle_deprecated_args, and
this series makes it a declared field. Its value is 4 on both sides.

Every guard also runs at each commit of the series, not only at the head — a
PR that is green only on top of its successors is not reviewable on its own. The
set is the config guards plus every registered test the series touches, ~33 files
per boundary, all green.

No GPU accuracy run. Everything above is CPU-side: resolution, projection and the
guards. A launch-path change that only shows up with real process groups is not
covered by any of it.

Speed Tests and Profiling

No benchmark run, and none is expected to move: nothing here changes a kernel, a
schedule, or the shape of any batch. What changes is the source of a
configuration read — a published dataclass attribute instead of a process-group
getter or an accessor hop.

The one place that could have mattered is torch.compile: gate helpers read
parallel leaves inside compiled forwards, and object.__getattribute__
graph-breaks. That was measured rather than assumed — the reads this series
introduces trace under torch.compile(fullgraph=True), which is pinned by a
regression test.

Checklist

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

🤖 Generated with Claude Code


CI States

Latest PR Test (Base): ❌ Run #33110212604
Latest PR Test (Extra): ❌ Run #33110429178
Latest PR Test (AMD ROCm 7.2): ❌ Run #33110212668

@ch-wan

ch-wan commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f99e81b7c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +114 to +115
if getattr(server_args, field.name) is not raw[field.name]
and getattr(server_args, field.name) != raw[field.name]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect equal-valued field replacements

When a resolver replaces a caller-supplied mutable with an equal-valued copy (for example, self.lora_paths = list(self.lora_paths)), the is not condition is true but the equality condition is false, so this guard passes even though the record no longer references the operator's original object. This contradicts the test's stated no-rebinding contract and leaves most mutable fields unprotected; treat identity changes as moved fields rather than requiring both identity and value to differ.

Useful? React with 👍 / 👎.

@ch-wan
ch-wan force-pushed the cheng/gc-p1 branch 2 times, most recently from f19507c to ef10c68 Compare August 27, 2026 09:07

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef10c68ee4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread python/sglang/srt/arg_groups/overrides.py
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ch-wan ch-wan added the run-ci label Aug 27, 2026
@ch-wan
ch-wan enabled auto-merge (squash) August 27, 2026 19:51
@ch-wan
ch-wan merged commit bd4bb17 into main Aug 27, 2026
129 of 150 checks passed
@ch-wan
ch-wan deleted the cheng/gc-p1 branch August 27, 2026 19:53
ch-wan added a commit that referenced this pull request Aug 28, 2026
`run_post_process_pass` refused the published record before running the pass,
so a pass that declares nothing could not run twice. `Engine.shutdown()` leaves
the launch published, and `Engine(server_args=sa)` with the same instance calls
`check_server_args()` again before republishing; `_hisparse_validation` reaches
the runner from there and returns `{}`, so the second launch raised.

The refusal now fires once there is something to record, which is what it was
for: a declaration made after publish is a silent no-op because the stash is
projected at publish and never again. A pass that declares nothing is a
validation, and validations are meant to run on every launch.

Reported by the codex reviewer on #36618. Reproduced: resolve, check, publish,
check again -> `ValueError: run_post_process_pass('_hisparse_validation')
called on the published config`.

`test_a_pass_that_declares_nothing_runs_on_the_published_record` pins both
directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ch-wan added a commit that referenced this pull request Aug 28, 2026
`run_post_process_pass` refused the published record before running the pass,
so a pass that declares nothing could not run twice. `Engine.shutdown()` leaves
the launch published, and `Engine(server_args=sa)` with the same instance calls
`check_server_args()` again before republishing; `_hisparse_validation` reaches
the runner from there and returns `{}`, so the second launch raised.

The refusal now fires once there is something to record, which is what it was
for: a declaration made after publish is a silent no-op because the stash is
projected at publish and never again. A pass that declares nothing is a
validation, and validations are meant to run on every launch.

Reported by the codex reviewer on #36618. Reproduced: resolve, check, publish,
check again -> `ValueError: run_post_process_pass('_hisparse_validation')
called on the published config`.

`test_a_pass_that_declares_nothing_runs_on_the_published_record` pins both
directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ch-wan added a commit that referenced this pull request Aug 28, 2026
`run_post_process_pass` refused the published record before running the pass,
so a pass that declares nothing could not run twice. `Engine.shutdown()` leaves
the launch published, and `Engine(server_args=sa)` with the same instance calls
`check_server_args()` again before republishing; `_hisparse_validation` reaches
the runner from there and returns `{}`, so the second launch raised.

The refusal now fires once there is something to record, which is what it was
for: a declaration made after publish is a silent no-op because the stash is
projected at publish and never again. A pass that declares nothing is a
validation, and validations are meant to run on every launch.

Reported by the codex reviewer on #36618. Reproduced: resolve, check, publish,
check again -> `ValueError: run_post_process_pass('_hisparse_validation')
called on the published config`.

`test_a_pass_that_declares_nothing_runs_on_the_published_record` pins both
directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ch-wan added a commit that referenced this pull request Aug 28, 2026
`run_post_process_pass` refused the published record before running the pass,
so a pass that declares nothing could not run twice. `Engine.shutdown()` leaves
the launch published, and `Engine(server_args=sa)` with the same instance calls
`check_server_args()` again before republishing; `_hisparse_validation` reaches
the runner from there and returns `{}`, so the second launch raised.

The refusal now fires once there is something to record, which is what it was
for: a declaration made after publish is a silent no-op because the stash is
projected at publish and never again. A pass that declares nothing is a
validation, and validations are meant to run on every launch.

Reported by the codex reviewer on #36618. Reproduced: resolve, check, publish,
check again -> `ValueError: run_post_process_pass('_hisparse_validation')
called on the published config`.

`test_a_pass_that_declares_nothing_runs_on_the_published_record` pins both
directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ch-wan added a commit that referenced this pull request Aug 28, 2026
`run_post_process_pass` refused the published record before running the pass,
so a pass that declares nothing could not run twice. `Engine.shutdown()` leaves
the launch published, and `Engine(server_args=sa)` with the same instance calls
`check_server_args()` again before republishing; `_hisparse_validation` reaches
the runner from there and returns `{}`, so the second launch raised.

The refusal now fires once there is something to record, which is what it was
for: a declaration made after publish is a silent no-op because the stash is
projected at publish and never again. A pass that declares nothing is a
validation, and validations are meant to run on every launch.

Reported by the codex reviewer on #36618. Reproduced: resolve, check, publish,
check again -> `ValueError: run_post_process_pass('_hisparse_validation')
called on the published config`.

`test_a_pass_that_declares_nothing_runs_on_the_published_record` pins both
directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
saturn-acc pushed a commit to saturn-acc/sglang that referenced this pull request Aug 31, 2026
…36618)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
nzr-niu pushed a commit to nzr-niu/sglang that referenced this pull request Sep 1, 2026
…36618)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation npu run-ci

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant