Conversation
|
This pull request has merge conflicts that must be resolved before it can be |
…d Llama compressed-tensors supports weight-only WNA16-INT quantization of the input embedding (CompressedTensorsEmbeddingWNA16Int, added in vllm-project#44340), but a VocabParallelEmbedding only consults the quant config when the model passes `quant_config` (and, for name-based targets, `prefix`) to it. - GPTNeoX passed neither, so a checkpoint with a quantized `embed_in` silently fell back to an unquantized embedding and failed to load with `KeyError: 'embed_in.weight_packed'`. - Llama passed `quant_config` but not `prefix`, so name-based targets (e.g. `re:.*embed_tokens$`) could not match (layer_name was empty) and hit the same silent fallback / `KeyError: 'embed_tokens.weight_packed'`. Pass `quant_config` and `prefix` to both input embeddings so quantized embeddings dispatch correctly. Verified end-to-end in vLLM with llm-compressor WNA16 embedding checkpoints (pythia-1.4b, Mistral-7B-v0.1): both load and generate coherently; accuracy impact is negligible. Signed-off-by: Karthik Kothuri <karthikkothuri2009@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Loads a tiny GPTNeoX checkpoint whose `embed_in` is WNA16-INT quantized and asserts it dispatches to CompressedTensorsEmbeddingWNA16Int, plus a generation smoke test. Guards the model-side quant_config/prefix plumbing (a missing embedding scheme silently falls back to unquantized and fails to load). Signed-off-by: Karthik Kothuri <karthikkothuri2009@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A compressed-tensors WNA16-INT VocabParallelEmbedding could be used for the input lookup but not as a tied output head: the embedding scheme's apply() raised NotImplementedError, and the tie paths assumed a plain .weight tensor that a packed embedding does not expose. For tied models (common in small SOTA models like Qwen3-0.6B and LFM2.5-350M, where the embedding/lm_head dominates size) untying to work around this materializes a full fp16 head and inflates the checkpoint instead of shrinking it. Quantize the single shared matrix and reuse it for both paths: - compressed_tensors_embedding: implement apply() (dequantize the packed table and run F.linear) so the quantized embedding serves the logits matmul. - vocab_parallel_embedding.tie_weights(): when the embedding exposes no plain weight (packed/quantized), return the embedding module directly, mirroring the existing GGUF path. Fixes AttributeError for the tie_weights() idiom. - gpt_neox: route the tie through tie_weights() so a quantized embed_in can be tied to embed_out. - lfm2: pass quant_config/prefix to embed_tokens so its embedding is quantizable. Validated with lm-eval (arc_easy, wikitext) on Qwen3-0.6B and LFM2.5-350M: tied W8 embedding is near-lossless (bpb +0.0%/+0.45%); W4 is heavier on these compression-sensitive models (+1.2%/+4.4%). Adds a tied-embedding regression test alongside the existing dispatch test. AI assistance (Claude) was used for this change. Co-authored-by: Claude Signed-off-by: Karthik Kothuri <karthikkothuri2009@gmail.com>
Thread quant_config and prefix into the input-embedding VocabParallelEmbedding constructions across the model zoo so compressed-tensors WNA16 embedding quantization works uniformly, extending the pattern already applied to gpt_neox/llama/lfm2. Covers 77 word-embedding sites: dense and MoE decoders, MTP/eagle draft models (which source quant_config from vllm_config), and the BERT/RoBERTa/ModernBERT encoder families (threaded through their embedding helper classes and applied to the word/token embedding only). Position/token-type embeddings, CLIP/SigLIP vision embeddings, and non-token embeddings (gemma3n modality embedder, qwen3_dspark markov head) are intentionally left unquantized. Passing quant_config is inert unless the checkpoint targets the embedding, so unquantized loads are unchanged. Signed-off-by: Karthik Kothuri <karthikkothuri2009@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
…ply dtype Addresses kylesayrs review on vllm-project#45535: - Move the tied-lm_head short-circuit into CompressedTensorsEmbeddingWNA16Int.tie_weights (overriding the base) and dispatch ParallelLMHead.tie_weights on the embedding's quant method, instead of special-casing a missing `.weight` in ParallelLMHead. - Register the full-table row-id arange as a non-persistent buffer in create_weights so apply() no longer rebuilds it each call. - Dequantize the table directly into x's dtype in apply() (drops a full-table .to(x.dtype) copy). Signed-off-by: Karthik Kothuri <karthikkothuri2009@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
The review-fix dispatch on embed_tokens.quant_method AttributeErrors when the embedding is a PPMissingLayer placeholder (pipeline-parallel rank without the embedding, where the tie still runs on the last rank). Return the placeholder untouched in that case, as the previous `.weight` short-circuit did. Signed-off-by: Karthik Kothuri <karthikkothuri2009@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
New draft model landed upstream after the initial sweep; plumb its embed_tokens the same way as the other draft models. Signed-off-by: Karthik Kothuri <karthikkothuri2009@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
When a compressed-tensors WNA16 embedding is reused as a tied lm_head, apply() previously dequantized the entire packed table into a dense weight every call and ran F.linear. Reuse vLLM's existing WNA16 Linear kernel (Marlin/Machete) for the logits matmul instead: the embedding's packed weight is already in the compressed-tensors layout those kernels expect, so process_weights_after_loading sets up the fused dequant-GEMM (only for tied embeddings, flagged in tie_weights) while keeping a gather-format copy for the input-lookup path. Falls back to a full-table dequant (no index tensor) + F.linear when no fused kernel is available. Numerically matches the dequant reference (max rel diff 0.025%, fp16). Perf on an RTX 5080 (sm120), Qwen3-scale vocab 151936 x 1024 W4-g64, us/call: M=1 current 1329 no-gather 1317 fused(Marlin) 240 -> 5.5x M=32 current 1703 no-gather 2395 fused(Marlin) 619 -> 2.8x M=256 current 3247 no-gather 2460 fused(Marlin) 2266 -> 1.4x Biggest win at decode (M=1), the common serving case for logits. Signed-off-by: Karthik Kothuri <karthikkothuri2009@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
8d45037 to
09f35c2
Compare
|
Drive-by data from an unrelated Turing (sm_75) experiment on the same tied- 1. M=1 is purely bandwidth-bound. On an RTX 2060 Mobile (250 GB/s achievable, 336 GB/s spec) the fp16 logits GEMV for Qwen2.5-Coder-3B (151936 × 2048, tied) runs 622 MB / 2.51 ms = 248 GB/s, 99% of roofline. Quantizing the weight to int8 gave 2.88 → 1.42 ms = 2.03x, i.e. 98% of the half-bandwidth floor. So at M=1 the only lever is bytes moved, and 2x is the ceiling for a W8 path. That makes the 8.7x in your W8-g128 M=1 row look like it's mostly removing a redundant pass (dequantize-to-dense then read) rather than kernel speed. Worth saying explicitly — it sets the expectation that the fused path is now at the memory floor. 2. Max-relative-diff understates argmax movement, and there's a nearly-free fix. Greedy decoding only cares about argmax, and one flip changes every later token. On 2632 captured decode states, int8 per-row symmetric gave 99.658% top-1 agreement (~1 flip per 292 tokens). Caveat: at 646 states I measured 646/646 and nearly called it lossless — the rate only shows up at a few thousand states. Cheap fix, applicable to any weight-only
Residual disagreements are all cases where the fp16 reference's own top1–top2 margin is ~0. K > 4 buys nothing. 3. Methodology note: a fp16-vs-fp16 control (same build, seed, Context: this came from adding runtime int8 quantization of an unquantized tied |
|
This pull request has merge conflicts that must be resolved before it can be |
Purpose
Follow-up to #45535 (compressed-tensors WNA16 input embeddings + tied
lm_head). This PR speeds up the logits matmul when a quantized embedding is reused as a tiedlm_head.In #45535,
CompressedTensorsEmbeddingWNA16Int.apply()dequantizes the entire packed[vocab, hidden]table into a dense weight on every call and runsF.linear. This PR routes that matmul through vLLM's existing WNA16 Linear kernel (Marlin/Machete) instead — a fused dequant-GEMM that never materializes the dense weight.Key insight: the embedding's packed weight is already in the compressed-tensors layout those kernels expect (packed along the input/hidden dim, identical to a WNA16 Linear), so
process_weights_after_loadingsets up the fused kernel directly. It's set up only for tied embeddings (flagged intie_weights), keeping a gather-format copy for the input-lookup path. When no fused kernel is available it falls back to a full-table dequant (no index tensor) +F.linear.Note
Stacked on #45535 — the tied-embedding
apply()this optimizes only exists there, so the net-new change here iscompressed_tensors_embedding.py; the rest of the diff belongs to #45535. Please review/merge after #45535. Kept as a separate PR to avoid growing the (already large) plumbing PR.Not a duplicate: quantized-
lm_head-via-Marlin already exists for the untied / separately quantized case — aParallelLMHeadindependently quantized as a GPTQ/AWQ/compressed-tensors Linear already dispatches to Marlin (cf. #40999). This PR covers the distinct tied case, where the embedding is thelm_headand its quant method is the embedding method (CompressedTensorsEmbeddingWNA16Int), not a Linear method. The open ModelOpt lm_head/embedding PRs (#35660, #42791, #44671, #41000) target a different backend (NVFP4/FP8 inmodelopt.py) and address loading/dispatch, not this fused tied-logits path. No open PR touchescompressed_tensors_embedding.py.Correctness
The fused path matches the dequant reference numerically (max relative diff 0.025%, fp16). The existing tied fixture test (
tests/quantization/test_quantized_embedding.py::test_tied_quantized_embedding) exercises the fused Marlin path end-to-end and generates coherently.Perf evals
RTX 5080 (sm120, CUDA 13),
apply()microbenchmark, µs/call (lower is better):F.linear)Biggest win at decode (
M=1), the common serving case for logits. The no-gather dequant (kept as the fallback path) is marginal versus the current dequant, as expected — the fused GEMM is the real lever.Test Plan
Test Result
MarlinLinearKernelfor the logits path (verifiedlogits_kernelis set and gather copies are preserved).pre-commit(ruff + mypy) clean on the changed file.This change was developed with AI assistance (Claude Code). All changed lines were reviewed by the submitter.