Repository navigation
Context / Sequence Parallel 4/5: split long prefills over position-stored KV (all-gather and ring) - #1141
Open
dghosef wants to merge 44 commits into
Open
Context / Sequence Parallel 4/5: split long prefills over position-stored KV (all-gather and ring)#1141dghosef wants to merge 44 commits into
dghosef wants to merge 44 commits into
Conversation
…ing to_partners One public face over the existing all_to_all_single: x's rows go to each group rank in counts the caller gives per rank, and the rows each rank sent come back. TP placement's partner exchange is the case of one row per partner, so to_partners goes; the ring over a pool stored by position (next) is the case of one peer per hop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ring, sp_exchange A node can now split its steps' tokens over SP ranks above a paged pool stored by position (context_parallel), the refusal of that pairing gone: - all_gather (the default over position storage): one gather of every rank's q, k and v, Part A's copied path unchanged, this rank's share of the output. - ring (sp_exchange: ring): the shard stripes each split step by KV position (a row with c stored tokens gives rank r its tokens at positions x with x % P == r), so each rank writes exactly the K/V it stores (write_kv(tokens=)), then P synchronous rounds pass queries round the partners: one causal paged call of partner j's queries over this rank's slots, the partial back, an in-place fp32 merge (merge_into). A row's first query that sees no key on a rank plans as a one-query row over a sink slot, masked out of the merge, so no causal row holds more queries than keys. Eager only: a striped shard plans a leased step copied. RingAttentionWrapper carries the plan-state interface (global qo_indptr for select_last_hidden, reads_step_kv) and its own eager key; bind warms one hop per offset. The striped shard depends on the pool it stripes by (an edge of the shard, which keeps the pool's prefix cache open) and supports a placed pool, so the runner plans the pool first. - sp_exchange on AttentionSpec, set only by a deployment's resources block: none, ulysses, all_gather or ring; unset derives it from storage, so existing configs are unchanged. A mismatch with the pool's storage is refused at load at any SP size. SeqShardSpec derives splits (any attention not none) and stripe_pool from raw fields, so an all-none node is a shard-less node to the engine and the refusals (DCP alone); a none attention beside splitting ones refuses a split label at plan. Ragged sp_exchange takes the same strings, "none" by default. SeqRange gains local/whole over contiguous or striped layouts (start deleted). Refused at load: an axis-1 shard over a placed pool, a ragged Ulysses beside a ring, ring attentions on two pools; refused at the call: a K/V write without attention of a split label over a placed pool; a placed run outside the exchange now raises too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k sp_exchange The matrix's split-over-positions cell (all-gather, ring), the sp_exchange choice with a config-keys-to-what-runs table, the memory rule (peak = residents + max(attention, MLP or MoE); ring lowers it past a per-model crossover), ring as eager only, and chunked prefill still off on SP ranks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Thinker declares a sequence shard over its one label, declares it in every step, and brackets its layer stack: the MoE padding mask is computed over the whole step and layer-0 embeds are cloned whole, then the embeds, MRoPE tables, deepstack and mask are scattered, and the final hidden states and the Talker's layer-N states gathered after the final norm. configs/qwen3omni_thinker_tp4_sp2.yaml runs it on TP4 x SP2 with the KV pool stored by position (all_gather, the storage's default exchange) and a placeholder min_tokens. The toy-Thinker test runs the real layers over the model's own declarations on two gloo ranks, split against copied, eager and compiled, and checks a split prefill samples each row's last token. Its by-position cases (all_gather, ring) are skipped until the engine's exchanges over position storage land. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xchanges exist Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…opied A pool stored by position (context_parallel) on a node with a sequence shard used to take all_gather when sp_exchange was unset, so a config that ran every step copied (DCP) before this PR started splitting prefills, and was refused beside expert parallelism. Unset now takes the storage's default: ulysses by head, none by position; splitting over position storage is the deployment's opt-in (all_gather or ring). The Thinker's TP4 x SP2 config opts in. The exchange is resolved once, on the spec (AttentionSpec.split_exchange, from resolve_exchange, which the shard's derive also asks), so the manager keeps one field rather than the configured and resolved pair; sp_exchange is a Literal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eps beside ones that do sp_exchange none beside a split-taking attention passed load and then failed every split step, and only in FlashInfer's plan; the dense backend ignored none and ran Ulysses. A paged pool serves whatever labels a step declares, so a split label can reach such an attention whenever the node splits. AttentionSpec.split_step_refusals now refuses it at load, which runs only where the node can split (decision 1). The plan-time raise (_split_range) and its test go; no backend can then see a split label with none, dense included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…where its node would be sequence_parallel_refusals refused a shard on a node with no attention that takes split steps before it looked at whether the node places its pools, though such a node runs copied like one without a shard. A model's declaration that never splits is a no-op (decision 1), so the separate rule goes: a non-splitting shard takes the no-shard path, which accepts a placed node and refuses an unplaced one (its SP ranks would only share heads), with one message for both kinds of non-splitting node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sp_exchange set on a node with no sequence shard, or at one SP rank, was accepted and did nothing. It is a deployment's setting, and a deployment value that can't take effect is refused (decision 1; a model's declarations that never split stay no-ops). AttentionSpec.derive refuses it without a shard, at any SP size; sequence_parallel_refusals refuses it at one SP rank. The ring engine test moves to SP placement, where the setting means something. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The axis-1 refusal over a pool stored by position needs the node to split, which unset no longer does there (R1.F5): the row opts in with all_gather. The storage-mismatch row moves to two SP ranks, as at one any sp_exchange is now refused as well (R1 cr#7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RingAttentionWrapper planned every partner's queries causal=True, and FlashInferManager's ring branch dropped step.causal, so a non-causal split step over position storage under sp_exchange ring (Cosmos3's DiT steps, for one) got a causal mask: wrong output, no error. The step's causal now reaches the plan. Not causal, each partner's queries in a row see the row's whole length on this rank (no first-query sink split, which only causal alignment needs), and a row with no slot here is masked as before. Passing it through costs no new concept, where a refusal would add a rule and a docs row. The per-partner layout moves into _ring_rows, which the next commit's check reuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ring plans one wrapper per partner whose rows differ by rank (a partner's first query in a row is planned apart only where this rank holds no key before it), and passed no check, so a planner refusal on one SP rank would leave its partner waiting in the first hop. Like ContextParallelWrapper, RingAttentionWrapper now takes the backend's check and applies it, before planning anything, to every rank's plan of every partner (_ring_rows), so a refusal is every rank's. No backend that can split sets a check today; this makes the ring safe for one that does, as the design described. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ring read each partner's packed partial back with q.dtype, but the bytes are in the kernel's output dtype (FlashInferPrefillWrapper.run casts q to the attention's). Queries in another dtype would have their out bytes viewed at the wrong width. It now uses the local partial's dtype, as exchange_partials and gather_merge do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under MSTAR_KV_PINNED_INDPTRS ContextParallelWrapper copies its trimmed paged indices into a page-locked buffer so FlashInfer's plan copies them asynchronously; the ring planned its P partner plans from pageable indices, a synchronous copy each. The buffer and its reuse event move out of ContextParallelWrapper into _PinnedIndices, which the ring holds one of per partner. Perf only, on an eager opt-in path; no CPU test can pin memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erved, not refused AttentionCallable.write on a split label over a pool stored by position raised, though both exchanges can serve it: all_gather gathers k and v alone and runs the write as a copied step (the placement sinks the slots this rank doesn't store), as ulysses does with q None; the ring writes this rank's own tokens, which are exactly the ones it stores. The refusal goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g's KV typed _warm_ring and RingAttentionWrapper.attend each built the per-offset send/receive counts, so a change to one could warm different NCCL pairs than the ring uses. Both take them from ring_hop. AttentionManager._kv is typed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ring merges each round's partial into one fp32 accumulator as it arrives; _merged takes every partial at once and allocates its output. The docstring now says so, so the two copies of the log-sum-exp merge are a stated choice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… dead branch The tests' placed KV fake copied KVManager.write_kv's tokens= addressing, so the real branch never ran. The fake now holds what write_kv reads (a plan state addressing each step token as a placed pool's does, the cache's own scatter) and takes write_kv from KVManager. The Thinker prefix-cache test appended a SeqShardSpec if the model declared none, which it now always does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y what it can't stripe The crossover where ring lowers peak memory (about P = 4 for the Thinker, sooner for dense models) is arithmetic from allocation sites, unmeasured; the docs and the Thinker config now say so rather than stating it. The ring row also says a step combining labels (batched CFG) fails at plan, since it has no stored positions to stripe by; no load-time declaration says a model may combine labels, so this stays a documented symmetric plan-time refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… opt-in Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 2, 2026
…-pcp Conflicts: docs/adding_models.rst (SP mode: this branch's split-step text plus the parent's "chunked prefill over a placed pool is not supported yet"), docs/models.rst (both Qwen3-Omni notes), and test_qwen3omni_vision_mixed.py (both sides' tests). Under this branch a node storing KV by position may split its steps, so the head-split refusal (engine and validate_deployment) now names the other side "keeps KV by head", and the LoRA docs and comment say LoRA is refused wherever the steps split (SeqShardSpec.splits), placed pool or not. A validate_deployment case pins it: placed + all_gather/ring refused, placed + sp_exchange unset served. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only FlashInferMLAManager sets _plan_check, and MLA can't take split steps, so the check RingAttentionWrapper took (f0c773f) was always None on every ring that runs, and its test monkeypatched it onto a FlashInfer manager. The docstring keeps the reason a future backend with a plan check would need one: partner plans differ by rank. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ring's call follows the step's causality since 7d816dd, and an unset sp_exchange over KV stored by head resolves ulysses and splits without a pick; only position storage needs the opt-in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ring was the one exchange that ignored the `run` closure: it reached back into the KV resource through a new attention-to-KV reference (`AttentionManager._kv`) and wrote and attended in `_ring`, calling the plan state with a signature of its own. It is now `sequence_shard.ring(run, q, k, v, r)` beside `ulysses` / `all_gather` / `head_slice`: it calls `run(q, k, v, tokens=r.index)`, `AttentionCallable._attend` forwards `tokens` to `write_kv`, and `attn.run` -> `_attend_placed` drives `RingAttentionWrapper.attend(queries, kv_cache_layer, new_keys)`, the same signature as `ContextParallelWrapper.attend`. Drops `_kv` and `_ring`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(abstractions-4 #4) `RingAttentionWrapper.reads_step_kv = True` was set only so Qwen3Attention's fused norm/RoPE/KV-write path would fall back to `attend(q, k, v)` and the write would go through the exchange. The ring attends only the pages, so it is now False, and the fused path decides on what it actually depends on: `attn.splits` (the call goes through an exchange) or `reads_step_kv()`. This also covers Ulysses / head-slice nodes, whose fused write would have bypassed the exchange (a split label raised in `write_target`, a copied label wrote every head into a pool storing heads over TP x SP). `RingAttentionWrapper.plan` no longer recomputes `stripe_owners` over the KV plan to check the counts the sequence shard derived from the same plan; it checks each partner's planned queries against them instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) `SeqShardSpec.derive` filtered its node's specs by `attends_split_steps`, then type-switched to ask a paged attention `resolve_exchange(specs)` and a ragged one `.sp_exchange`: a hook in disguise. `resolve_exchange(specs)` is now the one `NodeResourceSpec` hook (default "none"; the paged spec resolves its `sp_exchange` against its pool's storage, the ragged self-attention returns its declared one), and the derive asks every spec over its nodes. Deletes `attends_split_steps`. Also deletes the base `splits_steps` hook, which only `SeqShardSpec` answered (with its `splits` field) and only the engine still read; the engine now reads `isinstance(spec, SeqShardSpec) and spec.splits`, as the rest of the shard code does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed where it acts (abstractions-4 #5) - `merge_into` re-implemented `_merged`'s math for two partials. The ring now merges each round's partial into its running result with `_merged`, as `ContextParallel` already does for its two parts; `merge_into` is deleted. - "sp_exchange set where the node's steps never split" was refused in two places (`AttentionSpec.derive` without a shard, `sequence_parallel_refusals` at one SP rank). `sequence_parallel_refusals` now refuses both cases. - Connecting every pair of a group once is the communicator's job: `CommGroup.connect_pairs` replaces `AttentionManager._warm_ring`. - `_ring_wrapper` moves from the `AttentionManager` base to `FlashInferManager`, the one backend that takes both split steps and a placed pool and the only one whose plan builds a ring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ons-4 #2) `RingAttentionWrapper.attend` now takes `queries` as a tuple (the shared plan-state signature), and the loop rebound that name to each round's partner queries; it is `theirs` now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…label (cleanup4-review P3-1) A pool stored by head holds this rank's tp * sp slice of the heads, so a layer that writes K/V itself (write_target) or calls attn.run on a copied step passes the TP heads where the pool holds fewer, silently. check_bypass now refuses any call outside an exchange over such a pool (by_head, from the caller's placement), so the next fused-write layer that forgets to check `splits` fails at its first step instead of writing the wrong heads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng (cleanup4-review P3-2) Only FlashInferManager builds a RingAttentionWrapper, but exchange sends any placed ring node to the ring. A backend that later gains split steps (MLA) would plan a ContextParallelWrapper and be handed this rank's striped share of q, a plan/run mismatch deep in the kernel. The placed-pool refusals at build now include ring on any backend but flashinfer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…timate (cleanup4-review P3-3) Each ring round merges into a new fp32 result while the previous one is alive. Per the review, the code stays (the merge math lives in one place, `_merged`); the term is now in the doc's unmeasured peak-memory estimate so whoever measures the ring/all-gather crossover counts it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt (cleanup4-review P3-4) RingAttentionWrapper names `_merged` (what it calls) and types its queries as ContextParallelWrapper.attend does; the tokens comment in AttentionCallable says "a stand-in KV" without the tests' parenthetical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eview-2 P3-1) A KV pool keeps one sequence shard, which its writes' check_bypass consults whichever node's step is running. Two SeqShardSpecs over nodes that share a by-head pool left the pool with the last one bound, so a write inside the other shard's exchange raised as if it were outside one. bind_sequence_shard now raises when a different shard is already bound, decided from config at load so every rank refuses alike. Rebinding the same shard (several attentions over one pool, as in Cosmos3) is unchanged. The docstring now says any write outside an exchange raises, and the SP docs name the refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cleanup4-review-2 P3-2) Since fac250f check_bypass refuses any call outside an exchange over a pool stored by head, not only a split label. The KVManager._sequence_shard comment and the exchange comment in AttentionManager now say "outside an exchange" and point to check_bypass, whose docstring carries both cases. The third comment, bind_sequence_shard's docstring, was updated with the P3-1 commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n B)
Option B moves the node-level split choice from the paged attention's
per-resource `sp_exchange` to the sequence shard's YAML, beside
`min_tokens`: `exchange: auto | none | ulysses | all_gather | ring`.
- `auto` (the default) derives from storage: Ulysses over KV stored by
head, copied (DCP, never split) over KV stored by position, so existing
configs run as before. `none` never splits. `ulysses` needs KV by head,
`all_gather` and `ring` KV by position; a value the node's storage can't
take is refused at load, at any SP size, with its reason.
- Each paged attention derives its exchange from its pool's storage and
the shard's choice through `resolve_exchange`, which the manager reads at
build from its dependencies. Deleted: `AttentionSpec.sp_exchange`, its
resolved twin `split_exchange`, the YAML plumbing, and the refusals only
that field needed (a set value with no shard or at one SP rank, a paged
attention taking none beside a split, its storage mismatch).
- The ring's striped layout is now chosen by `exchange: ring`.
- Ragged `sp_exchange="ulysses"` stays a model declaration.
- Thinker TP4xSP2 config: `thinker_shard: {min_tokens: 8192, exchange:
all_gather}`. Docs: the how-to-pick table is per shard `exchange`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s over TP (optionB-review P2-1) A ragged attention declaring sp_exchange="ulysses" planned heads/(tp*sp) heads whenever its node had a sequence shard, even one whose exchange (none, or auto over KV stored by position) means the node never splits. The node then runs copied and feeds the attention heads/tp heads, which the wrapper wasn't planned for. The builder now also reads the shard's derived `splits`. Pinned in test_a_shard_that_never_splits_plans_every_step_copied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ld (optionB-review P3-1) `AttentionManager._build_backend` refuses exchange 'ring' on a backend other than FlashInfer over a context_parallel pool at sp=1 too (TP-mode context parallelism), where the shard is a no-op. That matches the shard's own exchange-vs-storage checks, which also run at any SP size, so the rule stays and the docs say so, and that it fires when the engine builds the attention rather than in pre-spawn validation. test_attention_a_placed_pool_cannot_serve_is_refused_at_build already pins it at sp=1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nB-review P3-2) The shard's `exchange` picks how split steps reach the node's paged attentions; a ragged attention keeps the model's `sp_exchange`, and only the shard's `none` (or `auto` over KV stored by position) reaches it, by stopping the node splitting. Said beside the serving.rst example and in the SeqShardSpec docstring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…endencies (optionB-review-2 P0-1) A ragged Ulysses attention now reads its sequence shard's `splits` from `EngineResourceInfo.dependencies`. The two hand builders of LTX-2.5's declaration (test_ltx2_5_sequence_parallel._resources and benchmark/ltx2_5/profile_step.ragged_denoise) shared one info with no dependencies, so building VIDEO_ATTN raised KeyError. Pass each spec its `depends_on` specs, as Engine does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts (optionB-review-2 P2-1) Scope the RaggedAttentionSpec docstring's head-sharding and return_lse sentences, and the adding_models.rst table cell and no-op sentence, to a node whose sequence shard splits, matching _build_ragged since 8d522e6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RaggedAttentionSpec docstring said run(..., return_lse=True) raises on any node whose shard splits. SeqShardSpec.splits doesn't depend on the SP size, and at one SP rank the shard can't split, so the resource attends without an exchange and returns the log-sum-exp. Say it raises on SP ranks of a splitting node, and give the return_lse paragraph in adding_models.rst the same scope. The resource table's RaggedAttentionSpec row now says Ulysses splits the heads over TP x SP only on a node whose shard splits; elsewhere they stay over TP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RaggedAttentionSpec row said the heads split across the node's TP ranks without saying that a Ulysses resource on a splitting node splits them over TP x SP, which the same cell explains a few lines down. Point at that case. The return_lse paragraph ran two cases into one sentence, so the partial merge read as if a Ulysses self-attention did it. Name both cases that can't return a log-sum-exp (every label of a "ulysses" resource, and a cross-attention pair whose keys split), then say the merge belongs to the split-key cross-attention. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dghosef
marked this pull request as ready for review
November 4, 2026 19:27
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context / Sequence Parallel stack: 1/5 #1124 → 2/5 #1125 → 3/5 #1138 → 4/5 #1141 → 5/5 #1147
Stacked on #1138.
What this does
context_parallel). Before, you had to pick one: split work (Ulysses, KV by head) or split memory (DCP).exchangeon its sequence shard (SeqShardSpec, aresources:override besidemin_tokens):auto(the default),ulysses,all_gather,ringornone.autofollows the KV storage: Ulysses over KV stored by head, and over KV stored by position the node runs every step copied (DCP). So existing configs don't change, and splitting over position-stored KV is something you opt into. A value the storage can't take fails at load (table below).sp_exchangefrom Context / Sequence Parallel 3/5: move Ulysses into the engine and pick a strategy per resource #1138, now"none"(the default) or"ulysses". A shard that never splits stops them splitting too, and then their heads stay over TP.configs/qwen3omni_thinker_tp4_sp2.yamlruns it on TP4×SP2 with KV stored by position,thinker_shard: {min_tokens: 8192, exchange: all_gather}, andmin_tokensis a placeholder.How it works
exchangeagainst its node's KV storage when the deployment is derived, and each paged attention picks up the resolved value through itsresolve_exchangewhen it's built. So there is one choice per node, and one attention can't split while another doesn't.x % P == r, which are exactly the slots it stores, so every K/V write is local. Then P rounds pass queries round the group; each round is one paged call over this GPU's slots, causal or not as the step declares, and the partials are merged in fp32. Hops are synchronous for now (overlap is a follow-up), only the FlashInfer backend plans it, and ring runs eager only: a captured step on a ring node runs copied.CommGroup.all_to_all_rowssends rows to each group rank in counts the caller gives. It replaces Context / Sequence Parallel 1/5: split the KV cache by position across GPUs (DCP) #1124'sto_partners, and the ring uses it for its hops.exchangeauto(default)auto(default)ulyssesall_gatherringnoneTesting
The CPU rows ran on 7ecc854. The two commits on top change only docs and a docstring.
ruff check .main(eceea1b)main's too)test/rust/), built from this headexchangevalues the storage can't take refused at load, and LoRA refused where steps splitautoover KV by position,none) plans every step copied, and a ragged Ulysses there plans its heads over TPmin_tokenssweep🤖 Generated with Claude Code