Repository navigation
feat: add --embed-weights host for a pinned host embedding table - #806
Merged
Merged
Conversation
jason-fxz
marked this pull request as ready for review
October 23, 2026 03:58
Collaborator
Author
Contributor
|
@LZ-QWQ please take a look |
Contributor
@jason-fxz Validated on R9700 (gfx1201) and W7900D (gfx1100) with ROCm 10.1.0.
Decode performance with Qwen3.8-Flash-Next-FP8 (256 input / 128 output tokens; median of six measurements per configuration):
No ROCm-specific issues found in the tested PR paths. |
KarrAcaRn
pushed a commit
to KarrAcaRn/FreeToken-ByAI
that referenced
this pull request
Oct 23, 2026
FlashML-org#805 FlashML-org#797 FlashML-org#795 adopted (FlashML-org#805 replaces our 8236bb3 kernel changes), FlashML-org#806 and FlashML-org#810 deferred. Assisted-by: Claude Opus 5.5
KarrAcaRn
pushed a commit
to KarrAcaRn/FreeToken-ByAI
that referenced
this pull request
Oct 24, 2026
Upstream FlashML-org#806 (--embed-weights host) now sits next to our --embed-device cpu: both flags stay, using them together is refused, and the host-table byte count covers tables moved by either one. FlashML-org#805/FlashML-org#795 hunks keep next's adopted versions; setup.py keeps our extra_compile with upstream's ROCm row_store args. Assisted-by: Claude Opus 5.5
KarrAcaRn
pushed a commit
to KarrAcaRn/FreeToken-ByAI
that referenced
this pull request
Oct 24, 2026
…ce cpu as its alias Upstream FlashML-org#806 pins the input-embedding tables as they load and its index kernel reads them zero-copy, as fast as our Triton UVA gather (2x faster for one row, equal for 4096), so our post-load move and the gather go. Kept from ours: tables tied to the LM head are skipped one by one, so gemma4's per-layer table also moves on a tied model, and the ft info forecast and tip, now priced without a load-time VRAM peak. Assisted-by: Claude Opus 5.5
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
--embed-weights hostkeeps the input token embedding table in pinned host RAM, so its VRAM goes to the MoE expert cache and the KV pool. The default stays--embed-weights gpu, which leaves serving unchanged.The lookup reads only a few rows per step (one per decode token, one per prompt token in prefill). The GPU reads them over PCIe, and the table never has to sit in VRAM. A table tied to the LM head stays on the GPU because the head reads the whole table every step, so
hostis refused at start-up when the model config ties them (tie_word_embeddings). It is also refused for a model whose embedding is not a plain table, such as the DF11 table of non-GGUF GLM-4.x checkpoints.Closes #754
Design
_materialize_loaded_weight_state_dictand in the dummy-weight path.--moe-cache-autogets the freed VRAM without extra code.VocabParallelEmbeddingops of the model; the tie comes frommodel_config.tie_word_embeddings, which every adapter that can tie its LM head uses to wire it. The op walk isiter_opsinlayers/base.py, the walkfinalize_quanthad, now shared by both.fast_index_copyfollows for pinned expert banks.host_ptr_identity/device_aliasmove fromfast_index_copy.cuhinto a shared headerfreetoken/pinned.h. On Windows/WDDM, where the device address can differ from the host address, they translate throughcudaHostGetDevicePointer.indexkernel (including the TP masked variant) and ggml'sop_get_rowsaccept a host weight. They take the launch device and stream from the output, and allocate the output on the ids device. With--embed-weights gputhey get the same tensors as before._host_tables_bytes), next to qwen4_exp's PLE table, before the MoE offload cache plans its banks.Results
1x H100 80GB HBM3 (driver 580.95.05), Xeon Platinum 8480+ (26 vCPUs), CUDA 13.0, torch 2.14.1+cu130, Python 3.14.3.
End-to-end:
--embed-weights gpuvshost, 24 GiB free VRAM, AIME25 problem 1, 3 runs per sideA separate process held GPU memory so the server saw 24.00 GiB free. Both sides run this branch;
gpuis the default.With Flash-Next, both sides produced the same completion token counts in each run (821, 778, 681).
The client sends one streamed
/v1/chat/completionsrequest per run:TTFT on this short prompt, gpu vs host: Qwen3.6 1.74 / 1.22 / 1.22 s vs 1.71 / 1.22 / 1.22 s; Flash-Next 3.02 / 1.11 / 0.81 s vs 3.02 / 0.83 / 0.81 s.
Lookup time, host vs GPU table
Measured inside a CUDA graph, in microseconds per lookup (host / GPU):
A decode-sized lookup (1 to 16 rows) costs 1.4 to 3.5 us more. An 8192-row prefill lookup costs 1.0 to 1.6 ms more. Long-prompt TTFT was not measured end to end.
Correctness
Host and GPU tables give bit-identical rows, eager and after CUDA graph replay with new ids. This covers bf16 tables with int32 and int64 ids, the TP masked variant on a half-vocab shard, and a Q4_0 GGUF table decoded to bf16 and fp32.
With the first commit rebased onto 20d5d93 (main with the NVFP4
get_rowscase), a pinned NVFP4 GGUF table gives rows bit-identical to the same table on the GPU and to gguf-py's dequantize. This covers [64, 320], [32768, 2048] and [16384, 2560] tables, bf16 and fp32 output, eager and after CUDA graph replay with new ids.Table selection was checked on 21 local checkpoints built on the meta device: qwen3_moe, qwen3_5, qwen4_exp (HF and GGUF), gpt_oss, DSV4, minimax_m2, muse_glimmer, GLM-4.7, and gemma4 (HF and GGUF). The config-based tie check gives the same keys, or the same refusal, as a walk of the actual LM-head wiring. The one difference is GLM-4.7 (DF11), which now gets the DF11 message instead of the tied one.
--embed-weights hoston Qwen/Qwen3.5-2B (tied embeddings) stops at start-up with aValueErrorthat says the table is tied to the LM head.Tests
The end-to-end and lookup numbers were measured with the first commit on c63fc13, before rebasing onto 7974c9c. The three commits in between touch GGUF mmproj vision input, two tests, and block-fp8 MoE decode. After the second commit, a Qwen3.6-35B-A3B start with
hostat 24 GiB free resolved the same 2778 expert cache slots. The tests above ran on the head of this branch.ROCm
@LZ-QWQ validated this PR on R9700 (gfx1201) and W7900D (gfx1100) with ROCm 10.1.0 (comment):
gpuandhost.hostreduced resident-weight VRAM by about 1.19 GiB and added 260 expert cache slots on both devices.Decode tok/s with Qwen3.8-Flash-Next-FP8, 256 input / 128 output tokens, median of six measurements:
--embed-weights gpu--embed-weights hostKnown issues
hostfrees that VRAM after loading, but does not keep the table out of VRAM during loading.freetoken-kernel-cachehas to ship from the same build. A cache with a different git stamp is already rejected, but a dev build without a stamp only compares the release number and can load an older cache;--embed-weights hostthen fails at the first lookup with a device-check error.gpuis unaffected.Not covered
--dummy-weightwithhostend to end.token_embdend to end: no local checkpoint has one.