Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6d25d20
ggml: support F16 input for FWHT matmul hint
bri-prism Aug 19, 2026
98a3706
ggml-cuda: advertise F16 Hadamard inputs
bri-prism Aug 19, 2026
2fa1eae
ggml-cuda: extend FWHT matmul hint to 1024/2048 blocks
bri-prism Aug 20, 2026
9692d67
llama: prism.hadamard GGUF contract + activation-side rotation
bri-prism Aug 20, 2026
8c27714
convert: emit prism.hadamard metadata from packing manifest
bri-prism Aug 20, 2026
f0ad296
ggml-metal: fix crash on F16 Hadamard input at unsupported FWHT widths
bri-prism Aug 20, 2026
0c36912
llama: set up Hadamard rotations after tensor data load
bri-prism Aug 20, 2026
8067209
ggml-metal: add 1024/2048-wide FWHT kernels
bri-prism Aug 20, 2026
f33b3c1
llama, convert: restrict prism.hadamard contract to verified matmul p…
bri-prism Aug 23, 2026
09ca0fd
ggml-vulkan: create F16 FWHT pipelines only when the device supports …
bri-prism Aug 23, 2026
f4a4697
llama: explicit sign vectors for the Hadamard GGUF contract
bri-prism Aug 23, 2026
bd714ee
llama: inverse Hadamard transform for latent embedding tables
bri-prism Aug 23, 2026
86e9022
llama: grouped-order GDN out_proj support for Hadamard-folded models
bri-prism Aug 23, 2026
cc92dd0
convert: match Hadamard-folded out_proj by suffix
bri-prism Aug 23, 2026
2d18730
llama: verify folded weights receive their transform at graph build
bri-prism Aug 23, 2026
ea2fa85
llama: run the Hadamard coverage check before graph scheduling
bri-prism Aug 24, 2026
5c5a462
cuda: fuse the Hadamard sign flip into the FWHT load
bri-prism Aug 24, 2026
ba54da6
llama, convert: allowlist gaps + device-side inverse transforms
bri-prism Aug 24, 2026
c0e4baf
hadamard: fix dead F16 fusion gate, contract validation, and grouped-…
bri-prism Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions conversion/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,144 @@ def map_tensor_name(self, name: str, try_suffixes: Sequence[str] = (".weight", "
raise ValueError(f"Can not map tensor {name!r}")
return new_name

def hadamard_folded_names(self) -> set[str]:
"""Source-tensor names folded under a Hadamard manifest, or empty."""
cached = getattr(self, "_hadamard_folded_names", None)
if cached is not None:
return cached
names: set[str] = set()
manifest_path = self.dir_model / "hadamard_packing.json"
if manifest_path.is_file():
with manifest_path.open("r", encoding="utf-8") as f:
for record in json.load(f).get("tensors", []):
if isinstance(record, dict) and isinstance(record.get("name"), str):
names.add(record["name"])
self._hadamard_folded_names = names
return names

def add_hadamard_metadata(self) -> None:
"""Transfer a packed-checkpoint transform contract into GGUF metadata."""
manifest_path = self.dir_model / "hadamard_packing.json"
if not manifest_path.is_file():
return

with manifest_path.open("r", encoding="utf-8") as f:
manifest = json.load(f)

schema_version = manifest.get("schema_version")
if schema_version not in (1, 2) or manifest.get("kind") != "hadamard-weight-fold":
raise ValueError(f"Unsupported Hadamard manifest: {manifest_path}")
if manifest.get("status") != "requires-matching-runtime":
raise ValueError(f"Unexpected Hadamard manifest status: {manifest.get('status')!r}")

transform = manifest.get("transform")
if not isinstance(transform, dict):
raise ValueError("Hadamard manifest is missing transform metadata")
block_size = transform.get("block_size")
if not isinstance(block_size, int) or block_size <= 0 or block_size & (block_size - 1):
raise ValueError(f"Invalid Hadamard block size: {block_size!r}")
if transform.get("name") != "normalized-signed-sylvester-walsh-hadamard":
raise ValueError(f"Unsupported Hadamard transform: {transform.get('name')!r}")
sign_mode = transform.get("sign_mode")
if sign_mode not in ("identity", "explicit"):
raise ValueError(f"Unsupported Hadamard sign mode: {sign_mode!r}")
sign_widths: list[int] = []
sign_values: list[int] = []
if sign_mode == "explicit":
signs = manifest.get("signs")
if not isinstance(signs, dict) or not signs:
raise ValueError("explicit sign mode requires a signs table")
for width_str, vec in sorted(signs.items(), key=lambda kv: int(kv[0])):
width = int(width_str)
# same width rule the runtime enforces, so a manifest that converts also loads
if width <= 0 or width % block_size != 0:
raise ValueError(
f"sign width {width} must be positive and a multiple of block size {block_size}"
)
if len(vec) != width or any(v not in (-1, 1) for v in vec):
raise ValueError(f"invalid sign vector for width {width}")
sign_widths.append(width)
sign_values.extend(int(v) for v in vec)

tensor_records = manifest.get("tensors")
if not isinstance(tensor_records, list) or not tensor_records:
raise ValueError("Hadamard manifest has no folded tensors")

# The runtime applies the activation transform only where the graph goes through
# build_lora_mm/build_lora_mm_id. Restrict the contract to architectures and tensor
# kinds verified to route every matmul through those helpers; anything else must
# fail here instead of producing a GGUF that loads but skips the transform.
_HADAMARD_ARCHS = {
gguf.MODEL_ARCH.LLAMA,
gguf.MODEL_ARCH.QWEN3,
gguf.MODEL_ARCH.QWEN3MOE,
gguf.MODEL_ARCH.QWEN35,
gguf.MODEL_ARCH.QWEN35MOE,
gguf.MODEL_ARCH.QWEN3NEXT,
}
if self.model_arch not in _HADAMARD_ARCHS:
raise ValueError(
f"Hadamard folding is not verified for arch {self.model_arch.name}; "
"the runtime would load the GGUF without applying the activation transform"
)
_HADAMARD_KINDS = re.compile(
r"output\.weight|"
r"blk\.\d+\.("
r"attn_q|attn_k|attn_v|attn_qkv|attn_gate|attn_output"
r"|ffn_gate|ffn_up|ffn_down"
r"|ffn_gate_exps|ffn_up_exps|ffn_down_exps|ffn_gate_up_exps"
r"|ffn_gate_shexp|ffn_up_shexp|ffn_down_shexp"
r"|ssm_out"
r")\.weight"
)
weight_names: list[str] = []
inverse_weight_names: list[str] = []
for record in tensor_records:
if not isinstance(record, dict) or not isinstance(record.get("name"), str):
raise ValueError("Hadamard manifest has an invalid tensor record")
if record.get("axis") != -1:
raise ValueError(f"Unsupported Hadamard tensor axis for {record['name']!r}")
role = record.get("role", "fold-before-matmul")
if role not in ("fold-before-matmul", "inverse-after-lookup"):
raise ValueError(f"Unsupported Hadamard tensor role for {record['name']!r}: {role!r}")
filtered = self.filter_tensors((record["name"], lambda: None))
if filtered is None:
raise ValueError(f"Hadamard tensor is filtered out: {record['name']!r}")
mapped = self.map_tensor_name(filtered[0])
if role == "inverse-after-lookup":
# the runtime applies the inverse transform only to the token-embedding
# lookup; any other latent table would load and silently stay rotated
if mapped != "token_embd.weight":
raise ValueError(
f"Hadamard tensor {record['name']!r} maps to {mapped!r}, which is not a "
"verified inverse-after-lookup table"
)
inverse_weight_names.append(mapped)
else:
if not _HADAMARD_KINDS.fullmatch(mapped):
raise ValueError(
f"Hadamard tensor {record['name']!r} maps to {mapped!r}, which is not on a "
"verified Hadamard-aware matmul path"
)
weight_names.append(mapped)

self.gguf_writer.add_uint32("prism.hadamard.version", 1)
self.gguf_writer.add_uint32("prism.hadamard.block_size", block_size)
self.gguf_writer.add_string("prism.hadamard.transform", "normalized-sylvester-walsh-hadamard")
self.gguf_writer.add_string("prism.hadamard.axis", "input-last-dimension")
self.gguf_writer.add_string("prism.hadamard.sign_mode", sign_mode)
self.gguf_writer.add_array("prism.hadamard.weight_names", weight_names)
if sign_mode == "explicit":
self.gguf_writer.add_array("prism.hadamard.sign_widths", sign_widths)
self.gguf_writer.add_array("prism.hadamard.sign_values", sign_values)
if inverse_weight_names:
self.gguf_writer.add_array("prism.hadamard.inverse_weight_names", inverse_weight_names)
if getattr(self, "_hadamard_gdn_v_grouped", False):
self.gguf_writer.add_bool("prism.hadamard.gdn_v_grouped", True)
logger.info("GGUF Hadamard: linear-attention out_proj kept in grouped V order")
logger.info("GGUF Hadamard contract: H%d, sign_mode=%s, %d folded weight(s), %d inverse-lookup",
block_size, sign_mode, len(weight_names), len(inverse_weight_names))

def set_gguf_parameters(self):
raise NotImplementedError("set_gguf_parameters() must be implemented in subclasses")

Expand Down Expand Up @@ -1061,6 +1199,8 @@ def prepare_metadata(self, vocab_only: bool):
logger.info("Set model quantization version")
self.gguf_writer.add_quantization_version(gguf.GGML_QUANT_VERSION)

self.add_hadamard_metadata()

def write_vocab(self):
raise NotImplementedError("write_vocab() must be implemented in subclasses")

Expand Down
34 changes: 27 additions & 7 deletions conversion/qwen.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,18 +547,28 @@ def reorder_rows(qs: Tensor, scales: Tensor, head_dim: int) -> tuple[Tensor, Ten
elif name.endswith((".linear_attn.in_proj_a.weight", ".linear_attn.in_proj_b.weight")):
weight, scale = reorder_rows(weight, scale, 1)
elif name.endswith(".linear_attn.out_proj.weight"):
col_perm = self._reorder_v_heads(
torch.arange(num_v_heads * head_v_dim, dtype=torch.long).unsqueeze(0),
1, num_k_heads, num_v_per_k, head_v_dim,
).squeeze(0)
weight, scale = apply_col_perm(weight, scale, col_perm)
if self._hadamard_folds_tensor(name):
# folded latent: a column permutation on the rotation axis cannot be
# refolded, so keep the training (grouped) order and let the runtime
# permute the activation instead
self._hadamard_gdn_v_grouped = True
else:
col_perm = self._reorder_v_heads(
torch.arange(num_v_heads * head_v_dim, dtype=torch.long).unsqueeze(0),
1, num_k_heads, num_v_per_k, head_v_dim,
).squeeze(0)
weight, scale = apply_col_perm(weight, scale, col_perm)

return weight, scale

def _repack_nvfp4(self, name: str, weight: Tensor, scale: Tensor, scale2: Tensor, input_scale: Tensor):
weight, scale = self._transform_nvfp4_weight(name, weight, scale)
super()._repack_nvfp4(name, weight, scale, scale2, input_scale)

def _hadamard_folds_tensor(self, name: str) -> bool:
# a manifest entry and `name` may differ only by leading wrapper prefixes
return any(name.endswith(n) or n.endswith(name) for n in self.hadamard_folded_names())

def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
num_k_heads = self.hparams.get("linear_num_key_heads", 0)
num_v_heads = self.hparams.get("linear_num_value_heads", 0)
Expand Down Expand Up @@ -605,8 +615,18 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
data_torch = torch.cat([qk_part, v_part], dim=0)

elif ".out_proj." in name:
# Out projection weight: reorder columns (input dimension)
data_torch = self._reorder_v_heads(data_torch, 1, num_k_heads, num_v_per_k, head_v_dim)
# wrapper prefixes may already be stripped by the time the name reaches
# modify_tensors, so match on the full name modulo those prefixes. Matching
# on the tensor-kind suffix alone would treat every layer as folded as soon
# as one layer is.
if self._hadamard_folds_tensor(name):
# Hadamard-folded latent: the rotation axis must keep the
# training (grouped) V order; the runtime permutes the
# activation tiled->grouped before the transform instead.
self._hadamard_gdn_v_grouped = True
Comment thread
khosravipasha marked this conversation as resolved.
else:
# Out projection weight: reorder columns (input dimension)
data_torch = self._reorder_v_heads(data_torch, 1, num_k_heads, num_v_per_k, head_v_dim)

yield from super().modify_tensors(data_torch, name, bid)

Expand Down
18 changes: 14 additions & 4 deletions ggml/src/ggml-cpu/ggml-cpu.c
Original file line number Diff line number Diff line change
Expand Up @@ -1334,7 +1334,7 @@ UseGgmlGemm1:;
const size_t nbw3 = nbw2*ne12;

assert(params->wsize >= ne13*nbw3);
GGML_ASSERT(src1->type == GGML_TYPE_F32);
GGML_ASSERT(src1->type == GGML_TYPE_F32 || src1->type == GGML_TYPE_F16);

#if 0
for (int64_t i13 = 0; i13 < ne13; ++i13) {
Expand All @@ -1353,9 +1353,19 @@ UseGgmlGemm1:;
size_t bs = ggml_blck_size(vec_dot_type);
int64_t ne10_block_start = (ith * ne10/bs) / nth;
int64_t ne10_block_end = ((ith + 1) * ne10/bs) / nth;
from_float((float *)((char *) src1->data + i13*nb13 + i12*nb12 + i11*nb11 + ne10_block_start*bs*nb10),
(void *) (wdata + i13*nbw3 + i12*nbw2 + i11*nbw1 + ne10_block_start*nbw0),
(ne10_block_end - ne10_block_start) * bs);
const char * src1_block = (const char *) src1->data + i13*nb13 + i12*nb12 + i11*nb11 + ne10_block_start*bs*nb10;
char * dst_block = wdata + i13*nbw3 + i12*nbw2 + i11*nbw1 + ne10_block_start*nbw0;
const int64_t n_block = (ne10_block_end - ne10_block_start) * bs;

if (src1->type == GGML_TYPE_F32) {
from_float((const float *) src1_block, dst_block, n_block);
} else {
const ggml_fp16_t * src_f16 = (const ggml_fp16_t *) src1_block;
float * dst_f32 = (float *) dst_block;
for (int64_t i = 0; i < n_block; ++i) {
dst_f32[i] = GGML_CPU_FP16_TO_FP32(src_f16[i]);
}
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions ggml/src/ggml-cpu/ggml-cpu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ static bool ggml_backend_cpu_device_supports_op(ggml_backend_dev_t dev, const st
op->type != GGML_TYPE_IQ1_S &&
op->type != GGML_TYPE_IQ1_M; // missing type_traits.from_float
case GGML_OP_MUL_MAT:
if (ggml_get_op_params_i32(op, 1) == GGML_HINT_SRC0_IS_HADAMARD &&
src0->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32) {
return src1->type == GGML_TYPE_F32 || src1->type == GGML_TYPE_F16;
}
return src1->type == GGML_TYPE_F32 || src1->type == ggml_get_type_traits_cpu(src0->type)->vec_dot_type;
case GGML_OP_SOFT_MAX_BACK: {
if (op->src[0]->type != GGML_TYPE_F32 || op->src[1]->type != GGML_TYPE_F32) {
Expand Down
26 changes: 20 additions & 6 deletions ggml/src/ggml-cpu/ops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11859,11 +11859,20 @@ void ggml_compute_forward_opt_step_sgd(const ggml_compute_params * params, ggml_
}
}

static void ggml_compute_forward_fwht_f32(const ggml_compute_params * params, ggml_tensor * dst) {
static inline float ggml_fwht_load(const float value) {
return value;
}

static inline float ggml_fwht_load(const ggml_fp16_t value) {
return ggml_fp16_to_fp32(value);
}

template<typename src_t>
static void ggml_compute_forward_fwht_impl(const ggml_compute_params * params, ggml_tensor * dst) {
const ggml_tensor * src0 = dst->src[0];
const ggml_tensor * src1 = dst->src[1];

GGML_ASSERT(src1->type == GGML_TYPE_F32);
GGML_ASSERT(src1->type == (std::is_same_v<src_t, float> ? GGML_TYPE_F32 : GGML_TYPE_F16));
GGML_ASSERT(dst->type == GGML_TYPE_F32);

GGML_TENSOR_BINARY_OP_LOCALS
Expand All @@ -11890,11 +11899,11 @@ static void ggml_compute_forward_fwht_f32(const ggml_compute_params * params, gg
const int64_t i12 = (r - i13 * ne11 * ne12) / ne11;
const int64_t i11 = r - i13 * ne11 * ne12 - i12 * ne11;

const float * src_row = (const float *) ((const char *) src1->data + i11 * nb11 + i12 * nb12 + i13 * nb13);
const src_t * src_row = (const src_t *) ((const char *) src1->data + i11 * nb11 + i12 * nb12 + i13 * nb13);
float * dst_row = (float *) ((char *) dst->data + i11 * nb1 + i12 * nb2 + i13 * nb3);

for (int64_t j = 0; j < n; j++) {
dst_row[j] = src_row[j] * scale;
dst_row[j] = ggml_fwht_load(src_row[j]) * scale;
}

// Scalar passes
Expand Down Expand Up @@ -11941,12 +11950,17 @@ void ggml_compute_forward_fwht(const ggml_compute_params * params, ggml_tensor *
switch (src1->type) {
case GGML_TYPE_F32:
{
ggml_compute_forward_fwht_f32(params, dst);
ggml_compute_forward_fwht_impl<float>(params, dst);
}
break;
case GGML_TYPE_F16:
{
ggml_compute_forward_fwht_impl<ggml_fp16_t>(params, dst);
}
break;
default:
{
GGML_ABORT("fatal error - fwht is F32 only");
GGML_ABORT("fatal error - fwht supports F32 and F16 input");
}
}
}
Expand Down
Loading
Loading