Repository navigation
Conversation
Add default-off cache coordination, helper-side weighted reduction and CPU activation quantization on demand. Allow helper cache capacities to be sized automatically at startup. Port the saved v2.2 implementation onto upstream 0.1.38 without changing the separate peer tier. Document the helper-only scope and current validation limits.
Dual-4090 IQ3_S results and a helper-ownership dispatch fixI tested PR head Summary: I could not reproduce the optimized helper outperforming layer split on our workload. However, I found a dispatch-order interaction worth reviewing: experts already resident on the helper can be assigned to the primary mapped-RAM PCIe path before the helper can claim them. A small local change improved median helper decode throughput by approximately 20–54% in the 256-output context sweep and 79.8% on a separate 2048-output code-maintenance workload. These are workload-specific results, not a general 80% speedup or a claim that helper mode should replace layer split. Setup and controls2× RTX 4090 24 GiB, no NVLink, PHB topology; Ryzen 9 7950X3D, 16-vCPU VM, approximately 161 GiB RAM. Model: Qwen3.8-Flash-Next IQ3_S ( Common inference settings: MTP Original helper, PR optimized helper, and the locally patched helper started with the same 8586 primary / 11796 helper physical slots, identical initial resident expert IDs, and zero initial overlap. For this pack, numeric For the steady comparison below, each configuration used a fresh server, the same 4096-token warmup input with exactly 64 generated tokens, then three measured requests with the same 31,400-token input and exactly 2048 output tokens. Equal warmup length does not imply identical generated tokens or identical adaptive-cache state across algorithms. Boundary cache snapshots were disabled in all headline speed runs. Dispatch issue and local changeIn In The local change checks current optimized-helper residency, excludes those experts from the primary PCIe miss count, and leaves their entries unclaimed until the existing helper dispatch. Remaining misses retain the existing PCIe quota logic. It does not change model weights, kernels, cache capacities, or original-helper/layer-split ownership decisions. The functional diff is included below. Steady decode: 31,400 input / 2048 outputThree measured runs per configuration; TG is decode tokens/s after warmup, with MTP enabled and suffix lookup disabled.
On this workload, the submitted optimization did not improve median TG over the original helper (75.3 vs 76.0). The local change improved median TG by 79.8% relative to the PR helper, but remained 14.7% below layer split. The large accompanying reduction in sampled GPU0 PCIe RX supports the dispatch explanation; these are mapped-host-memory reads, not evidence of SSD streaming. Important qualifications:
Context sweep: 256 output tokensEach context point used an equal-length 64-output warmup and three measured runs, with matching input token IDs between configurations and zero reuse. Each configuration used one server across the context sweep, retaining adaptation history; these were not independent fresh-server replicates for every cell.
The patched helper's +4.95% and +2.97% median TG differences relative to layer split at 127K and 259.5K are small observed advantages, not established general wins. At 127K, all three matched comparisons favor the patch (LS 118.3–119.7 vs patched helper 123.5–126.2 tok/s); at 259.5K, the three paired comparisons are mixed. Neither cell establishes statistical equivalence, and neither has independent fresh-server replication or a long-output confirmation at that context size. Prefill remains about 1.9–2.5× slower with the patched helper across this sweep. At 259,500 input tokens, the approximately 68-second TTFT penalty outweighs the small decode-only advantage for the tested 256-token responses. We are therefore keeping layer split auto for our zero-reuse agent workload. I have not used the earlier 32K screening result of 95.7 → 119.2 tok/s as a controlled fix-speedup headline: its PR-helper and patched-helper warmups generated 227 and 64 tokens respectively. The final sweep and steady comparison above use equal-length warmups. Cache behavior and validation limitsSeparate cache diagnostics support the complementary-cache mechanism: the PR optimized helper retained zero primary/helper overlap at startup, after warmup, and after run 3. The original helper reached 2073 overlapping experts, or 17.57% of helper capacity. Those diagnostics were separate from the timed runs. The ten-prompt greedy smoke tests for the PR and local fix showed no observed obvious corruption or NaN/Inf symptoms; the JSON cases remained valid. This is not numerical-parity validation: no comparable teacher-forced KL/top-1 analysis was obtained. CTest recorded 56 passes, 2 skips, and 4 environment-related failures per build (missing fixtures and the mlock limit), not an all-green suite. No OOM or crash occurred in the campaign. Three early-ending measured requests were retained as invalid (63 output tokens, Functional diffThese are the functional changes from the tested local patch. Benchmark-only header context has been removed; no diagnostic instrumentation is included in this diff. Helper-residency priority patch for PR head b28121fdiff --git a/include/strata/core/remote_experts.hpp b/include/strata/core/remote_experts.hpp
--- a/include/strata/core/remote_experts.hpp
+++ b/include/strata/core/remote_experts.hpp
@@ -38,6 +38,7 @@ public:
std::string& err);
bool owns(int64_t index) const { return owned_[(size_t) index] != 0; }
bool optimized_decode() const { return remote_opt_ != nullptr; }
+ bool has_expert(int64_t layer, int32_t expert) const { return cache_.slot_of(layer, expert) >= 0; }
bool finish(float* out, std::string& err);
int64_t resident() const { return cache_.resident(); }
int64_t computed() const { return computed_; }
diff --git a/src/core/expert_source.cpp b/src/core/expert_source.cpp
--- a/src/core/expert_source.cpp
+++ b/src/core/expert_source.cpp
@@ -1888,6 +1888,13 @@ void expert_pool_dispatch_multi(ExpertDispatch& d, const float* x_f, const int32
if (ids[i] >= 0 && ids[i] < d.n_expert) d.usage[(size_t) d.layers * (size_t) d.n_expert + (size_t) ids[i]] += 1.0f;
// ---- plan v0.3 P6: the GPU's share, decided and published FIRST so the GPU starts while the CPU works.
// Distinct experts in routing order; resident ones and the last pcie_num/256 of the missed ones go to the GPU.
+ // Optimized helper residents are already on a GPU: do not classify them as
+ // primary PCIe misses. Keep them unclaimed until RemoteExperts::begin below.
+ const auto optimized_helper_has = [&](int32_t e) {
+ for (int r = 0; r < d.remote_count; ++r)
+ if (d.remote[r]->optimized_decode() && d.remote[r]->has_expert(d.layers, e)) return true;
+ return false;
+ };
const int64_t n = n_tok * k;
int32_t kind[kMaxWindowEntries]; // per entry: -1 CPU, 0 VRAM, 1 PCIe
if (d.plan != nullptr && n <= kMaxWindowEntries && n <= d.plan->cap) {
@@ -1901,7 +1908,7 @@ void expert_pool_dispatch_multi(ExpertDispatch& d, const float* x_f, const int32
distinct[nd++] = i;
const int32_t e = ids[i];
if (e >= 0 && e < d.n_expert && d.host_res[(size_t) d.layers * (size_t) d.n_expert + (size_t) e] < 0 &&
- !(d.peer != nullptr && d.peer->has(d.layers, e))) ++nmiss;
+ !(d.peer != nullptr && d.peer->has(d.layers, e)) && !optimized_helper_has(e)) ++nmiss;
}
}
const bool pcie_ok = d.pcie_num > 0 && d.src->pcie_layer(d.layers);
@@ -1923,6 +1930,8 @@ void expert_pool_dispatch_multi(ExpertDispatch& d, const float* x_f, const int32
: (size_t) slot * (size_t) d.cache_blob));
} else if (d.peer != nullptr && d.peer->has(d.layers, e)) {
kd = 2; // multi-GPU: the second GPU computes it
+ } else if (optimized_helper_has(e)) {
+ kd = -1; // helper claims this row below; not a PCIe candidate
} else {
if (miss_rank >= nmiss - m && fetches < P.staging_cap && fetches < 64) {
const uint8_t* src = d.src->pinned(d.layers, e) ? d.src->blob(d.layers, e) : nullptr;Does your benchmark exercise the primary mapped-RAM PCIe quota with experts already resident on the helper? Comparing that routing path, the exact command line, and a small reproducible payload would help determine why the submitted helper optimization behaves differently here. The proposed ownership change is the main finding I would like reviewed, independently of whether helper mode ultimately outperforms layer split. |
Added where a config goes to several GPUs (setup --gpus / a multi-GPU install, start --gpus, "use both" at start). Single-GPU configs are not touched. Opt-out: setup --no-remote-expert-opt, or "remote_expert_opt": false in the config (a user key, kept on re-runs). The engine uses it only with a helper cache (--expert-cache-device1..3), so a layer split runs as before. tools/test_setup_remote_opt.py; the 2x16 GB golden configs gain the flag. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks! Taken into 0.1.39. The engine flag stays default-off, and setup now adds --remote-expert-opt to a config on two or more GPUs. To leave it out, pass setup --no-remote-expert-opt or set "remote_expert_opt": false in the config. It acts only with a helper cache (--expert-cache-device1..3), so a layer split runs as before. We have one GPU here: we checked the build, the refusal without --serve and byte-identical default output. Merged with #646, which publishes the routing weights only when a helper needs them, so with the flag the doorbell still sends them. The dual-GPU numbers are yours (dual RTX 4090 +63% mixed / +132% code over the plain helper path). |
217 upstream commits (0.1.38 -> 0.1.39: Niko1221#646 decode kernels and zero-doorbell verify graph, Niko1221#603 long-context top-k, Niko1221#583 byte-budget ring, Niko1221#598 pipelined layer split, Niko1221#578 helper GPUs, Niko1221#663, Niko1221#540, Niko1221#600 in the experimental build, batch slots Niko1221#465, the SYCL port, older-CPU builds, ...). 23 files conflicted (~70 hunks); every hunk was resolved by hand and checked against both sides. Resolution rules: - The prompt path's stage overlap: upstream's Niko1221#598 (equivalent to our e85f7ee; run() drains the whole chain on every exit), with our release_inherited_pin in the stage threads and our early ids copy for the host grouping (a5a3254) kept. - STRATA_FP16=load's form-aware weights kept wherever upstream's new code assumed BF16: the shared-expert gate (sigmoid+scale fused as upstream), GDN alpha/beta (our kernels; upstream's new gdn_step_commit_kernel added), the hc v3 kernels (upstream's EXACT_T with our dot8w). - verify.cpp: upstream's all-resident fast path, plan copy and Niko1221#578 rows, with our 2-GPU fetch overlap and partner fetch back in the non-resident branch; mapped buffers stay portable (the partner reads them). - Volta prompt attention: upstream's dispatch (the m8n8k4 kernel on sm_70 in this build) with our split hi/lo accumulator chains inside it; the wmma kernel stays opt-in (STRATA_ATTN_WMMA=1 without STRATA_ATTN_M884). - qsa_decode_attn.cu: upstream's (our adf56cc imported the same PR Niko1221#540; upstream's is it plus review fixes, bit-exact). - POWER / aarch64: upstream's new x86 intrinsics in verify.cpp through the portable strata_store_fence / strata_cpu_pause; its AVX2 start-up checks and log line behind x86 guards; stubs for the new CPU probes; the router lookahead keeps portable.cpp's VSX dot on non-x86 (upstream's dispatch would have sent POWER to its scalar fallback); the pool's ThreadAffinity (any CPU count) with our note_host_pin; SMT2 selection with Niko1221#642's sort. - setup.py / tests: both sides (CUDA 12 engine, ISA floor, ARM / non-x86). Checked: - every line either side added since 0.1.38 is in the merge or replaced on purpose (script over all 45 files both sides changed); - all parity tests pass (iq, native_expert on the real shards, mmvq_multi, gr, gdn, gdn_rec, qsa, qsa_topk, elementwise, router_dot, shared_expert, kv_*, sampler, rope, s2/s_gemv, gemm_bf16, ...); - teacher-forced 2K and 32K logs byte-identical to the pre-merge build (4 GPUs, launch config); - 4 GPUs: decode text identical, same windows and acceptance, 28.25 -> 26.9 ms/window (~86 -> ~90 tok/s); prompts 7.8K 3,992 -> 4,038, 65K 7,102 -> 7,172, 123K 7,323 -> 7,334 tok/s; --prefill-ring 320 applies; - 2 GPUs: decode texts identical to the pre-merge binary on three prompts, decode 61.5-71.0 -> 63.5-72.7 tok/s, 65K prompt 3,975 -> 4,083. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZHWKWn7UYZV64KwpGrWuN
…k's carries Hardin22's two-GPU work (pipelined verify windows, fewer host waits, the asynchronous expert tier, the dense-path launches, resident RAM mode on a split, the later cards' VRAM reserve) on upstream 0.1.39. Where both did the same thing, upstream's is kept: the shared expert on its own stream (cfd3b72), the fused SwiGLU quantization, the row zeroing on the GPU, the per-stage dense weights' loader filter (Niko1221#639), the P-core order (Niko1221#642). Hardin22's late PLE rows serve every stage (upstream's only the all-resident graph); its split host steps (prepare / launch / serve / finish) carry upstream's trace (Niko1221#649) and Niko1221#578's helper hooks. Batch windows (Niko1221#465 / Niko1221#559) keep upstream's paths: no per-row hand-off, side branches or batched PLE inside them. 2c1365e (updates and engines from Hardin22's repository) is left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1.39's --remote-expert-opt The golden two-GPU configs (Hardin22's: both cards, resident mode kept) now carry Niko1221#578's --remote-expert-opt, which 0.1.39 adds on two or more GPUs; test_setup_risk's --yes case likewise. The two Unsloth UD-Q4_K_XL entries stay as upstream records them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Added where a config goes to several GPUs (setup --gpus / a multi-GPU install, start --gpus, "use both" at start). Single-GPU configs are not touched. Opt-out: setup --no-remote-expert-opt, or "remote_expert_opt": false in the config (a user key, kept on re-runs). The engine uses it only with a helper cache (--expert-cache-device1..3), so a layer split runs as before. tools/test_setup_remote_opt.py; the 2x16 GB golden configs gain the flag.
Added where a config goes to several GPUs (setup --gpus / a multi-GPU install, start --gpus, "use both" at start). Single-GPU configs are not touched. Opt-out: setup --no-remote-expert-opt, or "remote_expert_opt": false in the config (a user key, kept on re-runs). The engine uses it only with a helper cache (--expert-cache-device1..3), so a layer split runs as before. tools/test_setup_remote_opt.py; the 2x16 GB golden configs gain the flag.
Added where a config goes to several GPUs (setup --gpus / a multi-GPU install, start --gpus, "use both" at start). Single-GPU configs are not touched. Opt-out: setup --no-remote-expert-opt, or "remote_expert_opt": false in the config (a user key, kept on re-runs). The engine uses it only with a helper cache (--expert-cache-device1..3), so a layer split runs as before. tools/test_setup_remote_opt.py; the 2x16 GB golden configs gain the flag. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
This PR improves multi-GPU decode performance by making the primary GPU and
expert-helper GPUs cooperate more efficiently.
The optimized v2.2 expert-helper path delivers substantial decode speedups
over both the previous expert-helper path and the automatic layer-split path.
Speedup summary
Benchmarks
RTX 5090 + RTX 4090 — 0.1.38
Decode throughput, in tokens/s:
Dual RTX 4090 — 0.1.37
Decode throughput, in tokens/s:
What changed
Complementary expert caches
Keep the primary and helper GPU expert caches complementary, reducing
duplicated experts and CPU fallback.
Lower inter-GPU transfer volume
Reduce weighted expert outputs on helper GPUs before returning results.
Skip unnecessary CPU quantization
Avoid CPU activation quantization when all selected experts are served by GPUs.
Automatic helper-cache sizing
Add
autosizing based on available VRAM, removing the need to manually tuneexpert counts.
The existing primary/helper GPU layout remains unchanged.