Repository navigation
DeepSeek: a short prompt's least-picked RAM experts on the CPU lane while the GPU streams the rest (MAYA_DS4_PREFILL_CPU, opt-in) - an agent's turn ~40% faster on PCIe 3.0 - #959
Conversation
…hile the GPU streams the rest (MAYA_DS4_PREFILL_CPU, opt-in) After each layer's routing, the CPU-lane experts not copied early are taken fewest rows first for the CPU lane while its estimated time stays under the stream's for the rest; their virtual slots move behind the streamed ones, the chunk is sorted again, a host thread computes them with cpu_experts while the device streams the others, and their outputs are combined after the layer's streamed and VRAM experts. A recursive mutex keeps the lane's scratch to one caller at a time. Off by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I tested 5ea06e1 on a Threadripper PRO 5975WX (32 CPU threads, 512 GiB RAM) with two CMP 170HX 64 GiB GPUs at a configured 150 W each. This uses an existing mixed IQ2_XXS GGUF pack for DeepSeek-V4.1-Flash-1017, TP=1, with CPU-lane experts in RAM. No weight conversion was needed. Both arms use the same PR binary, fixed read-only expert profile, 32K context, parallel=1, and MAYA_DS4_PROMPT_PICK=length:96. The only feature change is CPU prefill off/on. The on arm reports DS4_PREFILL_CPU activity on both GPUs (including nonzero CPU experts and rows); the off arm reports none. Decode's separate streaming calibration reports zero streaming layers in both arms, which does not prevent this prefill path from running. For one sequential off/on pass, native prompt time was 2566.8/1927.1 ms for a fresh 1024-token prompt and 5274.2/5040.6 ms for a fresh 4096-token prompt (-24.9% and -4.4%). The repeated 4096-token prompt reused 4095 tokens and took 35.1/39.1 ms. Decode for the fresh prompts was 2652.9/3382.5 ms and 2665.1/2729.5 ms respectively, with 128 generated tokens each. Token hashes differ between off/on, so these are timing observations, not a claim of identical outputs or improved model quality. This is a single-pass applicability check, not a tuned performance result. Results and exact configuration: results and sanitized configuration. |
|
Thanks for testing it on a very different box! I repeated the decode question here (same machine as in the PR description, the PR's code with
So no decode penalty after the CPU share here, and the share only runs while a prompt chunk is read. One thing that may explain your single pass: the first request after an engine start decodes much slower and varies a lot while the expert cache is still cold - 13.7 / 14.0 / 13.7 / 17.0 tok/s for the first request of our four runs, 20-25 tok/s for the later ones. If your 1,024-token prompt was the first request of each arm, that alone could account for 2.65 vs 3.38 s. A warm-up request first, or a few prompts per arm, should show it. On your box the defaults are probably far off, which would also explain the smaller prompt gain. They were measured here: |
|
Thanks for the follow-up. Each arm did have a 256-token / 32-generated-token warm-up before the fresh 1024-token request, although that may still leave the expert cache under-warmed. I agree that this single pass does not establish a decode penalty. A hardware detail: these cards report PCIe Gen2 x16 (both current and maximum). The same run's Maya load log reports a 6.8 GB/s link on each GPU, with prompt-ring expert slots of 10.1 MB and 11.5 MB. For the fresh 1K prefill, the CPU diagnostics are 69.5 ms / 521 rows and 56.0 ms / 420 rows, about 0.133 ms/row on each GPU; at 4K they are about 0.111 and 0.115 ms/row. Those measurements support trying different planner estimates. Calibrated off/on timings are still unmeasured here; the published numbers remain the default-estimate applicability check. |
|
Tested 5ea06e1 on a third, quite different box: one consumer CPU (AVX-512), three GPUs on a layer split with mixed PCIe links. Only this PR's setting differs between the arms. Box: Ryzen 9 9950X (16C/32T, AVX-512, one socket), 96 GB DDR5-6000 (dual channel), Linux, driver 615.71, CUDA 13.1. GPUs: RTX 5090 32 GB (PCIe 5.0 x8, the engine's ring line: 28.9 GB/s), RTX 3090 24 GB on PCIe 4.0 x4 (OCuLink, 6.7 GB/s), RTX 3090 24 GB on PCIe 4.0 x8 (13.4 GB/s). Model / engine: DeepSeek-V4.1-Flash-1017 Maya-XS (v2), v1.1.61 ( Harness (as in the PR description): the same binary in both arms, only
Two runs per arm, each value one run; the two runs of an arm agree within 0.15 s.
Not tested: |
… link (MAYA_DS4_PREFILL_CPU_EXPERT_MS unset) The planner weighed a streamed expert with one time for every GPU, so on a layer split with mixed links the slowest link got the smallest CPU share (mw00#959: a 5090 on PCIe 5 x8 and two 3090s on PCIe 4 x4 / x8). Unset, DS4_PREFILL_CPU_EXPERT_MS is now each GPU's own: the layer's expert bytes over the link link_h2d measures at the load, in steps of 0.05 ms; the load logs it per GPU. A value still sets one for every GPU. 2x RTX 3090 on PCIe 3.0 x16 (11.4-12.1 GB/s: 0.85-1.10 ms), two runs each: through the staging the same as 0.9, with DS4_FLOW_COPY_DMA=1 fresh prompts the same and agent turns 2% slower than the hand-tuned 1.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad (MAYA_DS4_PREFILL_CPU_ROW_MS unset) ROW_MS was one value measured on two Ivy Bridge Xeons (0.49 ms); a faster CPU's lane computes a row in a quarter of that (mw00#959: 0.11-0.13 ms on a Threadripper 5975WX), so the planner gave it far too small a share. Unset, the load now times the lane once: the first layer with 64 experts on the CPU lane (no VRAM slot, copied into RAM where the lane copies), those 64 for one and two rows in turn as the share takes them, four passes, the first a warm-up, the middle of the other three, in steps of 0.01 ms; the load logs it. A value still sets it; where no layer has 64 such experts the default stays and the log says so. On the box the default came from (2x Xeon E5-2697 v2, 2x RTX 3090) it measured 0.48-0.49 ms, and six runs gave the same prompt times as 0.49 set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @sanasol for the third box and the per-GPU breakdown. Both estimates the planner uses are now measured where they are not set (152dc6e, 20e2451):
Either setting given still applies to every GPU as before. Here (PCIe 3.0 x16: 0.85-1.10 ms an expert, 0.48-0.49 ms a row measured) it plans like the hand-set values: the same prompt times through the staging, agent turns about 2% slower than the hand-tuned 1.2 with One GPU, since most setups have one (one RTX 3090, Maya-S, 256K context, A correction to my earlier comment: I had assumed x4 links for the CMP 170HX; your cards' Gen2 x16 / 6.8 GB/s figures are what the new default works from. |
|
Checked In one ordered smoke pair, fresh 1024/4096-token prompts took 1915/4996 ms with explicit old costs and 1665/4739 ms with measured costs. Other CPU validation jobs ran during the session, so these are descriptive observations, not an isolated performance comparison. Greedy token hashes differed on three of four cases; quality was not evaluated. These equal-bandwidth links do not validate the mixed-link case. Driver limits remained 150 W throughout the samples; one instantaneous power reading was 216.47 W. The highest sampled core/HBM temperatures were 54/59 C. No production setting was changed. Evidence: MAYA.json; MAYA_FIXTURES.json; MAYA_OPERATING.json; maya_worker.py. |
TL;DR: When an agent sends a few hundred to a few thousand new tokens, almost every expert kept in RAM is needed, but most of them only for one to three of those tokens. Maya sent every one of them whole over PCIe to the graphics card, about 1 ms each. With
MAYA_DS4_PREFILL_CPU=1the CPU computes the least-used ones itself while the card loads and computes the rest, split per layer so both finish at about the same time. On my PC an agent's turn of 500-700 new tokens is read in 5.4-6.0 s instead of 8.9-9.7 s (4.1-4.9 s together with the DMA of #958). Off by default; the last bits of the answer can move, as they do between the chunked pass and windows.What
After each layer's routing on the flow (a chunk of at most
MAYA_DS4_PREFILL_CPU_MAX_Ttokens, default 4096, a layer alone on its device, no helper):flow_spec) are sorted fewest rows first and taken for the CPU while the CPU's estimated time (a row's time, measured once at the load;MAYA_DS4_PREFILL_CPU_ROW_MSsets it) stays under the stream's for the rest (an expert's bytes over that GPU's own link as the load measures it;MAYA_DS4_PREFILL_CPU_EXPERT_MSsets one time for every GPU);moe_sort), soflow_reststreams only its own and the CPU's rows are one range[pc_r0, pc_r1);cpu_expertson them (inputs and row tokens read back to pinned buffers) whileflow_layerstreams and computes the others;pcpu_finishjoins it, uploads its rows and adds them withmoe_combine(moe_combine_xwhere the sums are fixed-point) after the layer's streamed and VRAM experts.cpu_expertsnow takes a recursive mutex (lane_mu) so the lane's scratch has one caller at a time (the share's thread beside a decode on the other slot);lane_busy=counts the times the share found it taken (0 in every run here).DS4_PREFILL_STATS=1addsDS4_PREFILL_CPU gpu= layers= experts= rows= cpu_ms= wait_ms= lane_busy=a prompt and GPU. Docs: a section and four rows indocs/SETTINGS.md.Why
A short prompt on a PC whose experts mostly sit in RAM streams nearly all of them: 600 new tokens touched 7,700 RAM experts (87 GB) on my PC, about 40% of them for one to three rows, at ~1 ms an expert over PCIe 3.0 - while the CPU lane computes such an expert in ~0.5 ms a row. GLM has had the same split since
MAYA_GLM_PREFILL_CPU.Measured
Two Xeon E5-2697 v2 (Ivy Bridge, 12 cores each: AVX and F16C, no AVX2 or FMA), 16x 32 GB DDR3-1600, 2x RTX 3090 24 GB on PCIe 3.0 x16 (both on socket 0); Linux, CUDA 13.0. Maya v1.1.59 (
027992c) + this PR (rebased onto main since; only docs changed there), DeepSeek-V4.1-Flash-1017 Maya-S,--tp 2(falls back to a layer split: one GPU a layer group),--parallel 2,--max-context 1048576,--dspark none, 20 CPU threads; 3,144 experts in VRAM, 12,212 (134.6 GB) copied into RAM in 2 NUMA parts and pinned.Harness: a fresh engine per run, one frozen copy of
expert-profile.binrestored before every start (the profile the server saves at each stop moves the placement otherwise), the same requests every run,MAYA_DS4_SLOT_KEEP=0,MAYA_DS4_PROMPT_PICK=length:96in every run (so a run's path does not depend on the timing pick). Per run: three fresh prompts (326 / 602 / 1,638 tokens, nothing reused, first token only), then a 20.6K-token start read once and eight reads on top of it (an agent's turn: 500-3,200 new tokens), then the decode bench. Runs in this order: neither setting,DS4_FLOW_COPY_DMA=1only (#958),MAYA_DS4_PREFILL_CPU=1only, both, neither again; "without" below is the mean of the two runs with neither.EXPERT_MS0.9)With
DS4_FLOW_COPY_DMA=1in both arms (#958,EXPERT_MS1.2): fresh 6.70 / 7.52 / 9.82 -> 3.18 / 3.92 / 6.59 s (-53 / -48 / -33%), an agent's turn 7.54-8.29 -> 4.08-4.94 s (-40 to -46%), 3,123 / 3,236 new tokens 11.10 / 11.34 -> 8.74 / 9.03 s (-20%). Both against neither: an agent's turn of 500-700 new tokens -49 to -54%.MAYA_DS4_PREFILL_CPU_EXPERT_MSsweep (a fresh engine each, the same profile), withDS4_FLOW_COPY_DMA=1on v1.1.56, fresh 326 / 602 / 1,638 tokens - 0.3: 4.5 / 5.3 / 8.1 s, 0.4: 4.2 / 5.0 / 7.7, 0.5: 4.0 / 4.7 / 7.6, 0.9: 3.4 / 4.1 / 6.8, 1.2: 3.2 / 3.9 / 6.6, 1.5: 3.5 / 4.4 / 7.5, 2.0: 4.0 / 5.1 / 8.8 (the CPU the limit from 1.5 on); through the staging 0.9 beat 1.46 (an agent's turn 5.6-6.4 vs 5.9-6.7 s), hence the default 0.9.Decode (three answers of 300 tokens, greedy, thinking off, two sets of questions A / B): 25.7 / 26.6 tok/s with it, 24.5 / 25.8 and 26.2 / 27.2 without (two runs), 25.7 / 26.2 with both PRs - the same within the runs' spread (the share only runs during prompt chunks).
Answers: the CPU's sums for the CPU's experts, so the last bits move. First-token top-10 distributions of three fresh prompts against the chunked pass without it, KL over the common tokens: 7.1e-4 / 2.1e-3 / 2.1e-3 (326 / 602 / 1,638 tokens) - the same size as the unchanged engine against itself across a restart (1.1e-3 / 5.0e-3 / 8.5e-3) and as windows of 16 against the chunked pass (v1.1.56: 3.8e-4 / 2.1e-3 / 6.0e-2); the first token was the same everywhere.
Update (2026-11-13)
DS4_FLOW_COPY_DMA=1); the tables above were measured with the fixed values.--parallel 1, the values measured at the load, two runs per arm): fresh 326 / 602 / 1,638 tokens 8.86 / 10.55 / 13.05 -> 5.11 / 6.51 / 10.05 s (-42 / -38 / -23%), eight agent turns of 506-3,236 new tokens 96.5 -> 66.2 s together (-31%); with DeepSeek:DS4_FLOW_COPY_DMAalso where the RAM copies are split across CPU sockets - short prompts 11-16% faster on two sockets, the same bits #958 as well 4.00 / 5.20 / 7.73 s and 56.0 s. Decode the same, the first token the same in every run.What I could and couldn't test
python tools/check_hip_compat.pypasses; I could not compile or run HIP here.🤖 Generated with Claude Code