llama/ggml: Hadamard weight-fold runtime (prism.hadamard GGUF contract, FWHT to 2048) - #119
llama/ggml: Hadamard weight-fold runtime (prism.hadamard GGUF contract, FWHT to 2048)#119bri-prism wants to merge 10 commits into
Conversation
Register-resident warp kernel handles N up to 2048 for F32/F16 inputs. test-backend-ops MUL_MAT_HADAMARD passes 14/14 on H200 (SM90) vs CPU reference, including H2048 F32/F16.
Load prism.hadamard.* v1 metadata (block size, transform, axis, sign mode, folded weight names), materialize one persistent rotation tensor per (block size, buffer type), and apply x' = Hx immediately before every folded mul_mat / mul_mat_id via the FWHT hint. Identity signs only; unsupported contract values fail loading loudly.
Pick up hadamard_packing.json from the model dir, validate the fold contract (schema v1, identity signs, last-axis), map folded tensor names to GGUF names, and write the prism.hadamard.* keys.
Metal advertises every mul_mat type combo, but the Hadamard-hint fallback for F16 inputs at widths without a dedicated FWHT kernel requests a nonexistent f32 x f16 mul_mv pipeline and crashes on the null pipeline. Decline the op in supports_op so it falls back to CPU instead. test-backend-ops MUL_MAT_HADAMARD: MTL0 13/13 (F16 wide-block case now correctly unsupported), 3/3 backends pass.
With mmap-backed buffer types (Metal shared buffers), weight->buffer is not assigned until load_all_data, so the rotation setup threw 'weight has no buffer' on every folded model. CUDA allocates buffers up front, which masked the ordering bug.
Instantiate the existing simdgroup-register FWHT template for 1024/2048 (F32 and F16 inputs) and extend the supported-size gate, replacing the dense rotation-matmul fallback for wide Hadamard-hint blocks. test-backend-ops MUL_MAT_HADAMARD: MTL0 14/14. Removes most of the Hadamard-hint decode overhead on Apple silicon versus the fallback.
There was a problem hiding this comment.
Pull request overview
Adds runtime support for Hadamard-folded GGUF weights and expands FWHT backend coverage.
Changes:
- Validates and emits the
prism.hadamardGGUF contract. - Injects activation transforms before folded matrix multiplications.
- Adds F16 and wider FWHT kernels with backend tests.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test-backend-ops.cpp | Expands FWHT cases through width 2048 and F16. |
| src/llama-model.h | Stores folded-weight metadata and rotations. |
| src/llama-model.cpp | Validates metadata and creates persistent rotations. |
| src/llama-model-loader.cpp | Enables string-array metadata loading. |
| src/llama-graph.h | Passes rotation mappings into graph construction. |
| src/llama-graph.cpp | Applies transforms before mapped matmuls. |
| src/llama-context.cpp | Supplies model rotations to graph parameters. |
| ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp | Generates F16 FWHT shaders. |
| ggml/src/ggml-vulkan/vulkan-shaders/fwht.comp | Supports F16 FWHT input. |
| ggml/src/ggml-vulkan/ggml-vulkan.cpp | Adds Vulkan F16 FWHT pipelines. |
| ggml/src/ggml-metal/ggml-metal.metal | Adds F16 and 1024/2048 Metal kernels. |
| ggml/src/ggml-metal/ggml-metal-ops.cpp | Dispatches typed Metal FWHT kernels. |
| ggml/src/ggml-metal/ggml-metal-device.m | Rejects unsupported F16 FWHT widths. |
| ggml/src/ggml-metal/ggml-metal-device.h | Defines FWHT pipeline and width interfaces. |
| ggml/src/ggml-metal/ggml-metal-device.cpp | Selects Metal pipelines by input type. |
| ggml/src/ggml-cuda/ggml-cuda.cu | Allows F16 Hadamard inputs on CUDA. |
| ggml/src/ggml-cuda/fwht.cu | Adds typed CUDA kernels through width 2048. |
| ggml/src/ggml-cpu/ops.cpp | Adds CPU F16 FWHT input conversion. |
| ggml/src/ggml-cpu/ggml-cpu.cpp | Advertises CPU F16 Hadamard support. |
| ggml/src/ggml-cpu/ggml-cpu.c | Converts F16 matmul inputs for CPU fallback. |
| conversion/base.py | Imports Hadamard manifests into GGUF metadata. |
Suppressed comments (1)
ggml/src/ggml-vulkan/ggml-vulkan.cpp:5747
- The shared-memory path has the same unconditional F16 pipeline creation. On a Vulkan device where
device->fp16is false, this shader requires a feature/extension that was not enabled and can make backend initialization fail. Gate this variant ondevice->fp16as well.
ggml_vk_create_pipeline(device, device->pipeline_fwht_f16[idx], "fwht_shmem_f16", fwht_shmem_f16_len, fwht_shmem_f16_data, "main", 2, sizeof(vk_op_fwht_push_constants), {1, 1, 1}, { block_size, n }, 1);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| filtered = self.filter_tensors((record["name"], lambda: None)) | ||
| if filtered is None: | ||
| raise ValueError(f"Hadamard tensor is filtered out: {record['name']!r}") | ||
| weight_names.append(self.map_tensor_name(filtered[0])) |
| if (device->subgroup_size <= n) { | ||
| ggml_vk_create_pipeline(device, device->pipeline_fwht_f32[idx], "fwht_f32", fwht_f32_len, fwht_f32_data, "main", 2, sizeof(vk_op_fwht_push_constants), {1, 1, 1}, { device->subgroup_size, n }, 1, true, true, device->subgroup_size); | ||
| ggml_vk_create_pipeline(device, device->pipeline_fwht_f32[idx], "fwht_f32", fwht_f32_len, fwht_f32_data, "main", 2, sizeof(vk_op_fwht_push_constants), {1, 1, 1}, { device->subgroup_size, n }, 1, true, true, device->subgroup_size); | ||
| ggml_vk_create_pipeline(device, device->pipeline_fwht_f16[idx], "fwht_f16", fwht_f16_len, fwht_f16_data, "main", 2, sizeof(vk_op_fwht_push_constants), {1, 1, 1}, { device->subgroup_size, n }, 1, true, true, device->subgroup_size); |
…aths The activation-side transform is applied only where the graph goes through build_lora_mm/build_lora_mm_id, but the converter accepted any mappable tensor and the loader accepted any weight name. Architectures that multiply projection weights with raw ggml_mul_mat (deepseek2 wq_a, kimi-linear, plm) would load a folded GGUF and silently skip the transform. Gate the contract on both ends: the converter refuses archs outside a verified set (llama, qwen3, qwen3moe, qwen35, qwen35moe, qwen3next) and tensors outside an explicit projection-kind allowlist, and the loader enforces the same checks on prism.hadamard.weight_names so folded GGUFs from other tools refuse to load rather than run wrong math.
…fp16 The fwht_f16 shaders declare float16_t, but VK_KHR_shader_float16_int8 is only enabled when device->fp16 is set, so unconditional creation could fail pipeline setup on devices without shader-float16 (or under GGML_VK_DISABLE_F16) even when no F16 FWHT is ever dispatched. Guard both the subgroup and shmem variants; ggml_vk_can_use_fwht already treats the null pipeline as unsupported and falls back to the rotation-matrix matmul.
|
Closing in favour of #121, which is the same work ported onto prism-v7. This targets prism-v6, and the prism-v7 README now marks that branch as a stale mid-migration snapshot that should not be built from, so this cannot land as-is. Nothing is lost. #121 carries all 18 commits from this branch and its follow-up (#120), rebased onto prism-v7, with the Metal FWHT kernels re-landed into the new ggml-metal/kernels layout since v7 moved the shaders out of the monolithic metal file. Verified there: 18/18 MUL_MAT_HADAMARD on CUDA (RTX 4090, 2/2 backends) and 17/17 on Metal (3/3 backends), and a folded checkpoint loads with 402 folded weights plus 1 inverse-lookup table and decodes at the same rate as on this branch. |
What
Runtime support for models whose weights were Hadamard-folded offline, plus the kernel coverage to run them at full speed:
prism.hadamardv1 GGUF metadata contract (block size, transform, axis, sign mode, folded weight names). The loader validates it, materializes one persistent rotation tensor per (block size, buffer type), and applies the activation-side transform immediately before every foldedmul_mat/mul_mat_idthrough the existing FWHT matmul hint. Identity signs only for now; any unsupported contract value refuses to load rather than running wrong math.convert_hf_to_ggufpicks up ahadamard_packing.jsonmanifest from the model directory and writes the contract keys.supports_opso it falls back to CPU), and a loader ordering bug where rotation setup ran before mmap-backed buffers were assigned, which broke every folded model on Metal.Why
Folding
W' = W H^Tinto the weights offline and applyingx' = Hxat runtime preserves the original computation exactly while flattening weight outliers ahead of low-bit quantization. The runtime half needs an explicit, validated metadata contract so a folded GGUF can never be silently loaded by a runtime that skips the activation transform.Validation
test-backend-ops -o MUL_MAT_HADAMARD: 14/14 on CUDA (SM90) and on Metal, against the CPU reference, including 2048-wide F32 and F16 cases.fwhtshader variants compile under glslc.llama-benchon both an H200 and an M5.