deepseek_v4: size the OpenMP team around the expert loaders - #897
Merged
Conversation
The engine never sized its OpenMP team, so the runtime default (every logical CPU) scheduled compute threads onto the CPUs the block pipeline's persistent expert-loader workers need. On a disk-bound decode those loaders do the rate-limiting work, so the collision costs real throughput; omp_tune.h records the same rationale for the GLM spin-wait tuning (a busy team steals cores from the I/O pool). Reserve the loader workers' CPUs when the user has not chosen a team: threads = logical CPUs - COLI_V4_EXPERT_LOADER_COUNT, skipped on tiny machines (team would drop below 2). OMP_NUM_THREADS and COLI_NO_OMP_TUNE=1 win, exactly like the other engines' tuning. The loader-count default moves to deepseek_v4_internal.h so the CLI and the block pipeline agree on one number.
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.
Summary
The DeepSeek V4 engine never sizes its OpenMP team β unlike every other engine in the project (colibri, inkling, kimi_k3, olmoe size via
omp_tune.h; GLM has its own block). The OpenMP runtime default is every logical CPU, which schedules compute threads onto the CPUs that the block pipeline's persistent expert-loader workers need. On a disk-bound decode those loaders are doing the rate-limiting work, so the collision costs measurable throughput: about 9β10% decode on the machine below. This is the same rationaleomp_tune.hrecords for the spin-wait half of the GLM tuning: a busy team steals cores from the I/O pool.The change: when the user has not chosen a team, set
threads = logical CPUs β COLI_V4_EXPERT_LOADER_COUNT(the loader pool's worker count, default 3).OMP_NUM_THREADSandCOLI_NO_OMP_TUNE=1win, exactly like the other engines' tuning. On machines where the reservation would leave fewer than 2 compute threads, the OpenMP default is left alone. The loader-count default moves todeepseek_v4_internal.hso the CLI and the block pipeline agree on one number.I considered reusing
coli_omp_tune_threads()(physical-core sizing) instead. On this box it recovers the same decode (+9.9%), but it pays for it in time-to-first-token: prefill is compute-heavy, and 8 threads prefill ~9% slower than even the untuned default (78.6 s vs 72.1 s median). The loader reservation improves both metrics at once, and it degrades gracefully on bigger machines (reserving 3 of 32 logical CPUs is a smaller bite than halving the team).Measurements
Hardware: AMD Ryzen 7 6800H (8 cores / 16 threads, Zen 3+, AVX2), 32 GB DDR5-4800, weights on a WD Green SN350 1 TB NVMe (Gen3 x4, ext4), Ubuntu 26.04 LTS, gcc 15.2.0.
Model: DeepSeek-V4-Flash-0731 (release FP4/FP8 weights),
memory=auto(~27.4 GiB),CTX=4096,target_only=1.Binary: release v1.5.0 (
8f512fc), unmodified, built withmake -C c deepseek-v4.Method: same 45-token prompt for every run, 160 generated tokens,
--no-dspark. Decode tok/s = generated Γ·after_firstfrom the engine's owntimingline. To make runs independent, every run restores an identical.coli_usagewarm-pin history and setsCOLI_V4_SAVE_USAGE=0(expert-cache hit rate came out 65.29β65.33% on all 12 runs, so the arms saw the same cache behavior). Warm-up policy: one discarded 16-token run first. Run count: 3 per configuration, interleaved with alternating order to cancel drift. Medians reported.omp_tune.hpicks)Per-run decode values: unset 0.7778/0.7698/0.7790 Β· 14: 0.8571/0.8601/0.8519 Β· 13: 0.8486/0.8409/0.8517 Β· 8: 0.8590/0.8546/0.8329.
14 threads (reserve 2) measured marginally better than 13 on this box; the patch still ties the reservation to
COLI_V4_EXPERT_LOADER_COUNTbecause that is the number the code can justify β the 13-vs-14 gap (~1%) is close to run-to-run spread, and a hardcoded "reserve 2" would silently drift if the loader pool ever changes size. If you'd rather chase the last percent I'm happy to change it.Behavior of the patched build (base
dev), verified on the real model:OMP_NUM_THREADSunset β a full 160-token generation completes and logs[OMP] deepseek-v4: 13 compute threads (16 logical CPUs minus 3 expert-loader workers); OMP_NUM_THREADS=<n> overrides, COLI_NO_OMP_TUNE=1 disablesOMP_NUM_THREADS=14β no[OMP]line, the user's value is used untouchedCOLI_NO_OMP_TUNE=1β no[OMP]line, OpenMP default preserved(The performance table above is measured on the unmodified v1.5.0 release binary with manual
OMP_NUM_THREADS, which exercises the exact team sizes the patch selects; the patched build sits ondev, whose memory planning differs enough that cross-base decode numbers would not be apples-to-apples.)This is one machine; I'd welcome numbers from other CPU/storage combinations, in particular boxes with more cores or faster storage.
Validation
make checkon the patched tree (Linux x86-64): 373 tests, OK (52 skipped)make -C c cuda-test(not applicable β CPU-only change)Exact benchmark commands
Compatibility
omp.hinclude is guarded by_OPENMP, same as colibri.c)