Proposed gguf support - #24
Atomic-Germ wants to merge 35 commits into
Conversation
- Phase 0: open_kernels/gguf_pool.py — GGUF-native pool reference (pack_q4_pool/pack_q8_pool with f32-scale chunks, q4 band law + pack_q8_pool_lmhead/dequant_pool_lmhead supertile law, minimal GGUF parser read_gguf handling v2+v3); self-test passes bit-exact (dequant(pool) == dequant(GGUF blocks)). - Phase 1: kernel f32-scale variants — open_kernels/designs/gemv_q4/gemv_q4.h (GEMV_Q4_SCALES_F32, GEMV_Q4_PREFIX, conditional kDBytes/kMetaBytes/kTileBytes, load_scale), gemv_q4.py (GEMV_SCALES_F32 env, SYM=gemv_q4s32, srchash includes flag); lm_head_q8.h/.cc/.py (LMHEAD_SCALES_F32, LMHEAD_Q8_PREFIX, load_scale integer-RNE version); lm_head_q4.py; wrapper .cc files (layer_x gemv_q4_gy/gup/gdown, dense gemv_q4_gy/gms) use GEMV_Q4_WRAP(GEMV_Q4_PREFIX, name) two-level macro (## doesn't expand macro operands — two-level pattern required). All builds verified: dx (both modes), lm_head_q4 (both), lm_head_q8 (both), gemv_q4 standalone. - Recipes: open_kernels/recipes/qwen36moe.py (chunk_bytes()/q8_chunk_bytes(), q4_bytes/q4_chunks/band_bytes quant param, CHUNK_F32=6144, Q8_CHUNK_F32=9216); dense.py (accepts quant "q4_1_f32", std_perm_gguf pack ops, builds() adds dx_f32/lm_head_q4_f32, programs() emits out["gguf"] twin section, dx_set/lm_set naming); dx.py (GEMV_SCALES_F32 env replaces spec via dataclasses.replace, F32/CH/GEMV_DEF/GYSYM/GMSYM); manifest.py emits complete nested m["gguf"] = manifest(replace(spec, quant="q4_1_f32")). - Phase 2: src/open_qwen36/weight_file.hpp (WeightFile base + exact fp16_to_f32), gguf_file.hpp/.cpp (GGUF v2/v3 reader, TensorInfo, kv accessors, embed_row for F32/F16/BF16/Q4_0/Q4_1/Q8_0/Q4_K_S/Q4_K_M/Q6_K, HF→GGUF name map gguf_name()), Q4nxFile inherits WeightFile; pools.cpp std_perm_gguf (block gather + nibble transpose + exact f32 widen; scale at 4*(kb*32+r), m at 1024+, codes at 2048+; p1 = p0 + 256 for i+16 values) and pack_norm (GGUF f32/f16→bf16 RNE) + put op auto-routes GGUF; manifest.cpp accepts std_perm_gguf and nested gguf manifest; core.cpp/core.hpp — weight selection (model.q4nx preferred over model.gguf when both exist), w_ weights view, derive_config() GGUF-KV→config.json (llama-family arch keys), embed_row per token. - Phase 4 (dense): HF-name map in GgufFile (embed_tokens→token_embd, lm_head→output, blk.N layers attn/ffn/norm renames) tested end-to-end in gguf_pack_test. - Phase 5: flm-add GGUF mode — GGUF_FILES, GGUF_QUANT_PREFERENCE = ["Q4_1", "Q4_0", "Q8_0"], gguf_quant_of()/choose_gguf_file() (multi-part refusal), fetch_assets/copy_from_dir gguf mapping (chosen gguf → model.gguf), main() auto-detect + dry-run display + details.weights = "gguf"; verified dry-run (picked Peach Q4_1, refused IQ4_XS with message) and real install (files list ['model.gguf','tokenizer.json','tokenizer_config.json']). - Validation: src/open_qwen36/gguf_pack_test.cpp (GGUF-PACK ctest) — synthetic GGUF → std_perm_gguf → bit-exact dequant; byte-identical to python packer on synthetic AND on real cached Peach Q4_1 (mradermacher/Peach-2.0-9B-8k-Roleplay-i1-GGUF, blk.0.attn_k.weight, 196608 B). All NPU hardware runs: gemv_q4 f32 PASS (cos=1.0, maxrel 3.9e-6, nbad=0, parity with q4nx); lm_head_q8 q4nx-synthetic PASS (4.5e-6); lm_head_q8 f32 random FAIL (see known issue). Fixtures regenerated; pytest 65 passed/1 skipped; ctest 2/2 pass (GGUF-PACK, OPEN-MANIFEST). - Toolchain set up: ironvenv via uv (Python 3.13), requirements installed, XRT at /opt/xilinx/xrt.
…lities since it's a linux-targeted dev tool
|
Haha, I had a branch almost ready, but let's keep it one branch :) |
|
Added some comments just based on a Claude review (which again is based on my branch) |
# Conflicts: # open_kernels/designs/dense/dx.py # open_kernels/designs/lm_head_q8/lm_head_q8.h # open_kernels/designs/lm_head_q8/lm_head_q8.py # open_kernels/recipes/dense.py # src/open_qwen36/manifest.cpp # src/open_qwen36/pools.cpp # src/open_qwen36/pools.hpp # utilities/flm-add/flm_add/__init__.py
The merge left raw()'s opening brace unclosed before the q8 chunk-size constants and helper functions, so the helpers nested inside raw's body and the Q8_CHUNK/Q4_CHUNK/code_index/bf16_* symbols were out of scope. Move the raw() body above the helpers so it compiles.
…rnels in build-all gemv_q4_gy.cc defined its symbol literally, breaking the LMHEAD_SCALES_F32 f32 build which expects gemv_q4s32_gy (undefined symbol at link). Use the shared GEMV_Q4_PREFIX macro like dense/gemv_q4_gy.cc so both variants emit. build-all.sh step-0 heal now also chowns open_kernels (kernel-export final.prj trees can be left root-owned by a prior sudo'd export).
|
I'm not sure what it will take to get the simplified flow working on windows also, but it probably already does. This wasn't really a deliberate out-of-scope addition, but I really did need it in order to work on the rest along the way:
I've also merged main back in since a lot happened since. |
|
it's all good. I'll try windows soon and get a PR going if it's no bueno. |
|
I am working on fixing the conflicts... |
|
(Or, Claude is!) |
…ename) main moved under this branch four times: the FLM -> OFLM rename and the preset build (#47), the GEMM-block prefill route (#39), the VLM / M-RoPE path (#45), and a local lm_head kernel TU. Each collided with the WeightFile seam, which is the one thing this branch is about. The rule applied throughout: take main's side, which carries the new work, and route it through the weights view. `w_` is `man_` for a .q4nx and the GGUF-derived manifest for a GGUF, so code that reads geometry, tensor names or the tail off `man_` reads the wrong manifest the moment the model is a GGUF -- silently, because both are valid manifests. Eighteen such sites in main's new code now go through `w_`. Two that were not mechanical: * `core.cpp` kept #24's GGUF config derivation (a GGUF needs no config.json -- the metadata carries it) beside main's image-token / mrope_section parsing, and #24's verbose embed check beside main's image-embedding path. * `lm_head_q4.py` kept main's locally GENERATED kernel TU -- designs/layer_x's copy is per-spec and gets removed again, which broke Gemma's lm_head build on 2026-09-08 -- and gave it #24's GEMV_Q4_PREFIX wrapping, so the f32-scale build still resolves gemv_q4s32_gy. The prefix is a compile flag, so one generated source serves both and SCALES_F32 stays in the source hash itself. `pools.cpp` keeps the source-agnostic `raw(const WeightFile&)`; main's `raw(const Q4nxFile&)` is subsumed, both file types deriving from WeightFile. `README.md` and `src/CMakeLists.txt` take main's side entirely -- main had already absorbed this branch's build-from-source rewrite under the new name -- except `open_qwen36/gguf_file.cpp`, which is re-added to the engine sources. `build.cmd` gains it too, and now runs gguf_pack_test. Verified: manifest_test, pools_test (Q4_K transcode and the mixed pools included) and gguf_pack_test all PASS, and open_qwen36_cli links, built with MSVC against the system XRT. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCT7r8ebHia4kGAWsaKc3Y
|
Merged main had moved under it four times: the FLM→OFLM rename and the preset build (#47), the GEMM-block prefill route (#39), the VLM / M-RoPE path (#45), and a local The rule throughout: take main's side, then route it through the weights view. Two that weren't mechanical:
Verified on Windows/MSVC against the system XRT: — Claude, on behalf of @vegah |
|
@Atomic-Germ Merge if it is okay, if not - just remove the latest commit. |
| // scales widen EXACTLY to f32 (open_kernels/gguf_pool.py). Zero loss. | ||
| const std::string name = with_layer(op.tensor, layer); | ||
| if (op.nch == 0 || op.in_dim == 0) fail("std_perm_gguf " + name + " without nch / in_dim"); | ||
| const GgufFile& g = dynamic_cast<const GgufFile&>(m); |
There was a problem hiding this comment.
Minor architectural worry: this makes the packing interpreter format-aware, and pools.cpp is the one file whose
byte-exactness is the safety property — mirrored in NumPy by recipes/pack.py and frozen against legacy_pools.py by
test_pack_plan.py. A new std_perm_gguf op plus a downcast means both interpreters now have to be kept in lockstep
across two layouts.
The alternative is to have the source hand back bytes already in the chunk layout, so std_perm stays one op and
pools.cpp never learns what a GGUF is. That's what I did on my branch and it kept pools.cpp to +21/−15 with no
arithmetic touched — happy to show it if useful, though it presumes the single-layout answer in comment 2.
There was a problem hiding this comment.
I'm definitely interested in that, at the very least we need to be looking at alternative methods of accomplishing this just in case there's an altogether better way we're missing.
There was a problem hiding this comment.
Happy to. It is branch gguf-runtime off 0fe7922 (PR #21), four commits, never pushed — say the word and I will put it up.
The seam is one header, weight_source.hpp. Three methods — raw(name), embed_row(...), drop_pages() — and one contract:
a source hands back, per tensor, EXACTLY the bytes
model.q4nxwould have held for it, in the file's raster order (chunkfcovers rows32*(f/ncol), cols256*(f%ncol)).
Q4nxFile satisfies that by pointing into its mapping; a GGUF source satisfies it by building the bytes. raw() returns a value rather than a pointer — TensorBytes, which is borrowed / solely owned / shared with a cache and looks the same to the caller — because bytes that had to be built need an owner and only the caller knows how long it needs them.
pools.cpp is then +21/−15, and all of it is two mechanical shapes. No op is added, std_perm stays one op, and no arithmetic is touched:
-const uint8_t* raw(const Q4nxFile& m, const std::string& name, size_t need, size_t* got = nullptr) {
- size_t n = 0;
- const uint8_t* p = m.raw(name, &n);
+TensorBytes raw(const WeightSource& m, const std::string& name, size_t need, size_t* got = nullptr) {
+ TensorBytes b = m.raw(name);
+ const size_t n = b.size();
if (n < need) fail(name + " is " + std::to_string(n) + " B, the plan needs " + std::to_string(need));
if (got) *got = n;
- return p;
+ return b;
}
-void apply(const PackOp& op, const Q4nxFile& m, int layer, uint8_t* dst, size_t dst_bytes, size_t ch) {
+void apply(const PackOp& op, const WeightSource& m, int layer, uint8_t* dst, size_t dst_bytes, size_t ch) {
if (op.op == "std_perm") {
...
- const uint8_t* src = raw(m, name, (op.chunk0 + op.nch) * ch) + op.chunk0 * ch;
+ const TensorBytes srcb = raw(m, name, (op.chunk0 + op.nch) * ch);
+ const uint8_t* src = srcb.data() + op.chunk0 * ch;
auto perm = std_perm(op.nch, op.in_dim);
for (size_t c = 0; c < op.nch; ++c) std::memcpy(dst + op.dst + c * ch, src + perm[c] * ch, ch);The other five ops (expert_stripes, expert_down, put, lmhead_q8, conv_transpose) are each the same two lines, and pack_pool / pack_consts / pack_lmhead are signature-only. pools.hpp swaps its q4nx_file.hpp include for weight_source.hpp.
The format work does not disappear, it moves — gguf_repack.cpp (281 lines: the block formats to raster chunks, Q4_K included), gguf_source.cpp (214: the WeightSource), gguf_file.cpp (364: the container). That is more total lines than this PR's. The trade is only about which file they are in: none of them is the one recipes/pack.py mirrors and test_pack_plan.py freezes.
Two things I should be straight about:
- It is based on
0fe7922and predates the OFLM rename (Cmake full build system #47), the GEMM-block prefill route (Open engine: batched prefill as a whole-array GEMM (1.95x TTFT) #39) and the VLM path (Open kernels: fast attention on every family, and images through the open engine (#16) #45), so it needs a rebase before it reads as a diff against today'smain. I have not done that rebase. - Its byte-exactness evidence is
specs/open-engine/tests/test_gguf_pack.py. Thetest_law_*half is self-contained — random blocks, no model, no network — and pins the NumPy statement of the law against bothopen_kernels/q4_1_pack.pyandgguf_repack.cpp, so a change to any one is caught by the other two. Thetest_model_*half needs a real GGUF and skips without one; it is not currently run anywhere, and I have not run it against the current tree.
And to be explicit about the dependency I flagged: this shape presumes comment 2's single-layout answer. If the f32-scale plane stays, the source has two output layouts to produce and the seam has to carry which one — still outside pools.cpp, but no longer free.
— Claude, on behalf of @vegah
There was a problem hiding this comment.
After way too much thought; you're definitely right.
|
I was trying to make sure my code was going to agree with this, and opus uncovered something: I think the Q4_0 path is off by a constant. A GGUF Q4_0 block stores nibbles 0-15 that stand for -8 to 7, so llama.cpp's The kernel already computes Why the tests don't catch it: One other thing worth flagging while you're in here. "Q4_0" now means two |
…mma tensor naming and GGUF-derived vocab/window fields are fixed, EmbeddingGemma queries the correct tensor, and the installer now has 12 focused GGUF tests including ModelScope and Q4NX precedence.
…weights and executes on the NPU
All new code is additive — no frontend touched, main build glob picks the sources up automatically. Ported (all MIT): - `src/include/AutoModel/model_backend.hpp` + `src/common/AutoModel/model_backend.cpp` supported_backends / resolve_backend_id with --backend → OFLM_BACKEND (FLM fallback via getenv_oflm) → catalog → default precedence. GGUF-vs-Q4NX dual path instead of the _shared_select_open_engine per-frontend hack. - `src/include/AutoModel/oflm_npu_backend.hpp` + `src/common/AutoModel/builtin_backends.cpp` - the shared Q4NX-loading NPU backend template, registered for every family incl. our gemma4-12b. flm_npu registered as a legacy alias so shared catalogs keep resolving. Granite (open-only) and qwen3.5-omni (not a causal_lm) noted in comments as intentionally unregistered. - `src/include/utils/npu_platform.hpp` + `src/common/npu_platform.cpp` aie2p/aie4 detection via column count (XRT platform query → amdxdna ioctl fallback, OFLM_PLATFORM override). Directly useful for the family-xclbin distribution plan. - `src/include/utils/file_access.hpp` - tiny mmap-observer seam their GGUF loader uses; no dependency yet. - Tests: `src/test/model_backend/` (10 tests, ported) and `src/test/npu_platform/` (helpers-only) — both standalone CMake, no NPU needed. All pass.
… per-file {url, revision} pins with full table validation (unknown keys,
non-string/empty URL, non-40-hex revision, wrong arity), percent-encoded filenames, and a hard error for --modelscope on pinned entries. One fix on the
way over: a non-object file_sources (catalog typo) now throws instead of being silently ignored.
- model_info_key record lookup + explicit-sha256 record support — GGUF repos carry sha256 outside LFS metadata, and per-generation artifacts can share one
tag.
- download_file_atomic + DownloadRequest (download_model.*) — resume-from-.part, size+hash verify, atomic promote. download_multiple_files routes entries
carrying expected_size/hash_algorithm through it and keeps the legacy path for everything else, so no other producer of that JSON breaks.
- The strict gate keys off the pinning itself (uses_pinned_sources: non-empty file_sources object), not chiza's corelib_aie4_gguf backend id. Any pinned
model — GGUF or otherwise — gets revision-pinned URLs and verified downloads; the existing 44 catalog entries behave bit-identically (verified: only
embed-gemma:300m's in-flight model.gguf lacks a record, and it keeps its old skip behavior outside the pull path).
- Verify is two-regime: pinned = strict (size+hash, delete corrupt, fail); unpinned = your existing advisory behavior, untouched.
- --force actually works now: build_download_list takes force_redownload (previously a forced pull with a complete tree downloaded nothing and reported
success). Also fixed a latent arg-order bug where the Outdated branch passed sub_process_mode into the use_modelscope slot, and wired up the dead
missing_from_manifest accounting so a model_list/model_info gap fails loudly as the comment always intended.
Co-authored-by: Cyrus Attoun <josh.attoun@sviworld.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cyrus Attoun <josh.attoun@sviworld.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cyrus Attoun <josh.attoun@sviworld.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Take proposed-gguf-support as the authority on GGUF: GGUF-direct dense-family loading, f32-scale twin kernels (q4_1_f32, 6144 B chunks), std_perm_gguf, WeightFile, nested gguf manifest section, q4nx-precedence, K-quant refusal, oflm-add GGUF installer, downloader pinning. feat/gguf keeps its speed/families work: qwen36moe recipe, batched prefill, deepstack/vision modeling, open-engine fixtures, buildall/export scripts. Merged engine (src/open_qwen36): step_embed deepstack + embedding_row, unified WeightFile/q4nx pools with std_perm_gguf std_fuse branch, need_all pack ops, GGUF-PACK/OPEN-VISION tests in CMake. Kept both oflm-add junction fallback and vision.json sidecar; dropped flm engine lookup (all-oflm). Dropped duplicate utilities/gguf-inspect in favor of utilities/gguf-utils/. Untracked generated layer_x/gemv_q4_gms.cc. Fixtures regenerated from merged recipes; 559 spec tests pass.
…scales) Generated deterministically by gemv_q4.py under GEMV_SCALES_F32=1 with the standard qkv geometry, alongside the already-tracked gemv_q4s32_p4b16r2 set.
…lfm2 re-exports dense chunk geometry The full fedora-default export now passes all 12 open_kernels specs plus the BERT set. dx_attn.py (the attention-only prefill dispatch) picks up dx.py's ATTN_QKV_BIAS abias stream and ATTN_PTAB_SPLIT meta block verbatim, which was blocking Qwen2.5-3B (biased q/k/v at 2 KV heads / hd 128, a two-element position record); non-bias single-record families keep the byte-identical paths. lfm2.py re-exports band_bytes/chunk_bytes off .dense like dense.py does via .qwen36moe, which dx.py's calls require on any family module.
As in #14 & #33 and also #9 tangentially
This isn't complete, but it's getting there