Add packed ternary GGUF export for CAT-Q - #2
Merged
Conversation
* Add packed ternary GGUF export for CAT-Q checkpoints
The Hugging Face export writes a fake-quantized model: the weights are
ternary, but each one still occupies 16 bits and runs through ordinary
floating-point matrix multiplication.
Add a second export path that stores the quantized projections as real
2-bit weights. CAT-Q quantizes a group as W_g = s_g * T_g with
T_g in {-1, 0, +1} and |g| = 128, which is exactly the `Q2_0` block type
of the Bonsai llama.cpp fork (one fp16 scale plus 128 two-bit codes,
34 bytes, 2.125 bits per weight), so the codes and scales are taken
straight out of the quantizer and packed without re-estimating anything.
`quantize/ternary_export.py` extracts the two factors while the merge is
still holding them and checks that they reproduce the fake-quantized
weight exactly; `deployment/gguf_export.py` packs them and writes the
GGUF. Conversion depends only on the `gguf` package - it needs no
llama.cpp checkout and produces no intermediate model.
Dense (Qwen3, LLaMA) and MoE (Qwen3-MoE) checkpoints are supported.
Expert projections are packed and stacked into the `ffn_*_exps` tensors
the runtime expects; embeddings, norms, the LM head and MoE routers stay
in floating point, as they are outside the quantized set.
Usage is `./export_gguf.sh`, mirroring `export_model.sh`, or
`main.py --export_gguf_path`; `deployment/README.md` covers building the
runtime and serving the result.
Verified against stock llama.cpp conversions: metadata and vocabulary
match for Qwen3-4B, Qwen3-30B-A3B and Llama-2-7B, and every tensor of a
Qwen3-4B export is byte-identical to a previously validated model.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Highlight checkpoint scale range and ternary deployment in news
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Pick up upstream README news and note ternary deployment
Take the latest Latest News block from the public repository and re-apply
the packed ternary deployment mention on top of it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Align CAT-Q news block with the top-level README
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exporting Qwen3-235B-A22B needs the 16-bit model and the packed result resident at the same time, which does not fit on a 512 GiB host. --gguf_low_memory drops every weight as soon as it has been packed, skips folding the merged floats back into the memory-mapped shards (which only faulted in private copies of pages the export no longer reads), spools the GGUF through a temporary file and pins the glibc mmap threshold so freed decoder layers are returned to the kernel. Host memory then stays close to the size of the packed model and the resulting file is byte-for-byte identical to a default export. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The public repository carries the same CAT-Q release plus three edits made there directly: the logo rename, the news/wording pass on the root README and a one-line rewording in the CAT-Q README. All three are kept, with the CAT-Q entries updated to mention the packed ternary deployment code that this branch adds. The conflicting sources were add/add conflicts against the older public copy of the same files, so the private versions are taken unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.
Released CAT-Q checkpoints only shipped the learnable parameters, and the one
export path available produced a fake-quantized Hugging Face model: the values
were ternary but every weight still occupied 16 bits. This adds a real export,
so a checkpoint can be turned into a deployable file where each quantized weight
takes 2 bits.
What is added
main.py --export_gguf_pathwrites the restored checkpoint straight to a singleGGUF file. Quantized projections are stored as
Q2_0, the group-128 ternaryweight type of the Bonsai
llama.cpp fork (34 bytes per 128 weights, 2.125 bits per weight). Everything the
CAT-Q recipe leaves in floating point - token embeddings, LM head, norms and the
MoE router - is written exactly as a stock llama.cpp conversion would.
quantize/ternary_export.pyquantize/q2_0.pyQ2_0block layout and packingquantize/gguf_export.pyexport_gguf.shexport_model.shdeployment/README.mdExporting stays a pure CAT-Q step: it reads the checkpoint and writes the GGUF
with no intermediate model and no inference runtime involved. Beyond the current
requirements it only needs the
ggufpackage. llama.cpp is needed to run theresult, not to build it.
Dense (Qwen3, LLaMA) and MoE (Qwen3-MoE) checkpoints are both supported. For MoE
models the per-expert
gate_proj/up_proj/down_projweights are packed andstacked into the
ffn_*_expstensors the runtime expects.Correctness
ternary_export.extract_ternarydoes not re-estimate anything from the mergedfloat weights: it re-runs the quantizer and keeps the two factors it works with,
then asserts that
scale * ternaryreproducesquantizer(weight)exactly beforethe tensor is handed on. A packed export is therefore numerically identical to
the fake-quantized model it replaces.
tests/test_ternary_export.py(220 lines) covers the round trip, theQ2_0bitlayout against the reference dequantization, the LLaMA q/k row permutation and
the MoE expert stacking.
Large models
By default the exporter keeps the whole 16-bit model resident while packing,
which needs about the size of the original model plus the size of the GGUF.
Qwen3-235B-A22B does not fit that way on a 512 GiB host, so
--gguf_low_memorydrops each weight as soon as it has been packed and spools the GGUF through a
temporary file.
Two things had to be fixed to make that actually work:
every memory-mapped page the loader handed out, even though the low-memory
path never reads those values again. Those writes are now skipped.
dynamic mmap threshold, after which same-sized allocations came from the heap
and their memory was never returned - about 10 GiB per layer late in a run.
The threshold is now pinned.
Peak host memory for Qwen3-235B-A22B drops from over 500 GiB to roughly 62 GiB,
and the output is byte-for-byte identical to a default export (verified on
Qwen3-1.7B and Qwen3-30B-A3B). The flag cannot be combined with
--tasksor--export_model_path, since the 16-bit weights are gone by the time packing ends.Released files
All eight checkpoints have been exported and uploaded next to their parameters
on IntelLabsChina/CAT-Q:
Testing
python -m compileall -q .python -m unittest discover -s tests -p "test_*.py"- 29 tests pass