Skip to content

llama/ggml: Hadamard weight-fold runtime (prism.hadamard GGUF contract, FWHT to 2048) - #119

Closed
bri-prism wants to merge 10 commits into
prism-v6from
hadamard-gguf-contract
Closed

llama/ggml: Hadamard weight-fold runtime (prism.hadamard GGUF contract, FWHT to 2048)#119
bri-prism wants to merge 10 commits into
prism-v6from
hadamard-gguf-contract

Conversation

@bri-prism

Copy link
Copy Markdown

What

Runtime support for models whose weights were Hadamard-folded offline, plus the kernel coverage to run them at full speed:

  • A prism.hadamard v1 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 folded mul_mat / mul_mat_id through the existing FWHT matmul hint. Identity signs only for now; any unsupported contract value refuses to load rather than running wrong math.
  • FWHT hint kernels extended from 512-wide to 1024/2048-wide blocks on CUDA and Metal. Backends without a matching kernel width fall back to a plain matmul against the real rotation matrix, so results stay correct everywhere.
  • F16 inputs accepted by the FWHT hint on CPU, CUDA, Metal, and Vulkan.
  • convert_hf_to_gguf picks up a hadamard_packing.json manifest from the model directory and writes the contract keys.
  • Two fixes found while validating: a Metal crash when an F16 hint input hits a width without a dedicated FWHT kernel (now declined in supports_op so 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^T into the weights offline and applying x' = Hx at 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.
  • All four Vulkan fwht shader variants compile under glslc.
  • End to end, the activation transform on a folded Q2_0_g128 model costs low single-digit percent decode at batch 1 and is prefill-neutral, measured with llama-bench on both an H200 and an M5.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runtime support for Hadamard-folded GGUF weights and expands FWHT backend coverage.

Changes:

  • Validates and emits the prism.hadamard GGUF 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->fp16 is false, this shader requires a feature/extension that was not enabled and can make backend initialization fail. Gate this variant on device->fp16 as 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.

Comment thread conversion/base.py Outdated
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]))
Comment thread ggml/src/ggml-vulkan/ggml-vulkan.cpp Outdated
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.
@bri-prism

Copy link
Copy Markdown
Author

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.

@bri-prism bri-prism closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants