Part of #405 (README vs llama.cpp CUDA benchmark sweep). Biggest gap found.
OLMoE-1B-7B fully offloads to the GPU (-g -1), yet its CUDA prefill is ~176× slower than llama.cpp on the same box, and prefill is even slower than its own decode — the tell-tale sign of a per-token prefill loop.
Measured (RTX 4070 Ti, llama.cpp CUDA b9529)
|
README (sharpi) |
llama.cpp -ngl 99 pp2048 |
gap |
| prefill |
97.4 t/s |
17 137 t/s |
176× |
| decode |
126.0 t/s |
426.1 t/s (tg128) |
3.4× (see #408) |
OLMoE prefill (97.4) < OLMoE decode (126.0) ⇒ prefill is running token-by-token, not as a batched trunk.
Root cause (verified)
CudaForwardPass.IsBatchedPrefillSupported() (src/SharpInference.Engine/CudaForwardPass.cs:2618) disables the fast int8-MMQ batched-trunk prefill (#136/#141) for any MoE model:
if (_isMoE || _tqEnabled || _hasAttnBias || !_hp.IsNeoxRope) return false;
_isMoE trips for OLMoE, so Prefill() falls through to the per-token Forward() loop (CudaForwardPass.cs:~2557-2572) — every prompt token re-reads all weights, no tensor-core MMQ, no batched attention.
The work
Extend the batched-trunk prefill to dense-attention MoE models (OLMoE: standard attention + a routed-MoE FFN). The trunk attention path is identical to the supported dense models; the new piece is a batched MoE FFN over N prompt tokens:
- Run the batched attention trunk (already exists —
PrefillBatchedTrunk).
- For the FFN, batch the router over N tokens, then do a grouped/segmented expert GEMM (gather tokens per expert → per-expert GEMM-N → scatter), instead of the per-token expert matvec. The CPU-MoE prefill already groups tokens per expert (
PrefillBatchedCpuMoe in CudaHybridGdnForwardPass) — mirror that bucketing for the on-GPU full-offload FFN.
- Relax the
_isMoE gate to allow OLMoE-shaped models (dense attention, on-GPU experts) once the batched FFN exists; keep it closed for the GDN/CPU-MoE hybrids that have their own path.
Argmax-stable (int8 MMQ) is the accepted contract, matching the dense models. Validate greedy parity vs the per-token loop and re-measure prefill.
References
Part of #405 (README vs llama.cpp CUDA benchmark sweep). Biggest gap found.
OLMoE-1B-7B fully offloads to the GPU (
-g -1), yet its CUDA prefill is ~176× slower than llama.cpp on the same box, and prefill is even slower than its own decode — the tell-tale sign of a per-token prefill loop.Measured (RTX 4070 Ti, llama.cpp CUDA
b9529)-ngl 99 pp2048tg128)OLMoE prefill (97.4) < OLMoE decode (126.0) ⇒ prefill is running token-by-token, not as a batched trunk.
Root cause (verified)
CudaForwardPass.IsBatchedPrefillSupported()(src/SharpInference.Engine/CudaForwardPass.cs:2618) disables the fast int8-MMQ batched-trunk prefill (#136/#141) for any MoE model:_isMoEtrips for OLMoE, soPrefill()falls through to the per-tokenForward()loop (CudaForwardPass.cs:~2557-2572) — every prompt token re-reads all weights, no tensor-core MMQ, no batched attention.The work
Extend the batched-trunk prefill to dense-attention MoE models (OLMoE: standard attention + a routed-MoE FFN). The trunk attention path is identical to the supported dense models; the new piece is a batched MoE FFN over N prompt tokens:
PrefillBatchedTrunk).PrefillBatchedCpuMoeinCudaHybridGdnForwardPass) — mirror that bucketing for the on-GPU full-offload FFN._isMoEgate to allow OLMoE-shaped models (dense attention, on-GPU experts) once the batched FFN exists; keep it closed for the GDN/CPU-MoE hybrids that have their own path.Argmax-stable (int8 MMQ) is the accepted contract, matching the dense models. Validate greedy parity vs the per-token loop and re-measure prefill.
References
src/SharpInference.Engine/CudaForwardPass.cs:2618(IsBatchedPrefillSupported),:2511(prefill gate),PrefillBatchedTrunk