gguf-py : add DT3

Registers the type id, file type and block size, and implements numpy
dequantization (verified bit-exact against the C implementation with
gguf-py/tests/test_quants.py, including random byte payloads).

Quantization is intentionally left unimplemented, like the K-quants:
DT3 planes come from an external solver (PTQTP) and are packed
directly, so a from-float numpy path would only invite quantizing
models with the wrong algorithm.
This commit is contained in:
Millaguie
2026-08-10 13:17:53 +02:00
parent e1fd6e0a13
commit b3323108b5
2 changed files with 39 additions and 0 deletions
+3
View File
@@ -5019,6 +5019,7 @@ class GGMLQuantizationType(IntEnum):
NVFP4 = 40
Q1_0 = 41
Q2_0 = 42
DT3 = 43
class ExpertGatingFuncType(IntEnum):
@@ -5075,6 +5076,7 @@ class LlamaFileType(IntEnum):
MOSTLY_NVFP4 = 39 # except 1d tensors
MOSTLY_Q1_0 = 40 # except 1d tensors
MOSTLY_Q2_0 = 41 # except 1d tensors
MOSTLY_DT3 = 42 # except 1d tensors
GUESSED = 1024 # not specified in the model file
@@ -5206,6 +5208,7 @@ GGML_QUANT_SIZES: dict[GGMLQuantizationType, tuple[int, int]] = {
GGMLQuantizationType.NVFP4: (64, 4 + 32),
GGMLQuantizationType.Q1_0: (128, 2 + 16),
GGMLQuantizationType.Q2_0: (64, 2 + 16),
GGMLQuantizationType.DT3: (128, 2 * (24 + 2 + 2)),
}
+36
View File
@@ -654,6 +654,42 @@ class TQ2_0(__Quant, qtype=GGMLQuantizationType.TQ2_0):
return (d * qs.astype(np.float32))
class DT3(__Quant, qtype=GGMLQuantizationType.DT3):
# Dual-plane ternary: w = d1 * t1 + d2 * t2 with t in {-1, 0, 1}. Each of
# the two planes is packed like TQ1_0 with all constants halved (blocks of
# 128 elements): 24 bytes of qs (passes of 16 and 8 bytes) and 2 bytes of
# qh per plane, then the two fp16 scales.
# Quantization is intentionally not implemented here: DT3 planes are
# produced by an external solver (PTQTP) and packed directly.
@classmethod
def dequantize_blocks(cls, blocks: np.ndarray) -> np.ndarray:
n_blocks = blocks.shape[0]
qs, rest = np.hsplit(blocks, [2 * 24])
qh, d = np.hsplit(rest, [2 * 2])
d = d.view(np.float16).astype(np.float32).reshape((n_blocks, 2, 1))
planes = []
for p in range(2):
pqs = qs[..., p * 24:(p + 1) * 24]
pqh = qh[..., p * 2:(p + 1) * 2]
qs0, qs1 = pqs[..., :16], pqs[..., 16:]
qs0 = qs0.reshape((n_blocks, -1, 1, 16)) * np.array([1, 3, 9, 27, 81], dtype=np.uint8).reshape((1, 1, 5, 1))
qs0 = qs0.reshape((n_blocks, -1))
qs1 = qs1.reshape((n_blocks, -1, 1, 8)) * np.array([1, 3, 9, 27, 81], dtype=np.uint8).reshape((1, 1, 5, 1))
qs1 = qs1.reshape((n_blocks, -1))
# only 4 trits per qh byte, the 5th base-3 digit is packer padding
pqh = pqh.reshape((n_blocks, -1, 1, 2)) * np.array([1, 3, 9, 27], dtype=np.uint8).reshape((1, 1, 4, 1))
pqh = pqh.reshape((n_blocks, -1))
pq = np.concatenate([qs0, qs1, pqh], axis=-1)
pq = ((pq.astype(np.uint16) * 3) >> 8).astype(np.int8) - np.int8(1)
planes.append(pq.astype(np.float32))
return d[:, 0] * planes[0] + d[:, 1] * planes[1]
class MXFP4(__Quant, qtype=GGMLQuantizationType.MXFP4):
# e2m1 values (doubled)
# ref: https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf