Skip to content

[Config] Round 6: the whole series in one diff (review only — do not merge) - #38114

Closed
ch-wan wants to merge 20 commits into
mainfrom
cheng/gc-r6-review
Closed

ch-wan wants to merge 20 commits into
mainfrom
cheng/gc-r6-review

Conversation

@ch-wan

@ch-wan ch-wan commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Do not merge. This branch exists so the series can be read in one place. The
work is in five stacked pull requests, each of which is where review comments
belong:

what it does size
#38046 "unset" gets its own spelling, and the declaration says what it means 8 files
#38047 the field declarations move to their namespaces; the record is assembled from them 18 files
#38048 the record remembers how it was asked for, and is sealed while resolution runs 6 files
#38049 the runtime reads the bags, not the record 83 files
#38113 a namespace declares what it derives, next to what it derives it from 51 files

GitHub shows a stacked PR only against its parent, so there is nowhere to see
the cumulative effect. The empty commit on top of #38113's branch is what makes
this a separate pull request from the same content.

What the series is

Configuration had two things living in one place. ServerArgs was the record of
what the operator typed and the thing the rest of the process read its
settings out of. Those are different questions -- a field nobody set reads the
same as one set to the value resolution would have picked anyway -- and the
answer to the second one changes during startup while the answer to the first
must not.

They are separated into three surfaces:

  • Input -- the record. What the operator typed; None / False means "not
    typed". Written once at construction, sealed for the length of resolution.
  • Decision -- the declaration stash. What resolution decided, with a source.
  • Effect -- the config bags. Decision if there is one, else input. What
    runtime code reads.

Everything in the series follows from that. The declarations move to
arg_groups/fields/, one class per namespace, so a namespace is one file and
one class; the record is assembled from the input half of those classes, which
is what keeps a derived field like attn_tp_size off the wire format; the
runtime's record reads become bag reads; and every value that is a pure function
of the configuration -- the predicates that existed once on each side of
publish, and the six parallel quotients -- becomes one declaration, computed
once at publish into the bag.

Numbers

record field reads in runtime code 199 → 15, each remaining one with a stated reason
server_args.py 4,262 → ~1,000 lines
NS(...) markers on ServerArgs fields 494 → 0
spellings of the mamba extra-buffer predicate 3 → 1
times a parallel quotient is resolved every read → once, at publish
passes whose only job is to fill in a default 0 (a declaration says it instead)
ways to answer for one stamp or live group → the published value

How it was checked

Against origin/main, at each step:

  • the namespace_of map, field by field: 494 / 494, identical;
  • the CLI surface -- every option's default, choices and action: 507 / 507,
    identical apart from the two ratio defaults this series deliberately changes;
  • the resolution result over 24 launch shapes x 489 fields: identical;
  • every name importable from sglang.srt.server_args: nothing lost;
  • every bag read the series introduces (306 of them) names its field's real
    namespace;
  • a full registered-unit sweep, 648 files, against the stack's merge-base:
    19 failures on each side, the same 19, none of them config.

The last one matters more than it looks. An earlier, narrower sweep scoped to
"tests that set config themselves" reported clean while two files were red,
because the reach of a change like this is transitive -- test_kv_index_translator
never names get_parallel(), it constructs an object that does.


CI States

Latest PR Test (Base): ❌ Run #34014678339
Latest PR Test (Extra): ❌ Run #34014678242
Latest PR Test (AMD ROCm 7.2): ❌ Run #34014678365


CI States

Latest PR Test (Base): ❌ Run #34083722007
Latest PR Test (Extra): ❌ Run #34083721784
Latest PR Test (AMD ROCm 7.2): ❌ Run #34083722012

@ch-wan
ch-wan force-pushed the cheng/gc-r6-review branch from e105296 to a6bfbe6 Compare September 6, 2026 08:45
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 6, 2026
@ch-wan ch-wan added the run-ci label Sep 6, 2026
@ch-wan
ch-wan force-pushed the cheng/gc-r6-review branch 6 times, most recently from e068fab to b50162a Compare September 6, 2026 21:57
ch-wan and others added 20 commits September 7, 2026 04:31
…it means

`swa_full_tokens_ratio` and `mamba_full_memory_ratio` carried real values as
their class defaults (0.8, 0.9), so a model family with an opinion had to ask
"is this field still equal to the class default?" to find out whether the
operator had set it. That question has two wrong answers: it says "the operator
set it" as soon as any earlier pass declares the field, and it says "the
operator did not set it" when the operator types the default value.

Both become `Optional[float] = None`. The record carries what the operator typed
and nothing else, and the family test becomes `is None`.

That leaves the other half: something has to say what the field means when
nobody answers. `Arg(fallback=...)` says it in the declaration.

    swa_full_tokens_ratio: A[
        Optional[float],
        Arg(help="...", resolvable=True, fallback=0.8),
        NS("schedule"),
    ] = None

The dataclass default stays `None`. A fallback is not a default: the record is
the wire format, and a child process has to keep being able to tell "unset"
from "set to the value resolution would have picked anyway".

It is applied at the bottom of the *effective* read chain -- override, then
decision, then input, then this -- in `resolution_result`, which the projection,
`/server_info` and every config bag read through. Deliberately **not** in
`resolving_view` / `resolved_view`: those are the decision-over-input surface a
pass reads while it is deciding, and `model_overrides/inkling.py` and
`deepseek_v4.py` both branch on `if cfg.swa_full_tokens_ratio is None` before
declaring 0.1. A `__getattr__` layer is read-time, so a fallback answering there
is not "the generic value, later" -- there is no later, and the family branch
would never fire. Running `_inkling_overrides` both ways:

    fallback on the effective surface only:  family declared swa = 0.1
    fallback also on the view a pass reads:  family declared swa = None

So "resolution first, then the fallback" holds -- not because a step is appended
to the pipeline, but because of which surface the value lives on. The one
in-pipeline reader that wants the effective value, the range check on the ratio,
asks `resolution_result` directly; that is what its comment already claimed it
was doing, and it runs after the model families.

The alternative was a pass that fills the field in when nothing claimed it. That
needs a slot (after the families, or it beats them), a second call site (the
dummy-model short circuit returns long before that slot), an idempotence
requirement so the second call is harmless, and the value written twice -- once
as a literal, once as prose in the help. A declaration needs none of it: nothing
has to run for a field to mean what it says.

Only a value fixed for the life of the configuration belongs here. A default
that depends on the machine (`get_device()`), on another field (`tokenizer_path`
following `model_path`) or on anything impure (`random.randint`) is a decision,
and decisions stay in a hook where their order is visible. Of the 29 fields a
hook currently fills from `None`, about 20 are conditional decisions of that kind
-- seven memory tiers choosing `chunked_prefill_size`, a model family choosing
`max_running_requests` -- and they are not candidates.

`mamba_radix_cache_strategy` keeps `"auto"`: unlike the ratios it already has a
spelling for "unset" that an operator can type and that means exactly that --
only its comparison changes, from the class default to the token itself, which
is the fix the comment at that site already prescribed. With that, neither family
module imports `ServerArgs` any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ServerArgs` carried all 487 declarations in one 4,462-line file, each tagged
with an `NS("...")` marker naming the namespace it belongs to -- structure
supplied by annotation, in a file a namespace away from the
`arg_groups/*_hook.py` that resolves it.

The declarations move to `arg_groups/fields/`, one module per top-level
namespace and one class per leaf namespace (21 of them, `exec.moe` becomes
`exec_.py::ExecMoe`). The class carries the `_NS_PATH` it stands for, so the
module a field is declared in *is* its namespace and the marker is redundant:
`namespace_of` reads the declaring class off the MRO instead. `NS` stays for
the one case a class cannot express -- a single ad-hoc dataclass whose fields
span namespaces, which is what the config-bag tests build.

`ServerArgs` composes the 21. It is still one flat dataclass with 487
attributes, so `server_args.tp_size`, `ServerArgs(model_path=..., tp_size=8)`,
pickling to a subprocess and all 408 parameter sites are untouched. `Model` is
the last base because reverse-MRO order puts the last base's fields first, and
`model_path` -- the one field with no default -- has to stay the first
positional argument.

Two things travel with the declarations. The twelve `*_CHOICES` lists and the
`add_*_choices` adders that extend them move to `arg_groups/choices.py`, since
the fields naming them can no longer import from `server_args` without a
cycle; `server_args` re-exports them for the plugins that have always reached
them there. And five fields whose only annotation element was the namespace
marker become plain annotations -- `A` is `Annotated`, which needs two
arguments, so stripping the marker would have left them invalid.

Verified three ways: the 487-entry `namespace_of` map is identical field for
field, the resolution result is identical across 24 launch shapes x 485
fields, and the CLI registers the same 503 options with the same defaults,
choices and actions.
Ten places still told the reader that a field's namespace comes from an
`NS(...)` marker on the field. Two of them are errors a developer reads when
something is misconfigured -- "no NS namespace" would send them looking for a
marker that no longer exists on any ServerArgs field.

The namespace now comes from the class that declares the field, which is what
`namespace_of` reads, so the messages name the namespace rather than the
mechanism that used to supply it.
The split re-exported the names the extension-point block defines. That misses
a list the block never mentions: `SAMPLING_BACKEND_CHOICES` is read only by the
CLI section, so `add_cli_args` raised `NameError` on the first call -- which no
import-time check catches.

Re-export everything `choices.py` defines. All 33 used to be importable from
`server_args`, and a rule narrower than that has no way to know which ones a
plugin reaches for. The typing names the moved declarations were the last
users of go the other way and are dropped.
Inheriting the namespace classes made the record's contents a property of
which classes happen to appear in a base list. That is correct only while
every namespace declares nothing but operator input, and it stops being
correct the moment a derived field is declared -- `attn_tp_size` belongs in
`parallel.py` next to the leaves it is derived from, and inheriting `Parallel`
would put it on the record, where it is neither input nor safe: the record is
the wire format, so a derived width pickled to a subprocess is a stamp, and a
width has to stay a live read for elastic scale-up to see the group it has.

`collect_input_fields` takes the classes that declare input and returns their
annotations, defaults and namespaces. Each source's annotations are resolved
in its own module and handed on as type objects -- carried across as text they
would be re-evaluated where they land, and the composing module deliberately
imports none of the names the declarations use.

So a namespace can now declare both halves side by side, and which half
reaches the record is one readable call instead of an invariant spread across
a base-class list. Nothing is registered on the derived side yet; this is what
makes it possible.

The order is the old reverse-MRO order, so the fields, the CLI's 504 options
and `model_path` as the first positional argument are all unchanged. Verified
the four ways this refactor can be: the 491-entry namespace map and the CLI
surface are identical to origin/main, the resolution result is identical
across 24 launch shapes x 489 fields, and nothing that was importable from
`server_args` stopped being.
`/server_info` reports `resolved_dict()` -- what resolution decided. There was
no way to ask the other question: what did the operator actually type? The two
are not derivable from each other, and the resolved side cannot stand in for
the raw one, because a field nobody set reads the same as one set to the value
resolution would have picked anyway.

The launcher stores the arguments it parsed and the in-process `Engine` stores
the call that built the record; `/server_info` and `Engine.get_server_info`
report it beside the resolved values, so both surfaces are answerable from one
request. It rides on the record rather than in a field: it describes how the
configuration was asked for, so it is not part of the configuration -- no CLI
flag, no namespace, not in the bags. Being on the record is what lets a
subprocess copy answer the same question the launcher can, and
`replace_resolved` carries it because a copy was launched by whatever launched
its parent.
The read-only guard armed on `_resolution_finished`, so for the whole run of
the pipeline nothing stopped a resolver from assigning a field. Nothing in
`srt/` does -- 0 assignments statically, and 0 writes observed across the
launch-shape matrix with a watching `__setattr__` -- but that was a convention,
and the defect it permits is invisible: a value a resolver wrote onto the
record is indistinguishable from a value the operator typed, which is the one
distinction the record exists to preserve.

Arm it when resolution starts instead. A resolver that assigns a field now
fails at boot with a message naming `declare_resolution`, which is where the
decision belongs: the stash carries a source and leaves the input intact.

`declare_direct_writes` asks for the seal by name through `record_writable`.
It hands the record to an out-of-tree platform plugin that sets fields on it;
those implementations cannot be converted by editing a resolver here, so the
write stays and the diff is captured into the stash afterwards. Naming the
exception is the point -- an in-tree resolver reaching for it is doing
something it should be declaring.

Costs nothing: the 211 test-side assignments all happen before `resolve_once`,
which a post-resolution write already refused.
The record is the operator's input; the bags are what is in effect. A reader
that takes the record and reads a field off it gets the input, which is the
wrong one of the two whenever resolution decided something -- and the mistake
is silent, because for most fields and most launches the two agree.

These seven already read both ways, sometimes in the same expression:
`get_tokenizer(get_serving().tokenizer_path, tokenizer_mode=server_args.tokenizer_mode, ...)`.
Every read here runs after its process publishes -- the two subprocess entry
points publish before anything else, and the engine's own reads all sit below
the launcher's publish -- so each one is a read of the same value from the
surface that owns it.

The parameters stay. Removing them is a signature change on call chains that
reach constructors other implementations override, which is a separate
decision from where a value is read.
Converting the reads left `_resolve_backend`, `_set_all_reduce_flags` and
`_compute_parallelism_ranks` taking a record they no longer name. The dead-
parameter ratchet is what noticed; the parameter and the argument go together
at every call site.

`_resolve_backend` shares its name with an unrelated function in
`flashinfer_comm_fusion`, whose own callers and tests are untouched. Their
callers keep theirs: `init_torch_distributed` still hands the record on.
Sixty-odd more files took the record and read config off it. Every one of them
runs after its process publishes -- the launcher's own reads sit below
`_launch_subprocesses`, the two subprocess entry points publish first thing,
and the serving and model-executor layers only exist afterwards -- so each is
the same value read from the surface that owns it.

Two findings worth keeping.

Eleven reads were `getattr(record, "field", default)`, which an AST scan for
attribute access does not see: the census that said "43 readers" was counting
the shape it could match, not the thing it was after.
`incremental_streaming_output` was read that way twice, and the transcription
tests were the only reason it surfaced.

And not every record read is a bag read waiting to happen. A multimodal
processor's `base_gpu_id` is the instance's, not the process's: two engines in
one process keep different ones, and
`test_publishing_another_config_does_not_move_the_device` exists to say so.
That one stays on the record, while `rl_on_policy_target` beside it moves --
the test suite is what drew the line.

The fixtures move with the code. Tests that hung config off a mock manager now
publish a record, which is what the serving layer reads; where a test states a
value, it says so with `override_server_args` instead of assigning through the
mock.
… didn't

The sweep caught what the file-scoped runs did not. Six functions were left
holding a record they no longer name -- the ratchet names them -- and five test
files drove code that now reads the bags without publishing anything, so the
first read failed closed.

Two reads go back to the record. `RequestMetricsExporter` is handed the
directory it writes to at construction, and a test builds several with
different ones; reading the process's value instead would make them the same
exporter. That is the same line the multimodal processor's `base_gpu_id` sits
on: a value one object owns is not the process's to answer for.
The field split gave every namespace a file, but only for the half an operator
types. The six parallel quotients -- `attn_tp_size` and its siblings -- were
sixty lines of near-identical properties in the runtime context, a file away
from the leaves they are quotients of, so reading `parallel.py` told you what
you could set and nothing about what that decides.

They are declared in `Parallel` now, in the same class as those leaves. They
carry no annotation, so they are not dataclass fields and
`collect_input_fields` never puts them on the record -- the same mechanism that
already keeps `_NS_PATH` off it. That is the right exclusion: a quotient has no
operator input to preserve, and the record is what crosses a process boundary,
so a width put there would be a stale copy the moment an elastic scale-up
restamps one.

A declaration names the field and says what it means, and nothing else. What a
quotient *is* is a function of the leaves beside it, computed by
`derive_parallel_widths`; how a value that moved after publish reaches a reader
-- the stamp, and the live group below it -- is a property of the reading, so
that table stays in `runtime_context` where `_derived_width` uses it.

`ParallelContext` installs a property per declaration instead of carrying its
own list, so the two cannot drift; a test asserts the declared set is exactly
what `derive_parallel_widths` returns, and that none of them is a record field.
Properties rather than `__getattr__`: these are read inside compiled model
code, where an attribute load is traceable and a dynamic lookup is not.
The same question was asked three ways: a `ServerArgs` member for the
resolution pipeline, a `runtime_context` function for readers after publish,
and the helper both delegated to. Three places to keep saying the same thing,
and a test whose whole job was to assert that two of them agreed.

It is declared now, in `ExecMamba`, beside the strategy it reads. A
`Derived(fn=...)` is a pure function of the published configuration, so
`publish` computes it once and stores it as an ordinary bag leaf: readers get a
plain attribute load, which is what a read inside compiled model code needs.
The function is handed the whole resolved config rather than the bag it lands
in, because a derivation is free to span namespaces and this one does -- it
reads `memory.disable_radix_cache` alongside its own strategy, which is why it
could never have been a method on either bag.

The helper stays: resolution needs the predicate before there is a bag to read.
The other two spellings and their thirty call sites go.

Four test files built runtime objects that read a parallel quotient without
stating a topology. They used to get one for free -- an uninitialized process
group answered with defaults, which is the fallback the previous commit
removed. They publish a config now, which is what a real process does. Fixing
`test_multi_ended_allocator` at the source also cleared two failures it already
had.
Same shape as the mamba one, three more times. `is_ep_joiner`,
`is_ep_scale_joiner` and `is_startup_weight_load_overlap` each existed as a
`ServerArgs` property for the resolution pipeline and, for the first two, again
as a `runtime_context` function for readers after publish.

They are declared where their leaves are -- the elastic-EP pair in `ExecMoe`
beside `ep_join_mode`, the overlap flag in `Model` beside
`startup_weight_load_mode` -- and computed into their bags at publish. Their
twenty-eight read sites become bag reads, and one parameter the conversion
emptied goes with them.

Three readers keep the pre-publish helper instead, because they run before
their process publishes: `initialize_dp_attention`, which the weight-cache
daemon calls while building its groups thirty lines before its `publish`, and
`PortArgs.init_new`, a factory that is handed the record and already reads
eighteen other fields off it.

The startup-schedule test stated the mode by standing in a namespace with the
predicate on it. That is inert once the predicate is a bag leaf, so it states
the mode by publishing a record with it -- which is also what the surrounding
tests were already asked to do.

While counting the read sites: eighteen of them read the predicate without
calling it. That is correct -- they are properties -- but a census that assumes
otherwise reports eighteen always-true conditions, so it is worth saying they
were checked and are fine.
Rebasing onto sixty-four commits of `main` brought in tests written against the
older reading. Eight files built runtime objects that now read the bags, and
did it by hanging values off a mock: `scheduler.server_args.skip_tokenizer_init`,
a `SimpleNamespace(cuda_graph_config=...)`, namespaces carrying a
disaggregation flag, a dLLM algorithm, an external-linker backend. Those are
inert once the value is a bag leaf.

They publish a record carrying what they state, and where one test wants a
different value it says so with `override_server_args` rather than assigning
through the stand-in. Two are pytest functions rather than cases, so they get
`setup_function` instead of `setUp`.

Two needed more than a bare record. The graph-capacity test wants specific
cuda-graph bounds, and a dummy model path returns from resolution before those
are parsed, so it builds the `CudaGraphConfig` itself; the decode-radix tests
want the radix flag on, which is not its default.
Four record reads arrived with sixty-four commits of `main`, written in the
older style: three in `serving_chat`, which already reads ten fields out of the
bags, and one in the UMBP linker. Leaving them makes the same file answer the
same kind of question two ways, which is the state this series exists to
remove. Both run well after their process publishes.

Their tests state the values with `override_server_args` rather than assigning
through the mock manager, for the same reason as the rest.
The re-export block added for the names the moved declarations used duplicated
twelve imports the file already had -- those names were imported *for* the
declarations in the first place. Harmless at runtime and invisible to the
repo's lint config, but it is twelve names imported twice, and the second copy
is the one a reader has to reconcile.

All twelve keep only the annotated re-export. The plain imports go: after the
split there is no code in this file that uses them, which the lint confirms by
removing them the moment the `# noqa` is not on the line.
`_derived_width` answered from a stamp or, failing that, a live process group.
The group read could never disagree with the stamp: `initialize_model_parallel`
stamps all six as its last statement, unconditionally; an elastic scale-up
restamps `attn_dp_size` through `update_dp_attention_post_scale` -- the comment
claiming it does *not* was wrong; no hardware backend builds groups of its own;
and `multimodal_gen`, which has its own `initialize_model_parallel`, stamps in
`_sync_srt_tp_group` -- the moment it lends its TP group to `srt` as `_ATTN_TP`,
which is what makes shared `srt` layers work inside it.

That last one was wrong when this commit was first written: the claim was that
`multimodal_gen` never reads a quotient. It does, through code it does not own
-- `srt/layers/attention/vision.py` builds a `VisionAttention` and asks for
`attn_tp_size` -- so a census of `multimodal_gen/` for `get_parallel()` finds
nothing and the read happens anyway. It surfaced as a hard failure in
`multimodal-gen-component-accuracy`, and the stamp above is the fix: this
package publishes no `srt` config, so the widths have to be stated where the
group is.

So the group read goes, and with it the last reason for a quotient to be
resolved on every read. Every input to `derive_parallel_widths` is a record
field -- `dcp_enabled` is `decode_context_parallel_size > 1`, not a fact about a
built group -- which is the same test the config-derived predicates in this
branch pass. The six are declared the same way, `Derived(fn=...)`, and computed
the same way: once, at publish, into ordinary bag leaves.

What remains is override, else stamp, else the published leaf. The stamp stays
above the leaf because a scale-up restamps `attn_dp_size` after publish; the
override stays on top because that is how a test names a width.

Overriding a leaf no longer moves its quotient: `override(tp_size=2)` leaves
`attn_tp_size` where the published config put it, because nothing is recomputed
on read. A test states a topology by publishing a config, which is what a real
process does, or by naming the width it wants. Six tests say it that way now,
and one pins the new rule directly.

Four tests pinned the group read, with mocks that built "a group exists but
nothing stamped it" -- a state no path produces. Two of them tested a width and
a rank the same way, because both used to read the group; they do not.
`attn_dcp_rank` still reads the group, gated on a width that no longer does, and
`SIZE_RANK_DELEGATIONS` splits along that line. The replacement patches the
group getter to raise, so "do not consult the group" is pinned rather than
merely unpinned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`.claude/rules/modify-component-must-read.md` points at this skill before anyone
touches these files, so a stale sentence here is a wrong instruction, not a
stale note. Four of its load-bearing statements stopped being true across this
series:

- **"every `ServerArgs` field carries `NS(...)`"** -- none do. A field's
  namespace is the `arg_groups/fields/` class that declares it, through that
  class's `_NS_PATH`; the marker survives only for an ad-hoc dataclass spanning
  namespaces, which is what the config-bag tests build. The same sentence was
  the docstring of `test_server_args_namespaces.py`, which is fixed here too.

- **the DCP degrade rule** -- "before dist init a live size read raises, except
  the DCP pair, which degrades". The six parallel quotients are not live reads
  any more: they are a function of the configured leaves, computed at publish
  into bag leaves, answered override -> stamp -> published leaf. `dcp_enabled`
  now means "the launch configured DCP" (`dcp_size > 1`), not "a group is
  installed here". The two agree wherever `initialize_model_parallel` has
  stamped, and differ in a process that publishes without dist init, which is
  worth knowing before writing a test.
  `test_attn_dcp_defaults_when_group_is_uninitialized`, which the skill named as
  the pin, was replaced by config-shaped tests.

- **the accessor shape to copy** -- `mamba_extra_buffer_enabled()`,
  `mamba_extra_buffer_lazy_enabled()`, `is_ep_joiner()`, `is_ep_scale_joiner()`
  are all gone. The shape to copy now is a `Derived(fn=...)` declared beside the
  leaves it is computed from, read as an ordinary bag leaf
  (`get_model().is_startup_weight_load_overlap`).

- **the namespace-coverage ratchet** described in terms of the marker.

Also states the consequence a test author actually trips over: overriding a leaf
no longer moves its quotient, so a topology is stated by publishing a config or
by naming the width.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ch-wan

ch-wan commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

The series is merged: #38046, #38047, #38048, #38049, #38113. This branch was only ever the one-diff view of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apple-silicon bypass-fastfail deepseek diffusion SGLang Diffusion documentation Improvements or additions to documentation hicache Hierarchical Caching for SGLang high priority lora Multi-modal multi-modal language model quant LLM Quantization run-ci-extra

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant