Repository navigation
Pr/cuda moe cache - #288
Conversation
Replaces the complex v3 scheduler/fusion/persistence design with per-node hybrid execution. - Keeps MUL_MAT_ID on CPU: CPU threads compute miss rows while CUDA concurrently computes the resident expert hits of the same node. - Adds scheduler-owned cache sessions with slot pinning, slot generation tracking, per-device dispatch locks, bounded fill workers, and safe nested session scopes. - Handles weight mutation, buffer destruction, and backend reload: every public backend write path now invalidates overlapping cache state on host WEIGHTS buffers, and the cache unregisters itself when the CUDA backend is unloaded. - Survives CUDA allocation pressure and tears down deterministically by cancelling queues, waiting for active nodes, joining workers, and then freeing device storage. - Adds conservative per-expert admission, sticky capacity-aware layer/tensor-to-device routing, and post-fill eviction throttling to reduce fill and eviction churn. - Fixes --moe-cache auto/on/off/0 semantics, repacking interactions, hardware eligibility, dormant behavior, output strides, and a small-K kernel overread. - Removes the unsafe auto-fit GGUF scanning rule, fused GLU and GPU-output handoff paths, hot-set persistence, and runtime bail-out policy machinery; HIP/MUSA builds compile inert stubs. - Adds lifecycle, concurrency, invalidation, fault fallback, routing, admission, parser, and benchmark coverage. - Documents configuration, eligibility, diagnostics, limitations, and reproducible benchmarking. GLM-5.2 validation showed a substantial decode speedup with statistically equivalent perplexity, coherent generation, and correct long-context retrieval. Output is quality-equivalent but not bit-identical because CPU and CUDA rounding can change near-tie token decisions. Non-CUDA and disabled-cache paths remain inert. Assisted-by: Codex
Track exact expert entries for shared pools and recalculate the process-wide reserve as cache sessions join and leave. Add coverage for mixed expert counts and mixed reserve lifetimes. Assisted-by: Codex
Keep draft placement anchored to the first reordered GPU, retain parent backends for split-mode none, and exclude non-GPU devices from automatic draft candidates. Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Keep DFlash and DSpark weights on the requested draft devices while exposing target backends required by shared tensors. Avoid the target main device for automatic placement, reserve both shared-head layouts, and refit from pristine parameters after sleep. Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
Select retained CPU rows from token count, routing width, and expert size while preserving fixed-row overrides. Assisted-by: Codex
Use the selected devices' effective expert-size floor consistently in fit planning, dormant-session detection, and CUDA session creation. Ampere-class devices admit smaller experts by default while older devices keep the conservative floor, and explicit overrides remain authoritative. Assisted-by: Codex
Reserve every 64-slot shape floor before distributing excess capacity so a complete fit cannot starve a rare pool. Expose the expert-count boundary through the capability query and reject sub-64-expert tensors consistently during fit, dormant detection, and execution. Assisted-by: Codex
Use the existing MMVQ path when activation rows follow its modulo layout, while retaining the dedicated activation-map kernel for irregular multi-token routing. Keep the environment override as a diagnostic force switch. Assisted-by: Codex
Run independent per-device fill workers concurrently by default when at least two Ampere-class devices are selected. Keep serialized fills on single or older devices and preserve the explicit override. Log and test the selected policy. Assisted-by: Codex
Measure both layer-split and canonical CPU-expert main-device MTP layouts, and include the target scheduler compute footprint in the additional context reservation. This prevents auto-fit from exhausting the primary GPU before the MTP context is created. Assisted-by: Codex
Assisted-by: Codex
Assisted-by: OpenAI Codex
Permit same-shape tensors with fewer than 64 experts to satisfy the pool floor in aggregate. Keep undersized aggregate pools ineligible, align auto-fit accounting, and cover the Llama 4 shape regime in tests. Assisted-by: OpenAI Codex
Raise the Ampere expert-size floor from 64 KiB to a measured 512 KiB boundary. Retain profitable sub-1-MiB OLMoE and Qwen3.6 experts, preserve the explicit override, and document the supporting A/B results. Assisted-by: OpenAI Codex
Expose a measured 1 GiB automatic slab floor through the cache capability query and exact fit planner while retaining forced small-capacity experiments. Resolve fixed undersized auto sessions to off before execution and defer backend policy logs until first eligible use. Assisted-by: OpenAI Codex
Use the tested eight-token cache limit for auto, on, and fixed-budget modes so explicit cache configurations do not bypass speculative verification batches by default. Keep larger prompt nodes on the stock path and cover forced-mode multi-token execution without an environment override. Assisted-by: OpenAI Codex
Report explicit and fit-selected cache modes at normal verbosity, emit a trace marker after the first successful pool allocation, and document the verbosity needed for allocation evidence. Assisted-by: OpenAI Codex
Use llama-bench's -v switch in reproduction commands while retaining -lv 4 guidance for common-parser applications. Assisted-by: OpenAI Codex
Admit on the first miss only when the exact shape inventory proves that every entry fits. Keep second-miss admission and replacement throttling for constrained pools, and preserve explicit overrides. Assisted-by: OpenAI Codex
Pass the routed-row count through the existing cache hook so token and fixed-scratch bounds fail visibly while preserving the stock CPU fallback. Assisted-by: OpenAI Codex
Assisted-by: OpenAI Codex
Assisted-by: OpenAI Codex
Assisted-by: OpenAI Codex
Upload cache slot IDs and activation rows with one contiguous transfer per node. Assisted-by: OpenAI Codex
Assisted-by: Buffy
…ession creation, and fit evaluation Assisted-by: Buffy
Assisted-by: Buffy
Assisted-by: Buffy
…ntro and best-use guidance The cache is now implemented by CUDA, Metal, and Vulkan providers, so the CUDA-only name was misleading. The new layout adds a what-is-it-for section, a quick start, an explicit note that fully VRAM-resident models are not the target, and keeps the deep-dive content (eligibility, demand fill, DeepSeek V4, GLM-5.2, benchmarking, limitations). Assisted-by: Buffy
…exps names, soft mode) The provider list claim now reflects that MUSA builds compile the CUDA source to stubs and do not provide the cache, the eligibility bullet documents the _chexps tensor-name suffix accepted by the probe, and the mode table documents soft. HIP builds share the CUDA implementation, and Metal/Vulkan always report Ampere-class capability. Assisted-by: Buffy
The -pw/--prefetch-weights flag is documented on the moe-cache branch but has not been merged into pr/cuda-moe-cache yet. The help text references cmd_params_defaults.prefetch_weights which does not exist, causing a build error. Remove the documentation line from llama-bench.cpp and README.md. The feature will be documented when it lands on this branch. Assisted-by: Buffy
|
Re-reviewed against the merged base. Now that #287 is in, this is a 33-file, +9910/-323 diff instead of 209 files, which is far easier to read. Thanks for merging the base in and for dropping the dead Local results, macOS/Metal
One ordering problem worth fixing before this landsThe
That matters because of your own Known Limitations entry: mixed K/V types do not work with the MoE cache, and the documented workaround is to pass identical types, specifically Confirmed on this exact build, Qwen3.6-27B, which sits right at the threshold: So between #288 landing and #289 landing, there is a window where Second thing, smaller but structural
Exit 0 means ctest records a pass. On any non-CUDA runner this test is indistinguishable from a real one, while executing none of the 3260 lines it contains. That is what let the Metal and Vulkan backends ship uncompiled earlier in this stack. Cheapest fix is to have it exit 77, which ctest understands as a genuine skip via What I still owe youCUDA is still unverified on my side. Everything above is the non-CUDA path. I have not built this on a CUDA box, have not run Two questions on the numbers when I get there, so I can check the right things:
|
|
@TheTom applying the fixes now. |
Add !is_mla guard to auto-asymmetric KV type upgrade. MLA models (DeepSeek-V4) have no separate V cache (V = view of K), so K and V types must be identical. Without this guard, auto-asymmetric would upgrade K from turbo4 to q8_0 while V stays turbo4, breaking the documented workaround of -ctk turbo4 -ctv turbo4 for MoE cache on DeepSeek-V4. Moves the guard from TheTom#289 into this PR to close the ordering window. Fix test-moe-cache to exit 77 on non-CUDA builds. Exit 0 was recorded as a pass by ctest, making 3260 lines of dead code indistinguishable from a real test run. Assisted-by: Buffy
2762098 to
6d4a1db
Compare
|
Verified on macOS/MetalThe exit-77 change is exactly the right fix. A non-CUDA runner now says skipped instead of silently claiming a pass over 3260 lines it never ran. On the MLA guard, which I did chase downYou used if (k_is_turbo && !hparams.is_mla()) {whereas #289 and
The I did try an alternative fix that hoisted a shared On the tradeoffTom has said the PP-for-TG tradeoff is fine by him, decode speed is what he is after, so please treat my earlier question about whether the DeepSeek-V4 PP gain is reproducible as withdrawn. Not worth your time. The one thing I would still like an answer on, because it changes what I test rather than whether I approve: with the cache on, is output expected to be bit-identical to cache-off, or only close? A hit computes an expert on the GPU and a miss on the CPU, so warmth alone could change results between runs. Your testing guide says "byte-identical or very close" and those need different oracles. If close is expected, I will compare distributions rather than diff tokens. Where this leaves the PRNon-CUDA is clean from my side and I have no blocking comments left. CUDA remains unverified by me, the bench has not been free. That is the only thing standing between this and my sign-off, and it is on me, not you. |
find_cuda_device() and find_other_cuda_device() require the device type to be GGML_BACKEND_DEVICE_TYPE_GPU. Unified-memory NVIDIA parts report GGML_BACKEND_DEVICE_TYPE_IGPU instead, since they use host memory, so the lookup fails and the whole suite skips on hardware that has a perfectly usable CUDA device. Observed on GB10 (Grace Blackwell), where the backend registry reports: dev 0: reg='CUDA' type=2 (IGPU) desc='NVIDIA GB10' dev 1: reg='CPU' type=0 test-backend-ops avoids this by excluding CPU rather than requiring GPU. This also affects Jetson/Orin and any integrated GPU. With the fix the suite actually runs on GB10: 37 OK, 2 SKIP, 1 FAIL (cache-shared-budget, reported separately).
|
CUDA verification done at last, on a GB10 (Grace Blackwell, sm_121). Two findings, one of which I have pushed a fix for as 1. The suite could not run at all on this hardware
The lookup requires GGML_BACKEND_DEVICE_TYPE_CPU = 0
GGML_BACKEND_DEVICE_TYPE_GPU = 1 // dedicated memory
GGML_BACKEND_DEVICE_TYPE_IGPU = 2 // integrated, host memory
GGML_BACKEND_DEVICE_TYPE_ACCEL = 3GB10 is unified memory, so IGPU is the correct classification. The consequence is that the entire MoE cache suite silently skips on Grace Blackwell, Jetson/Orin, and any integrated GPU, which is a meaningful slice of the hardware this feature is aimed at. Fixed at both call sites in Worth noting how this compounds with the exit-77 change from 2.
|
The budget-sharing test computed its reserve from ggml_backend_dev_memory, which on UMA devices (Grace Blackwell, Jetson/Orin) reports /proc/meminfo system memory. The cache's own budget path queries cudaMemGetInfo, and the two disagree on unified memory, so the reserve left the wrong remainder and granted values landed outside the expected 8-16 MiB band. Query cudaMemGetInfo in the test to match the cache, with a fallback to the backend device memory API when CUDA is unavailable. CMake wires the CUDA include dirs, cudart link, and GGML_CUDA define into the test target when GGML_CUDA is enabled. Assisted-by: Buffy
|
First fully green And thank you for tracing the actual mechanism. I had only got as far as "the cache and the test seem to be reading different memory numbers on UMA" and explicitly flagged it as a hypothesis I had not confirmed. You confirmed it and fixed the right side: matching the cache's own query rather than loosening the expected band, which would have hidden it. One thing to fix before this lands
const cudaError_t err = cudaSetDevice(physical >= 0 ? (int) physical : 0);and the call order in So Neither of us can see this. On a single-GPU box Suggest saving and restoring: int prev_device = 0;
cudaGetDevice(&prev_device);
// ... cudaSetDevice / cudaMemGetInfo ...
cudaSetDevice(prev_device);Related and in the same two lines: when Both are cheap, and I would rather they go in now than become a confusing multi-GPU-only failure later. Where that leaves my reviewEverything I can test is now green: No blocking objections from me on the code. The remaining CI red on this PR is I have not benchmarked the soft-mode PP/TG table and am not planning to. Tom has said the tradeoff is what he wants, so re-measuring it would not change any decision. |
run_shared_budget called cudaSetDevice without restoring the previous device, and falls back to device 0 when the physical device is not reported. Both leak into run_route_override, which runs next and specifically exercises two-device routing, and would surface as a confusing multi-GPU-only failure. Save the current device, use it as the fallback target, and restore it on every path, including the cudaMemGetInfo error path. Assisted-by: Buffy
|
Both points covered, and you went further than I asked by restoring the device on the Being straight about the limits of this particular check: the save/restore only matters on a two-GPU box, and this one has a single device, so Sign-off from meNo blocking objections to #288. Everything I have hardware for is green:
For anyone reading this thread later, the three test-infrastructure defects found along the way are worth remembering as a set, because each one hid the next:
Only after all three did a single line of the 3260-line suite actually execute on this hardware, and then it passed. The feature was fine the whole time; the harness could not see it. Remaining CI red on this PR is |
This branch carries the GGML_API change that moves extern into the macro on
every path. That makes the definition site expand to
extern int turbo3_cpu_wht_group_size = 0;
which gcc rejects under -Werror:
ggml-turbo-quant.c:28: error: 'turbo3_cpu_wht_group_size' initialized and
declared 'extern' [-Werror]
cc1: all warnings being treated as errors
Declares the symbol with GGML_API, so it still carries dllexport/visibility,
then defines it plainly on the next line.
Verified: builds clean, and the ODR property from TheTom#274 is preserved -- the
symbol is still exported from libggml-base and libggml-cpu still has zero
definitions of it. Compiles warning-free under -Werror -Wall -Wextra for
static, shared-ELF and MinGW expansions of GGML_API.
The CUDA CI job builds with fatal warnings and both of these are in code
added by this PR, so the job has been failing at 'Build with CMake'.
1. ggml-cuda.cu, VMM pool OOM retry:
error: comparison between 'enum cudaError_enum' and 'enum cudaError'
[-Werror=enum-compare]
cuMemCreate is a driver-API call returning CUresult, while
cudaErrorMemoryAllocation is a runtime-API cudaError_t. The comment said
the per-vendor alias kept this portable, but that only holds on HIP and
MUSA, whose vendor headers map cuMemCreate onto the runtime error type
(hip.h:142 and hip.h:85). On CUDA the two are distinct enums that happen
to share the value 2, so the logic worked wherever it compiled at all.
Uses CUDA_ERROR_OUT_OF_MEMORY on CUDA and keeps the runtime constant on
HIP/MUSA.
2. common/fit.cpp, layer-footprint overflow guard:
error: comparison of integer expressions of different signedness:
'uint64_t' and 'long int' [-Werror=sign-compare]
Both operands are already guarded positive on the preceding lines, so the
right-hand side is cast to uint64_t, preserving the guard's meaning.
Verified on aarch64 Linux + CUDA 13.0 with -DLLAMA_FATAL_WARNINGS=ON: a
keep-going build reports 0 errors and produces all binaries; test-moe-cache,
test-moe-cache-fit and test-turbo-quant all exit 0.
The test returns 77 when no CUDA device is present, but ctest only
interprets that as a skip when the SKIP_RETURN_CODE property says so.
Without it 77 is just a non-zero exit, so every CPU-only runner records the
skip as a failure. That is what windows (x64-cpu-static) has been failing
on:
98% tests passed
41 - test-moe-cache (Failed)
With the property set, ctest reports:
The following tests did not run:
42 - test-moe-cache (Skipped)
and the suite passes. Verified locally on a build with no CUDA device.
419c179 to
22a370e
Compare
|
Heads-up and an apology: I rewrote this branch's commit metadata and force-pushed, which I should have asked about first. What changed: only the author/committer email on my own three commits, which were wrongly attributed to a work address. Your two commits are untouched and still authored by you, and every tree is byte-identical, so there is no content difference at all. I used What it means for you: if you have this branch checked out locally, the SHAs moved: Sorry for the churn. I will not force-push here again. The one upside is that it triggered a completely fresh CI run, which re-runs Current state on |
|
@TheTom No worries, I dont need "credit" or anything :) I just like working on the project. |
|
Appreciated. To be clear though, the apology was for force-pushing your branch without asking, not about credit. The authorship line was me saying I had not clobbered your work, and it came out sounding like the other thing. Either way it will not happen again. CI is running clean on the new head so far, only the pre-existing HIP nodiscard red. |
0d19d8b
into
TheTom:feature/turboquant-kv-cache
Overview
Do not merge until #287 is complete