config: constructing a config no longer resolves it (#35907)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4bc79a1b49
commit
64aa859da2
@@ -48,6 +48,11 @@ def _launch_server_target(launch_server_func: Callable, server_args: ServerArgs)
|
||||
|
||||
|
||||
def launch_or_reuse_server(launch_server_func: Callable, server_args: ServerArgs):
|
||||
# Resolve in the parent, before the fork. The pipeline probes the device
|
||||
# (the default attention backend reads the CUDA capability), and a forked
|
||||
# child cannot re-initialize CUDA once this process has.
|
||||
server_args.resolve_once()
|
||||
|
||||
base_url = resolve_base_url("", server_args.host, server_args.port)
|
||||
|
||||
# Reuse an already-running server instead of forking a second one onto the
|
||||
|
||||
@@ -398,7 +398,7 @@ def _create_ray_engine_backend(server_args: ServerArgs):
|
||||
placement_group=pg,
|
||||
placement_group_bundle_index=0,
|
||||
),
|
||||
).remote(**dataclasses.asdict(server_args))
|
||||
).remote(**dict(server_args._raw_input))
|
||||
|
||||
class _Proxy:
|
||||
"""Forwards method calls to the remote RayEngine actor."""
|
||||
@@ -432,15 +432,18 @@ def throughput_test(
|
||||
server_args: ServerArgs,
|
||||
bench_args: BenchArgs,
|
||||
):
|
||||
# A programmatic caller may hand over a freshly constructed record, and
|
||||
# the backends below read the resolved paths and the raw snapshot.
|
||||
server_args.resolve_once()
|
||||
if bench_args.backend == "engine":
|
||||
if server_args.use_ray:
|
||||
backend = _create_ray_engine_backend(server_args)
|
||||
else:
|
||||
backend = Engine(**dataclasses.asdict(server_args))
|
||||
backend = Engine(server_args=server_args)
|
||||
if not backend:
|
||||
raise ValueError("Please provide valid engine arguments")
|
||||
elif bench_args.backend == "runtime":
|
||||
backend = Runtime(**dataclasses.asdict(server_args))
|
||||
backend = Runtime(**dict(server_args._raw_input))
|
||||
else:
|
||||
raise ValueError('Please set backend to either "engine" or "runtime"')
|
||||
|
||||
@@ -569,6 +572,7 @@ def cli_main():
|
||||
raise e
|
||||
|
||||
server_args = ServerArgs.from_cli_args(args)
|
||||
server_args.resolve_once()
|
||||
bench_args = BenchArgs.from_cli_args(args)
|
||||
|
||||
logging.basicConfig(
|
||||
|
||||
@@ -989,8 +989,10 @@ def latency_test(
|
||||
|
||||
|
||||
def main(server_args, bench_args):
|
||||
# Post-init write to the legacy cuda_graph_max_bs_decode field would
|
||||
# not propagate to cuda_graph_config; update the decode phase directly.
|
||||
server_args.resolve_once()
|
||||
|
||||
# The legacy cuda_graph_max_bs_decode field does not propagate; set the
|
||||
# decode phase.
|
||||
if server_args.cuda_graph_config is not None:
|
||||
server_args.cuda_graph_config[Phase.DECODE].max_bs = max(bench_args.batch_size)
|
||||
|
||||
|
||||
@@ -1255,6 +1255,7 @@ def cli_main():
|
||||
args = parser.parse_args()
|
||||
|
||||
server_args = ServerArgs.from_cli_args(args)
|
||||
server_args.resolve_once()
|
||||
bench_args = BenchArgs.from_cli_args(args)
|
||||
|
||||
run_benchmark(server_args, bench_args)
|
||||
|
||||
@@ -102,6 +102,10 @@ def launch_server_internal(server_args):
|
||||
def launch_server_process_and_send_one_request(
|
||||
server_args: ServerArgs, compile_args: CompileArgs
|
||||
):
|
||||
# Keeps the device probe out of the fork below, for a caller that reaches
|
||||
# this without resolving first.
|
||||
server_args.resolve_once()
|
||||
|
||||
proc = multiprocessing.Process(target=launch_server_internal, args=(server_args,))
|
||||
proc.start()
|
||||
base_url = f"http://{server_args.host}:{server_args.port}"
|
||||
@@ -177,6 +181,8 @@ def compile_server_args(args, compile_args: CompileArgs) -> ServerArgs:
|
||||
args.watchdog_timeout = compile_args.timeout
|
||||
args.warmups = "compile-deep-gemm"
|
||||
server_args = ServerArgs.from_cli_args(args)
|
||||
# `cuda_graph_config` is None until resolution parses it.
|
||||
server_args.resolve_once()
|
||||
server_args.cuda_graph_config[Phase.DECODE].backend = Backend.DISABLED
|
||||
server_args.cuda_graph_config[Phase.PREFILL].backend = Backend.DISABLED
|
||||
print(f"Disable CUDA Graph and Torch Compile to save time...")
|
||||
|
||||
@@ -393,6 +393,10 @@ class Runtime:
|
||||
if is_port_available(port):
|
||||
break
|
||||
self.server_args = ServerArgs(*args, log_level=log_level, port=port, **kwargs)
|
||||
# The spawned server gets a copy of this record, and this object keeps
|
||||
# reading it afterwards -- `get_tokenizer` wants the downloaded GGUF
|
||||
# file and the rewritten ModelScope path, not what the caller typed.
|
||||
self.server_args.resolve_once()
|
||||
|
||||
self.url = self.server_args.url()
|
||||
self.generate_url = self.url + "/generate"
|
||||
@@ -455,7 +459,7 @@ class Runtime:
|
||||
from sglang.srt.utils.hf_transformers_utils import get_tokenizer
|
||||
|
||||
return get_tokenizer(
|
||||
self.server_args.tokenizer_path,
|
||||
self.server_args.tokenizer_path or self.server_args.model_path,
|
||||
tokenizer_mode=self.server_args.tokenizer_mode,
|
||||
trust_remote_code=self.server_args.trust_remote_code,
|
||||
revision=self.server_args.revision,
|
||||
|
||||
@@ -15,6 +15,10 @@ suppress_noisy_warnings()
|
||||
|
||||
def run_server(server_args):
|
||||
"""Run the server based on the gRPC flags and server_args.encoder_only."""
|
||||
# The flags dispatched on below are decided by resolution (`--grpc-mode`
|
||||
# folds into `smg_grpc_mode`), and `prepare_server_args` returns raw input.
|
||||
server_args.resolve_once()
|
||||
|
||||
if server_args.encoder_only:
|
||||
# For encoder disaggregation
|
||||
if server_args.smg_grpc_mode or server_args.grpc_mode:
|
||||
|
||||
@@ -287,11 +287,11 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
||||
dist_timeout=self.server_args.dist_timeout,
|
||||
)
|
||||
|
||||
from sglang.srt.runtime_context import get_context
|
||||
from sglang.srt.runtime_context import get_context, publish
|
||||
from sglang.srt.server_args import ServerArgs as SrtServerArgs
|
||||
|
||||
if get_context()._server_args is None:
|
||||
get_context().set_server_args(SrtServerArgs(model_path="dummy"))
|
||||
publish(SrtServerArgs(model_path="dummy"), role="diffusion_gpu_worker")
|
||||
|
||||
# set proc title
|
||||
if model_parallel_is_initialized():
|
||||
|
||||
@@ -20,7 +20,6 @@ from __future__ import annotations
|
||||
import json
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from types import SimpleNamespace
|
||||
|
||||
import torch
|
||||
|
||||
@@ -67,7 +66,12 @@ def _srt_trace_server_args():
|
||||
prev_server_args = srt_server_args_module.get_global_server_args()
|
||||
except ValueError: # nothing published yet
|
||||
prev_server_args = None
|
||||
set_global_server_args_for_scheduler(SimpleNamespace(trace_modules="request"))
|
||||
# publish resolves what it is handed, so a stand-in cannot go through it.
|
||||
from sglang.srt.server_args import ServerArgs as SrtServerArgs
|
||||
|
||||
set_global_server_args_for_scheduler(
|
||||
SrtServerArgs(model_path="dummy", trace_modules="request")
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
|
||||
@@ -1081,6 +1081,8 @@ class Engine(EngineScoreMixin, EngineBase):
|
||||
|
||||
# Configure global environment
|
||||
configure_logger(server_args)
|
||||
server_args.resolve_once()
|
||||
|
||||
_set_envs_and_config(server_args)
|
||||
|
||||
# Defensive: ensure plugins loaded (may already be loaded by
|
||||
|
||||
@@ -12,6 +12,10 @@ from sglang.srt.utils import MultiprocessingSerializer, kill_process_tree
|
||||
|
||||
|
||||
def launch_server_process(server_args: ServerArgs) -> multiprocessing.Process:
|
||||
# Resolve here, not in the child: the pipeline probes the device, and a
|
||||
# forked child cannot re-initialize CUDA if this process already has. The
|
||||
# child's gate then finds nothing left to do.
|
||||
server_args.resolve_once()
|
||||
|
||||
p = multiprocessing.Process(target=launch_server, args=(server_args,))
|
||||
p.start()
|
||||
|
||||
@@ -464,8 +464,8 @@ class RayEngine(Engine):
|
||||
|
||||
# Set dist_init_addr on server_args so PortArgs.init_new() can compute
|
||||
# TCP addresses correctly (required for DP attention path).
|
||||
dp_server_args = dataclasses.replace(
|
||||
server_args,
|
||||
dp_server_args = server_args.replace_resolved(
|
||||
"ray.dp_controller",
|
||||
dist_init_addr=f"{rank0_node_ip}:{port_args.nccl_port}",
|
||||
)
|
||||
# Create the DP controller in-process. This blocks until all actors
|
||||
|
||||
@@ -47,8 +47,6 @@ class SchedulerActor:
|
||||
dp_rank: Optional[int],
|
||||
dist_init_addr: Optional[str] = None,
|
||||
):
|
||||
import dataclasses
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.managers.scheduler import Scheduler, configure_scheduler_process
|
||||
from sglang.srt.utils.numa_utils import (
|
||||
@@ -56,10 +54,11 @@ class SchedulerActor:
|
||||
numa_bind_to_node,
|
||||
)
|
||||
|
||||
# Override dist_init_addr if provided (for multi-node)
|
||||
# Override dist_init_addr if provided (for multi-node), through
|
||||
# `replace_resolved` so the copy keeps the parent's resolution.
|
||||
if dist_init_addr:
|
||||
server_args = dataclasses.replace(
|
||||
server_args, dist_init_addr=dist_init_addr
|
||||
server_args = server_args.replace_resolved(
|
||||
"ray.scheduler_actor", dist_init_addr=dist_init_addr
|
||||
)
|
||||
|
||||
# Get actual GPU IDs from Ray runtime context
|
||||
|
||||
@@ -1034,10 +1034,11 @@ class _ServerArgsOverride:
|
||||
self._prev_capture = ctx.flags.capture.enable_torch_compile
|
||||
from sglang.srt.arg_groups.overrides import (
|
||||
_apply_fields,
|
||||
declare_resolution,
|
||||
declare_late_resolution,
|
||||
)
|
||||
|
||||
server_args = ServerArgs(model_path="dummy")
|
||||
server_args.resolve_once()
|
||||
# Underscore names seed private property caches (the strict guard
|
||||
# exempts them); everything else must be a real config field.
|
||||
unknown = {name for name in self._fields if not name.startswith("_")} - set(
|
||||
@@ -1047,23 +1048,19 @@ class _ServerArgsOverride:
|
||||
raise ValueError(
|
||||
f"override_server_args: unknown ServerArgs field(s): {sorted(unknown)}"
|
||||
)
|
||||
# Declared so the projection sees it.
|
||||
# Declared so the projection sees it; late, because the record is
|
||||
# resolved already and not yet published.
|
||||
# Underscore names are not fields at all (they seed private property
|
||||
# caches), so they stay a direct write.
|
||||
declared = {
|
||||
name: value for name, value in self._fields.items() if name[0] != "_"
|
||||
}
|
||||
if declared:
|
||||
declare_resolution(server_args, "override_server_args", **declared)
|
||||
declare_late_resolution(server_args, "override_server_args", **declared)
|
||||
_apply_fields(
|
||||
server_args,
|
||||
{name: value for name, value in self._fields.items() if name[0] == "_"},
|
||||
)
|
||||
# The dummy boundary skips materialization, which would leave the
|
||||
# strict mutation guard unarmed on the published object — mark it
|
||||
# materialized so bare post-publish writes raise like they do on a
|
||||
# fully resolved config.
|
||||
object.__setattr__(server_args, "_declarations_materialized", True)
|
||||
ctx.set_server_args(server_args)
|
||||
self._installed = True
|
||||
return server_args
|
||||
@@ -1205,6 +1202,9 @@ ROLE_NAMESPACE_SETS: dict[str, frozenset[str] | None] = {
|
||||
"encoder": None,
|
||||
"expert_backup": None,
|
||||
"weight_cache_daemon": None,
|
||||
# The diffusion GPU worker runs a model and publishes a placeholder so
|
||||
# shared SRT reads do not fail closed; declared full for that reason.
|
||||
"diffusion_gpu_worker": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -1325,6 +1325,7 @@ def publish(server_args, *, role: str, hf_config: Any = None) -> RuntimeContext:
|
||||
f"publish role {role!r} has no ROLE_NAMESPACE_SETS entry; declare "
|
||||
"its namespace set (None for the full tree)."
|
||||
)
|
||||
server_args.resolve_once()
|
||||
discarded = _CONTEXT.overrides_log()
|
||||
_CONTEXT.set_server_args(server_args)
|
||||
if discarded:
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import dataclasses
|
||||
import functools
|
||||
import glob
|
||||
import importlib
|
||||
import importlib.util
|
||||
@@ -28,7 +30,6 @@ import random
|
||||
import socket
|
||||
import tempfile
|
||||
import uuid
|
||||
from functools import cached_property
|
||||
from typing import Any, Callable, Dict, List, Literal, Optional, Union
|
||||
|
||||
from sglang.kernels.ops.kv_canary.consts import RealKvHashMode
|
||||
@@ -3642,7 +3643,96 @@ class ServerArgs:
|
||||
] = None
|
||||
|
||||
def __post_init__(self):
|
||||
self._run_resolution_pipeline()
|
||||
"""Construction leaves the record at what the caller asked for.
|
||||
|
||||
Resolution is a separate act, entered through ``resolve_once``: the
|
||||
launcher runs it once per engine, and every publishing process asks the
|
||||
gate on the way in. A record that is only constructed -- a fixture, a
|
||||
config being inspected, one being handed to a subprocess that will
|
||||
resolve it itself -- stays raw.
|
||||
"""
|
||||
|
||||
def resolve_once(self) -> None:
|
||||
"""Run the resolution pipeline, unless this record has been through it.
|
||||
|
||||
Resolution is a deterministic function of the raw inputs -- two records
|
||||
built from the same arguments declare the same things -- but the
|
||||
handlers do not survive a second pass over their own output: DP
|
||||
attention halves ``chunked_prefill_size`` again on every re-entry.
|
||||
|
||||
The publishing entry of every process calls this. In a child the record
|
||||
arrived by pickle and brought its declarations along, so the child has
|
||||
nothing left to derive and projects what the parent decided.
|
||||
"""
|
||||
if getattr(self, "_declarations_materialized", False):
|
||||
return
|
||||
if getattr(self, "_resolution_failed", False):
|
||||
raise RuntimeError(
|
||||
"resolution already failed on this ServerArgs; the handlers that "
|
||||
"ran left their writes on the record, and a second pass would "
|
||||
"read that partial output as fresh input. Build a new record "
|
||||
"from the corrected arguments."
|
||||
)
|
||||
try:
|
||||
self._run_resolution_pipeline()
|
||||
except BaseException:
|
||||
# The handlers that ran already wrote to the record, and they are
|
||||
# not idempotent over their own output.
|
||||
object.__setattr__(self, "_resolution_failed", True)
|
||||
raise
|
||||
# Set here too, because the dummy/absent-model path returns before the
|
||||
# materialization that normally sets it: the gate is about whether the
|
||||
# handlers ran, not how far they got.
|
||||
self._declarations_materialized = True
|
||||
|
||||
def replace_resolved(self, source: str, **changes: Any) -> ServerArgs:
|
||||
"""A copy of this record that stays resolved, and says what it changed.
|
||||
|
||||
`dataclasses.replace` builds a new instance, so the copy carries none of
|
||||
what makes a record resolved: no raw snapshot, no declarations, no
|
||||
materialization. The next publish therefore finds an unmaterialized
|
||||
record and runs the pipeline over values it already decided -- DP
|
||||
attention halves `chunked_prefill_size` a second time (8192 -> 4096 ->
|
||||
2048) and the schedule conservativeness is scaled again (0.3 -> 0.09).
|
||||
The Ray paths replace `dist_init_addr` on a resolved record, which is
|
||||
how they hit it.
|
||||
|
||||
The change is appended to the stash rather than left on the field: the
|
||||
projection reads the raw snapshot plus the declarations, so a field the
|
||||
copy set on its own would publish the parent's raw value instead.
|
||||
|
||||
The carry is shallow. The containers are copied so the copy's own
|
||||
declaration does not travel back into the parent, but everything inside
|
||||
them -- the stash entries, the raw-input values, the memoized
|
||||
`ModelConfig` -- is shared. That is fine for what this is for: a copy
|
||||
that immediately crosses a process boundary (Ray actors, the gateway's
|
||||
workers), where pickling severs the sharing. A caller that mutates the
|
||||
copy's deep structure in-process mutates the parent's too.
|
||||
"""
|
||||
replacement = dataclasses.replace(self, **changes)
|
||||
if not getattr(self, "_declarations_materialized", False):
|
||||
# Not resolved yet: the copy goes through the gate itself.
|
||||
return replacement
|
||||
|
||||
# Everything outside the fields, enumerated from the instance: the raw
|
||||
# snapshot, the stash, and what resolution memoized -- including the
|
||||
# `get_model_config()` cache, which a resolved copy can no longer fill
|
||||
# (the read-only guard refuses the write).
|
||||
field_names = {field.name for field in dataclasses.fields(self)}
|
||||
for name, value in vars(self).items():
|
||||
if name in field_names or name == "_declarations_materialized":
|
||||
continue
|
||||
if isinstance(value, (dict, list, set)):
|
||||
value = copy.copy(value)
|
||||
object.__setattr__(replacement, name, value)
|
||||
stash = getattr(replacement, "_resolved_overrides", None)
|
||||
if stash is None:
|
||||
stash = []
|
||||
object.__setattr__(replacement, "_resolved_overrides", stash)
|
||||
if changes:
|
||||
stash.append((source, dict(changes)))
|
||||
object.__setattr__(replacement, "_declarations_materialized", True)
|
||||
return replacement
|
||||
|
||||
def _declare(self, source: str, **fields: Any) -> None:
|
||||
"""This record's handlers declaring their resolution writes.
|
||||
@@ -9522,8 +9612,17 @@ class ServerArgs:
|
||||
# Lazy init to avoid circular import
|
||||
from sglang.srt.configs.model_config import ModelConfig
|
||||
|
||||
if hasattr(self, "model_config"):
|
||||
return self.model_config
|
||||
memo = getattr(self, "model_config", None)
|
||||
if memo is not None:
|
||||
# A configuration built before resolution describes the path the
|
||||
# caller typed; the GGUF and ModelScope handlers declare a
|
||||
# different `model_path`, and every later decision keyed on the
|
||||
# architecture would read the wrong contents. Only a real
|
||||
# `ModelConfig` is checked -- a fixture's stand-in stays untouched.
|
||||
if not (
|
||||
isinstance(memo, ModelConfig) and memo.model_path != self.model_path
|
||||
):
|
||||
return memo
|
||||
self.model_config = ModelConfig.from_server_args(self)
|
||||
if self.model_config.is_hybrid_swa:
|
||||
logger.info(
|
||||
@@ -9556,9 +9655,9 @@ class ServerArgs:
|
||||
# get_context().override(source, ...); a value one runner or worker
|
||||
# owns travels as a constructor argument to it.
|
||||
if (
|
||||
not name.startswith("_")
|
||||
and getattr(self, "_declarations_materialized", False)
|
||||
getattr(self, "_declarations_materialized", False)
|
||||
and not getattr(self, "_internal_write", False)
|
||||
and (not name.startswith("_") or name in _underscore_field_names())
|
||||
):
|
||||
raise AttributeError(
|
||||
f"server_args.{name} assigned after resolution; server_args is "
|
||||
@@ -9599,30 +9698,47 @@ class ServerArgs:
|
||||
def enable_mamba_extra_buffer_lazy(self) -> bool:
|
||||
return mamba_extra_buffer_lazy_of(self)
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def max_speculative_num_draft_tokens(self) -> Optional[int]:
|
||||
"""Return the maximum draft-token count speculative decoding may use."""
|
||||
"""Return the maximum draft-token count speculative decoding may use.
|
||||
|
||||
Memoized only once the record is resolved: an answer computed off a raw
|
||||
record describes inputs resolution is about to rewrite (auto speculative
|
||||
sizing fills `speculative_num_draft_tokens` in), and a cache filled that
|
||||
early would keep answering with it.
|
||||
"""
|
||||
memo = self.__dict__.get("_max_speculative_num_draft_tokens")
|
||||
if memo is not None:
|
||||
return memo
|
||||
if self.speculative_num_draft_tokens is None:
|
||||
return None
|
||||
if not self.speculative_adaptive:
|
||||
return self.speculative_num_draft_tokens
|
||||
result = None
|
||||
elif not self.speculative_adaptive:
|
||||
result = self.speculative_num_draft_tokens
|
||||
else:
|
||||
from sglang.srt.speculative.adaptive_spec_params import (
|
||||
resolve_candidate_steps_from_config,
|
||||
)
|
||||
|
||||
from sglang.srt.speculative.adaptive_spec_params import (
|
||||
resolve_candidate_steps_from_config,
|
||||
)
|
||||
|
||||
candidate_steps = resolve_candidate_steps_from_config(
|
||||
cfg_path=self.speculative_adaptive_config,
|
||||
)
|
||||
# TODO: adaptive spec currently requires topk=1, so each runtime state
|
||||
# needs steps + 1 draft-token slots. Revisit this if topk>1 is supported.
|
||||
return max(candidate_steps) + 1
|
||||
candidate_steps = resolve_candidate_steps_from_config(
|
||||
cfg_path=self.speculative_adaptive_config,
|
||||
)
|
||||
# TODO: adaptive spec currently requires topk=1, so each runtime
|
||||
# state needs steps + 1 draft-token slots. Revisit this if topk>1
|
||||
# is supported.
|
||||
result = max(candidate_steps) + 1
|
||||
if getattr(self, "_declarations_materialized", False):
|
||||
object.__setattr__(self, "_max_speculative_num_draft_tokens", result)
|
||||
return result
|
||||
|
||||
@property
|
||||
def mamba_cache_chunk_size(self) -> int:
|
||||
# For mamba cache with extra buffer, the chunk size is the max of FLA_CHUNK_SIZE
|
||||
# (or mamba_chunk_size if it is defined in the model's config) and page_size.
|
||||
# It is used to determine the caching point in a sequence during prefill.
|
||||
# A pre-seeded `_mamba_cache_chunk_size` (fixtures supply one so a dummy
|
||||
# model never loads an HF config) is honored as-is; otherwise the memo
|
||||
# is only kept once the record is resolved, because `page_size` below
|
||||
# is resolution-written.
|
||||
if not hasattr(self, "_mamba_cache_chunk_size"):
|
||||
|
||||
try:
|
||||
@@ -9639,6 +9755,8 @@ class ServerArgs:
|
||||
assert (
|
||||
max(chunk_size, page_size) % min(chunk_size, page_size) == 0
|
||||
), f"For SSM models, either chunk_size or page_size must be divisible by the other, got {chunk_size=}, {page_size=}"
|
||||
if not getattr(self, "_declarations_materialized", False):
|
||||
return max(chunk_size, page_size)
|
||||
self._mamba_cache_chunk_size = max(chunk_size, page_size)
|
||||
return self._mamba_cache_chunk_size
|
||||
|
||||
@@ -10331,6 +10449,23 @@ def m3_fp8_attn_gemm_enabled(args) -> bool:
|
||||
# reference. Do not add new call-sites — the counts are ratcheted
|
||||
# (decrease-only) by test/registered/unit/test_legacy_global_ratchet.py.
|
||||
# Imports are in-function so the two modules stay cycle-free at import time.
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _underscore_field_names() -> frozenset:
|
||||
"""Real dataclass fields whose names start with an underscore.
|
||||
|
||||
The read-only guard exempts underscore names because they are the record's
|
||||
own bookkeeping (the stash, the flags, the memoized model config). A *field*
|
||||
that happens to start with an underscore is still resolved configuration --
|
||||
`_speculative_draft_quantization_explicitly_set` is one -- and exempting it
|
||||
by spelling would leave exactly one leaf writable on a read-only record.
|
||||
"""
|
||||
return frozenset(
|
||||
field.name
|
||||
for field in dataclasses.fields(ServerArgs)
|
||||
if field.name.startswith("_")
|
||||
)
|
||||
|
||||
|
||||
def set_global_server_args_for_scheduler(server_args: ServerArgs):
|
||||
"""Legacy publish shim (role=scheduler) — prefer
|
||||
``runtime_context.publish(server_args, role=...)`` in new code."""
|
||||
|
||||
Reference in New Issue
Block a user