Skip to content

Add packed ternary GGUF export for CAT-Q - #2

Merged
genggng merged 8 commits into
IntelChina-AI:mainfrom
genggng:main
Aug 18, 2026
Merged

Add packed ternary GGUF export for CAT-Q#2
genggng merged 8 commits into
IntelChina-AI:mainfrom
genggng:main

Conversation

@genggng

@genggng genggng commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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_path writes the restored checkpoint straight to a single
GGUF file. Quantized projections are stored as Q2_0, the group-128 ternary
weight 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.

File Role
quantize/ternary_export.py Recovers the ternary codes and per-group scales from the quantizer
quantize/q2_0.py The Q2_0 block layout and packing
quantize/gguf_export.py Metadata, vocabulary and tensor writing
export_gguf.sh Launcher matching export_model.sh
deployment/README.md Export-and-serve walkthrough

Exporting 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 gguf package. llama.cpp is needed to run the
result, 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_proj weights are packed and
stacked into the ffn_*_exps tensors the runtime expects.

Correctness

ternary_export.extract_ternary does not re-estimate anything from the merged
float weights: it re-runs the quantizer and keeps the two factors it works with,
then asserts that scale * ternary reproduces quantizer(weight) exactly before
the 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, the Q2_0 bit
layout 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_memory
drops 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:

  • Folding the merged floats back into the model faulted in a private copy of
    every memory-mapped page the loader handed out, even though the low-memory
    path never reads those values again. Those writes are now skipped.
  • Freeing a decoder layer worth of tensors on each iteration grew the glibc
    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 --tasks or
--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:

Model GGUF
Llama2-7B 2.25 GB
Qwen3-1.7B 1.00 GB
Qwen3-4B 1.75 GB
Qwen3-8B 4.34 GB
Qwen3-14B 6.63 GB
Qwen3-32B 11.41 GB
Qwen3-30B-A3B 9.24 GB
Qwen3-235B-A22B 64.80 GB

Testing

  • python -m compileall -q .
  • python -m unittest discover -s tests -p "test_*.py" - 29 tests pass
  • End-to-end export of all eight checkpoints, each loaded by the Bonsai runtime

genggng and others added 8 commits July 23, 2026 13:39
* 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>
@genggng
genggng merged commit 1aca419 into IntelChina-AI:main Aug 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant