`init_tokenizer_manager` wrote the chat-template-detected `reasoning_parser` /
`tool_call_parser` onto the published `ServerArgs`, after
`TokenizerManager.__init__` had already projected the config bags — so the
namespace readers and the resolved-config readback disagreed with the instance,
and a second `Engine` in the same process would inherit the first one's
detection through the shared bags.
Detection is per-engine control-plane state, which the manager already models:
`record_config_updates` records it, the readback endpoints overlay it, and
`config_value` reports what is in effect. `OpenAIServingChat` — the only reader
of these two fields in the tokenizer process — follows the overlay. The
architecture pass (`resolve_auto_parsers`, before the schedulers fork) is
unchanged: the scheduler resolves its own bags from the instance it receives.
`sa = get_server_args()` followed by `sa.field` reads the same startup record as
the direct form; the read ratchet added in the previous slice pinned twelve of
them as the remaining surface. Eleven now read the accessor for what they
actually want:
- `is_enable_moe_cp_allgather` compares the attention-CP and MoE-DP sizes to
decide whether a forward needs an allgather, so it reads the live topology
through `get_parallel()` — the same source `get_moe_cp_size()` right above it
already uses. Both groups exist once model-parallel init has run, which is
before any forward.
- The DeepSeek MLA decode-backend gate and Inkling's attention paths read
`get_exec().kernel`; Inkling's KV-dtype checks read `get_model()`. These are
per-runner fields, and the value they get is the config published for the
runner being built — unchanged from what the alias returned.
- The int8 mamba checkpoint pool reads `get_exec().mamba`. It keeps its guard
for callers that construct the pool with no published config; that guard now
catches the namespace accessor instead of the slot.
`model_loader`'s `moe_dp_size` stays on the instance and is exempt: the dict it
belongs to already reports the live size under `"dp"`, so that entry is the
configured intent, and `get_parallel()` shadows the name with the live value.
Alias-form baseline 12 -> 0. What remains on `get_server_args()` in the package
is the derived API (properties and methods computed from several fields plus the
HF config) and four config-intent reads of live-shadowed sizes, each exempt by
name with its reason.
`RayEngine.__init__` set the caller's `PlacementGroup` on `ServerArgs` as an
undeclared attribute, and every consumer read it back off the config object:
the non-DP launch path took `pg` and `is_custom_pg` from it, the DP path had to
re-attach the handle after `dataclasses.replace` dropped it, the DP controller
asked the config whether the group was the caller's, and the Ray HTTP launcher
had to clear the field so a stale handle could not leak in.
A live cluster object is not config. It now travels as a `placement_group`
argument on the two launch hooks — `Engine._launch_subprocesses` and
`Engine._launch_scheduler_processes` — defaulting to `None`, with `RayEngine`
holding the caller's group in `_placement_group` and the DP path passing
`is_custom_pg` down to the controller. The public API is unchanged:
`RayEngine(placement_group=pg, ...)` still works, `pg` still falls back to the
ambient group, and the HTTP launcher gets `None` from the default.
Writer ratchet 18 -> 15.