Compare commits

...
3 Commits
Author SHA1 Message Date
Millaguie 0ecb2a521e tests : check DT3 byte positions against hand-computed literals
The previous byte-position test packed with the test's own packer on
both sides of the comparison, so it exercised none of the library code.
It now pins hand-computed byte values (43/100/127/42/124...) at the
region boundaries (79/80, 119/120) as ground truth and drives both
directions through the library: to_float must place each literal byte's
trit at the exact element, and from_float must produce the exact literal
byte, for both planes.

Also probes ggml_validate_row_data over all 256 byte values in qs and
qh positions (must accept exactly the 243/81 reachable codes), the
all-0xaa block, and a well-formed packed block.
2026-08-10 13:46:21 +02:00
Millaguie f39d565d1a llama : warn when quantizing to DT3
The reference-quantizer disclaimer only existed in the code and in
llama-quantize --help; now it is also printed where the mistake would
actually be made, at the start of a quantization run targeting DT3.
2026-08-10 13:46:21 +02:00
Millaguie d8feee2542 ggml : harden DT3 validation and reference quantizer
ggml_validate_row_data now rejects unreachable code bytes: the ceiling
division packing reaches only 243 of the 256 byte values in qs and 81
in qh (4 trits plus an always-zero padding digit), so corruption that
previously loaded and generated garbage silently is caught at load
time. Previously only the two fp16 scales were checked.

quantize_dt3 no longer discards quant_weights silently: an ignored
imatrix now prints a loud warning (once), otherwise an imatrix A/B on
DT3 would come out byte-identical and invite the false conclusion that
the imatrix does nothing.

The two initial trit passes of quantize_row_dt3_ref now clamp like the
refit passes do, so a NaN input cannot push an out-of-range value from
lroundf into the packer.
2026-08-10 13:46:21 +02:00
3 changed files with 212 additions and 42 deletions
+36 -3
View File
@@ -2554,7 +2554,8 @@ void quantize_row_dt3_ref(const float * GGML_RESTRICT x, block_dt3 * GGML_RESTRI
d1 = amax;
const float id = d1 ? 1.0f/d1 : 0.0f;
for (int j = 0; j < QK_DT3; j++) {
t1[j] = (int8_t) lroundf(x[j] * id); // -1, 0, 1
const int v = (int) lroundf(x[j] * id); // -1, 0, 1 (clamp guards against NaN input)
t1[j] = (int8_t) MAX(-1, MIN(1, v));
}
}
@@ -2567,7 +2568,8 @@ void quantize_row_dt3_ref(const float * GGML_RESTRICT x, block_dt3 * GGML_RESTRI
d2 = amax;
const float id = d2 ? 1.0f/d2 : 0.0f;
for (int j = 0; j < QK_DT3; j++) {
t2[j] = (int8_t) lroundf((x[j] - d1*t1[j]) * id); // -1, 0, 1
const int v = (int) lroundf((x[j] - d1*t1[j]) * id); // -1, 0, 1 (clamp guards against NaN input)
t2[j] = (int8_t) MAX(-1, MIN(1, v));
}
}
@@ -2611,7 +2613,15 @@ void quantize_row_dt3_ref(const float * GGML_RESTRICT x, block_dt3 * GGML_RESTRI
}
size_t quantize_dt3(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrow, int64_t n_per_row, const float * quant_weights) {
(void)quant_weights; // not used
if (quant_weights) {
// say it loudly, or an imatrix-vs-no-imatrix A/B on DT3 would come out
// byte-identical and someone would conclude the imatrix does nothing
static bool warned = false;
if (!warned) {
fprintf(stderr, "%s: WARNING: imatrix ignored: the DT3 reference quantizer does not use it; quality comes from ternaria's PTQTP\n", __func__);
warned = true;
}
}
const size_t row_size = ggml_row_size(GGML_TYPE_DT3, n_per_row);
quantize_row_dt3_ref(src, dst, (int64_t)nrow*n_per_row);
return nrow * row_size;
@@ -5796,6 +5806,29 @@ bool ggml_validate_row_data(enum ggml_type type, const void * data, size_t nbyte
if (!validate_fp16(q[i].d[0], i) || !validate_fp16(q[i].d[1], i)) {
return false;
}
// the ceiling-division packing reaches only 243 of the 256
// byte values in qs, and only 81 in qh (4 trits plus an
// always-zero padding digit); anything else is corruption,
// and a corrupted block would otherwise load and generate
// garbage without any warning
for (int p = 0; p < 2; ++p) {
for (size_t j = 0; j < sizeof(q[i].qs[p]); ++j) {
const uint32_t b = q[i].qs[p][j];
const uint32_t v = (b*243) >> 8; // inverse of the ceiling division
if ((v*256 + 242)/243 != b) {
fprintf(stderr, "ggml_validate_row_data: found invalid dt3 qs byte 0x%02x at block %zu\n", (unsigned) b, i);
return false;
}
}
for (size_t j = 0; j < sizeof(q[i].qh[p]); ++j) {
const uint32_t b = q[i].qh[p][j];
const uint32_t v = (b*243) >> 8;
if (v % 3 != 0 || (v*256 + 242)/243 != b) {
fprintf(stderr, "ggml_validate_row_data: found invalid dt3 qh byte 0x%02x at block %zu\n", (unsigned) b, i);
return false;
}
}
}
}
} break;
case GGML_TYPE_IQ1_S:
+5
View File
@@ -884,6 +884,11 @@ static void llama_model_quantize_impl(const std::string & fname_inp, const std::
throw std::runtime_error(format("invalid output file type %d\n", ftype));
}
if (ftype == LLAMA_FTYPE_MOSTLY_DT3) {
LLAMA_LOG_WARN("%s: DT3 is being quantized with the in-tree REFERENCE quantizer only - "
"the measured DT3 quality requires the planes from ternaria's PTQTP pipeline\n", __func__);
}
// mmap consistently increases speed on Linux, and also increases speed on Windows with
// hot cache. It may cause a slowdown on macOS, possibly related to free memory.
#if defined(__linux__) || defined(_WIN32)
+171 -39
View File
@@ -143,53 +143,117 @@ static int test_single_trits(const ggml_type_traits * qfns) {
return num_failed;
}
// spot-check the byte positions of the region boundaries against values
// computed by hand from the packing formula
static int test_byte_positions(void) {
// an all-zero plane packs to qs = 128, qh = 127 everywhere (hand-computed:
// qs: xi = 1 for the 5 trits -> q = 121 -> ceil(121*256/243) = 128,
// qh: q = 40*3 = 120 -> ceil(120*256/243) = 127)
constexpr uint8_t ZERO_QS = 128;
constexpr uint8_t ZERO_QH = 127;
static void build_zero_pattern_block(uint8_t * block, float d1, float d2) {
memset(block + OFF_QS, ZERO_QS, 2*DT3_QS_BYTES);
memset(block + OFF_QH, ZERO_QH, 2*DT3_QH_BYTES);
const uint16_t h1 = ggml_fp32_to_fp16(d1);
const uint16_t h2 = ggml_fp32_to_fp16(d2);
memcpy(block + OFF_D, &h1, sizeof(h1));
memcpy(block + OFF_D + 2, &h2, sizeof(h2));
}
// check the byte positions of the region boundaries against literal values
// computed by hand from the packing formula (and cross-checked one by one
// against an independent implementation of the spec). Both directions go
// against the LIBRARY code — to_float and from_float — never against a
// packer defined in this file, so a bit-placement bug in ggml-quants.c
// cannot cancel out.
static int test_byte_positions(const ggml_type_traits * qfns, const ggml_type_traits_cpu * qfns_cpu) {
int num_failed = 0;
// verify the element -> byte mapping structurally: flipping element `pos`
// must change EXACTLY the one byte the formula says, and no other
struct pos_case {
int pos;
size_t off; // expected changed byte, offset inside plane data
struct byte_case {
int pos; // element with t = -1, all others 0
size_t off; // byte offset inside the plane data (qs: 0..23, qh: 24, 25)
uint8_t value; // hand-computed byte value at that offset
};
const pos_case pcases[] = {
{ 0, 0 }, // region A, byte 0
{ 15, 15 }, // region A, byte 15
{ 16, 0 }, // region A, digit 1 of byte 0
{ 64, 0 }, // region A, digit 4 of byte 0
{ 79, 15 }, // region A boundary: last element, byte 15
{ 80, 16 }, // region B boundary: first element, byte 16
{ 87, 23 }, // region B, byte 23
{ 119, 23 }, // region B boundary: last element, byte 23
{ 120, 24 }, // region C boundary: first element, qh[0]
{ 121, 25 }, // region C, qh[1]
{ 126, 24 }, // region C, digit 3 of qh[0]
{ 127, 25 }, // region C, last element, qh[1]
const byte_case cases[] = {
{ 0, 0, 43 }, // region A: most significant trit of qs[0]: q = 121-81 = 40 -> 43
{ 16, 0, 100 }, // region A: second trit of qs[0]: q = 121-27 = 94 -> 100
{ 79, 15, 127 }, // region A boundary: last trit of qs[15]: q = 121-1 = 120 -> 127
{ 80, 16, 43 }, // region B boundary: first trit of qs[16]: q = 40 -> 43
{ 119, 23, 127 }, // region B boundary: last trit of qs[23]: q = 120 -> 127
{ 120, 24, 42 }, // region C boundary: first trit of qh[0]: q = (40-27)*3 -> 42
{ 127, 25, 124 }, // region C: last element, qh[1]: q = (40-1)*3 -> 124
};
uint8_t zero_plane[DT3_QS_BYTES + DT3_QH_BYTES];
{
const int8_t t0[QK_DT3] = {0};
ref_pack_plane(t0, zero_plane, zero_plane + DT3_QS_BYTES);
for (size_t c = 0; c < sizeof(cases)/sizeof(cases[0]); ++c) {
const int pos = cases[c].pos;
const size_t off = cases[c].off;
const uint8_t val = cases[c].value;
// unpack direction: writing the literal byte must put -1 at exactly
// element `pos` of the corresponding plane
for (int plane = 0; plane < 2; ++plane) {
uint8_t block[DT3_BLOCK_SIZE];
build_zero_pattern_block(block, 1.0f, 0.25f);
if (off < DT3_QS_BYTES) {
block[OFF_QS + plane*DT3_QS_BYTES + off] = val;
} else {
block[OFF_QH + plane*DT3_QH_BYTES + (off - DT3_QS_BYTES)] = val;
}
float out[QK_DT3];
qfns->to_float(block, out, QK_DT3);
const float dp = plane == 0 ? 1.0f : 0.25f;
for (int j = 0; j < QK_DT3; ++j) {
const float expected = j == pos ? -dp : 0.0f;
if (out[j] != expected) {
printf("FAILED: byte 0x%02x at plane %d offset %zu: out[%d] = %f, expected %f\n",
val, plane, off, j, out[j], expected);
num_failed++;
}
}
}
// pack direction: quantizing a single -1 must produce exactly the
// literal byte at the right offset of plane 0 (plane 1 is the zero
// pattern since there is no residual), and nothing else
{
float x[QK_DT3] = {0.0f};
x[pos] = -1.0f;
uint8_t block[DT3_BLOCK_SIZE];
qfns_cpu->from_float(x, block, QK_DT3);
uint8_t expected[DT3_BLOCK_SIZE];
build_zero_pattern_block(expected, 1.0f, 0.0f);
if (off < DT3_QS_BYTES) {
expected[OFF_QS + off] = val;
} else {
expected[OFF_QH + (off - DT3_QS_BYTES)] = val;
}
for (size_t b = 0; b < DT3_BLOCK_SIZE; ++b) {
if (block[b] != expected[b]) {
printf("FAILED: pack of -1 at element %d: byte %zu is 0x%02x, expected 0x%02x\n",
pos, b, block[b], expected[b]);
num_failed++;
}
}
}
}
for (size_t c = 0; c < sizeof(pcases)/sizeof(pcases[0]); ++c) {
int8_t t[QK_DT3] = {0};
t[pcases[c].pos] = -1;
// all-zero input packs to the all-zero pattern with zero scales
{
const float x[QK_DT3] = {0.0f};
uint8_t plane[DT3_QS_BYTES + DT3_QH_BYTES];
ref_pack_plane(t, plane, plane + DT3_QS_BYTES);
uint8_t block[DT3_BLOCK_SIZE];
qfns_cpu->from_float(x, block, QK_DT3);
for (size_t b = 0; b < sizeof(plane); ++b) {
const bool should_differ = b == pcases[c].off;
const bool differs = plane[b] != zero_plane[b];
if (differs != should_differ) {
printf("FAILED: pos %d: byte %zu %s, expected %s\n",
pcases[c].pos, b,
differs ? "changed" : "unchanged",
should_differ ? "changed" : "unchanged");
uint8_t expected[DT3_BLOCK_SIZE];
build_zero_pattern_block(expected, 0.0f, 0.0f);
for (size_t b = 0; b < DT3_BLOCK_SIZE; ++b) {
if (block[b] != expected[b]) {
printf("FAILED: pack of all-zero block: byte %zu is 0x%02x, expected 0x%02x\n",
b, block[b], expected[b]);
num_failed++;
}
}
@@ -198,6 +262,73 @@ static int test_byte_positions(void) {
return num_failed;
}
// ggml_validate_row_data must accept exactly the reachable byte values:
// 243 of 256 in qs, 81 of 256 in qh (4 trits + an always-zero padding digit)
static int test_validate(void) {
int num_failed = 0;
bool valid_qs[256] = {false};
bool valid_qh[256] = {false};
for (uint32_t q = 0; q < 243; ++q) {
valid_qs[(q*256 + 242)/243] = true;
if (q % 3 == 0 && q <= 240) {
valid_qh[(q*256 + 242)/243] = true;
}
}
printf("(invalid-byte messages below are expected, the validator is being probed)\n");
for (int b = 0; b < 256; ++b) {
uint8_t block[DT3_BLOCK_SIZE];
build_zero_pattern_block(block, 1.0f, 0.25f);
block[OFF_QS] = (uint8_t) b;
if (ggml_validate_row_data(GGML_TYPE_DT3, block, DT3_BLOCK_SIZE) != valid_qs[b]) {
printf("FAILED: validate qs byte 0x%02x: expected %s\n", b, valid_qs[b] ? "valid" : "invalid");
num_failed++;
}
build_zero_pattern_block(block, 1.0f, 0.25f);
block[OFF_QH + DT3_QH_BYTES] = (uint8_t) b; // plane 1 qh, to also cover the second plane
if (ggml_validate_row_data(GGML_TYPE_DT3, block, DT3_BLOCK_SIZE) != valid_qh[b]) {
printf("FAILED: validate qh byte 0x%02x: expected %s\n", b, valid_qh[b] ? "valid" : "invalid");
num_failed++;
}
}
// a block filled with 0xaa must be rejected (0xaa is a reachable qs code
// but violates the qh padding-digit invariant)
{
uint8_t block[DT3_BLOCK_SIZE];
memset(block, 0xaa, DT3_BLOCK_SIZE);
const uint16_t h = ggml_fp32_to_fp16(1.0f);
memcpy(block + OFF_D, &h, sizeof(h));
memcpy(block + OFF_D + 2, &h, sizeof(h));
if (ggml_validate_row_data(GGML_TYPE_DT3, block, DT3_BLOCK_SIZE)) {
printf("FAILED: validate accepted a block filled with 0xaa\n");
num_failed++;
}
}
// packed real data must pass
{
int8_t t1[QK_DT3];
int8_t t2[QK_DT3];
for (int j = 0; j < QK_DT3; ++j) {
t1[j] = rng_trit();
t2[j] = rng_trit();
}
uint8_t block[DT3_BLOCK_SIZE];
ref_pack_block(t1, -0.5f, t2, 0.125f, block);
if (!ggml_validate_row_data(GGML_TYPE_DT3, block, DT3_BLOCK_SIZE)) {
printf("FAILED: validate rejected a well-formed block\n");
num_failed++;
}
}
return num_failed;
}
// random trits and scales (negative scales included) must round-trip exactly
static int test_roundtrip(const ggml_type_traits * qfns) {
int num_failed = 0;
@@ -405,7 +536,8 @@ int main(int argc, char * argv[]) {
num_failed += test_layout_constants();
num_failed += test_single_trits(qfns);
num_failed += test_byte_positions();
num_failed += test_byte_positions(qfns, qfns_cpu);
num_failed += test_validate();
num_failed += test_roundtrip(qfns);
num_failed += test_quantize_pack_parity(qfns_cpu);
num_failed += test_vec_dot(qfns_cpu);