Skip to content

Commit 153a7f6

Browse files
kmbandyclaude
andcommitted
feat(ml8): native 4-bit token_embd via GGML_OP_ML8_GET_ROWS + role-uniform tiering
Adds a native ml8-4 token embedding path so token_embd can be 4-bit (centroid LUT in an F8_E4M3 sidecar) instead of fp8 — reclaiming size on the largest tensor. The generic get_rows aborts on ML8_4 (no LUT slot), so this introduces a dedicated custom op: GGML_OP_ML8_GET_ROWS (ggml_ml8_get_rows(w, centroids, ids)) - ggml op def + name/symbol tables + asserts (op count 102->103) - CPU compute: LUT-dequant gather (ggml-cpu/ops.cpp) + dispatch + n_tasks - CUDA/HIP kernel + host dispatch (ml8.cu/.cuh) — pure gather, no AITER needed; + ggml-cuda.cu compute dispatch + supports_op - loader op-swap GET_ROWS -> ML8_GET_ROWS for ML8_4 token_embd (keeps it on GPU) + synthetic probe case - build_inp_embd routes ML8_4 token_embd through the new op via the registry - qwen35: register token_embd centroids; tied LM head shares token_embd's LUT (no separate output.centroids in a tied GGUF) Role-uniform tier override (ML8_TIER_OVERRIDE / role_targets) lets a role be moved uniformly between ml8(4-bit) and fp8(8-bit) — the "tread shaping". The qwen35 FFN fused path now dispatches up/down per-tensor: ml8_4 stays inline (byte-identical), a role-uniform fp8 tier routes through build_lora_mm. The data-free ml8-4 embed calibration pass + converter emit token_embd as ML8_4. Validated on dense Qwen3.5-0.8B (token_embd=ml8, ssm_out/ffn_down/attn_v=fp8, --awq none): PPL 19.7179 / 498MB — beats the uniform-ml8 clean q3 (19.9387 / 558MB) on BOTH axes; smaller than UD-Q4_K_XL (546MB) but +0.78 PPL behind. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 64900fb commit 153a7f6

16 files changed

Lines changed: 600 additions & 118 deletions

File tree

ggml/include/ggml-ml8.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,27 @@ GGML_API struct ggml_tensor * ggml_ml8_mul_mat_id(
102102
struct ggml_tensor * x,
103103
struct ggml_tensor * ids);
104104

105+
// Native ml8-4 row gather (token-embedding lookup). Gathers row `ids[i]` from the
106+
// ml8-4 weight `w` [K, N] and dequantizes it via the per-K-group centroid LUT to
107+
// K fp32 values — kept native 4-bit (no inline bf16 dequant of the table).
108+
//
109+
// Tensor shapes (ggml row-major):
110+
// w : [K, N] GGML_TYPE_ML8_4 (N = vocab rows)
111+
// centroids : [16, n_groups_k] GGML_TYPE_F8_E4M3 per-K-group LUT
112+
// ids : [n_rows, ...] GGML_TYPE_I32
113+
//
114+
// Output:
115+
// y : [K, n_rows, ...] GGML_TYPE_F32 (matches ggml_get_rows layout)
116+
//
117+
// Constraints:
118+
// - K % QK_ML8 == 0
119+
// - centroids ne0 == 16, ne1 == K / QK_ML8
120+
GGML_API struct ggml_tensor * ggml_ml8_get_rows(
121+
struct ggml_context * ctx,
122+
struct ggml_tensor * w,
123+
struct ggml_tensor * centroids,
124+
struct ggml_tensor * ids);
125+
105126
#ifdef __cplusplus
106127
} // extern "C"
107128
#endif

ggml/include/ggml.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,15 @@ extern "C" {
625625
// HIP backend dispatches to mt_ml8_moe_gemm; CPU fallback dequantizes
626626
// each routed expert block-by-block.
627627
GGML_OP_ML8_MUL_MAT_ID,
628+
// ml8-4 native row gather (token-embedding lookup for an ml8-4
629+
// token_embd). Unlike GGML_OP_GET_ROWS, the row data is ML8_4 and is
630+
// dequantized via the per-K-group centroid LUT sidecar — kept native
631+
// 4-bit (no inline bf16 dequant of the whole table).
632+
// src[0] = w (GGML_TYPE_ML8_4, [K, N]) — embedding rows (N = vocab)
633+
// src[1] = centroids (GGML_TYPE_F8_E4M3, [16, K/QK_ML8])
634+
// src[2] = ids (GGML_TYPE_I32, [n_rows, ...])
635+
// dst = y (GGML_TYPE_F32, [K, n_rows, ...])
636+
GGML_OP_ML8_GET_ROWS,
628637

629638
GGML_OP_COUNT,
630639
};

ggml/src/ggml-cpu/ggml-cpu.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2164,6 +2164,11 @@ static void ggml_compute_forward(struct ggml_compute_params * params, struct ggm
21642164
ggml_compute_forward_ml8_mul_mat_id(params, tensor);
21652165
}
21662166
break;
2167+
case GGML_OP_ML8_GET_ROWS:
2168+
{
2169+
ggml_compute_forward_ml8_get_rows(params, tensor);
2170+
}
2171+
break;
21672172
case GGML_OP_NONE:
21682173
{
21692174
// nop
@@ -2507,6 +2512,7 @@ static int ggml_get_n_tasks(struct ggml_tensor * node, int n_threads) {
25072512
case GGML_OP_ML8_MUL_MAT:
25082513
case GGML_OP_ML8_APPLY_ROTATION:
25092514
case GGML_OP_ML8_MUL_MAT_ID:
2515+
case GGML_OP_ML8_GET_ROWS:
25102516
{
25112517
n_tasks = n_threads;
25122518
} break;

ggml/src/ggml-cpu/ops.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11480,6 +11480,67 @@ void ggml_compute_forward_ml8_mul_mat(const ggml_compute_params * params, ggml_t
1148011480
free(w_row_fp32);
1148111481
}
1148211482

11483+
// ─── ggml_compute_forward_ml8_get_rows (MAD-256 ml8-4 native embed) ───────
11484+
//
11485+
// CPU dispatch for GGML_OP_ML8_GET_ROWS — native 4-bit token-embedding gather.
11486+
// For each requested row id, dequantize that ml8-4 weight row directly into the
11487+
// output via dequantize_row_ml8_4_with_lut (the per-K-group centroid LUT
11488+
// sidecar). The table stays native 4-bit — no inline bf16 dequant of W. Same
11489+
// addressing as ggml_compute_forward_get_rows_q, tiled across gathered rows.
11490+
//
11491+
// Tensor layouts (set by ggml_ml8_get_rows):
11492+
// dst->src[0] = w (GGML_TYPE_ML8_4 , [K, N] N = vocab rows)
11493+
// dst->src[1] = centroids (GGML_TYPE_F8_E4M3, [16, K/QK_ML8] shared LUT)
11494+
// dst->src[2] = ids (GGML_TYPE_I32)
11495+
// dst = y (GGML_TYPE_F32 , [K, ids->ne0, ids->ne1, ids->ne2])
11496+
void ggml_compute_forward_ml8_get_rows(const ggml_compute_params * params, ggml_tensor * dst) {
11497+
const ggml_tensor * w = dst->src[0];
11498+
const ggml_tensor * lut = dst->src[1];
11499+
const ggml_tensor * ids = dst->src[2];
11500+
GGML_ASSERT(w && lut && ids);
11501+
GGML_ASSERT(w->type == GGML_TYPE_ML8_4);
11502+
GGML_ASSERT(lut->type == GGML_TYPE_F8_E4M3);
11503+
GGML_ASSERT(ids->type == GGML_TYPE_I32);
11504+
GGML_ASSERT(dst->type == GGML_TYPE_F32);
11505+
11506+
const int64_t K = w->ne[0];
11507+
const int64_t N = w->ne[1];
11508+
GGML_ASSERT(dst->ne[0] == K);
11509+
GGML_ASSERT(K % QK_ML8 == 0);
11510+
const int64_t n_groups_k = K / QK_ML8;
11511+
GGML_ASSERT(lut->ne[0] == 16 && lut->ne[1] == n_groups_k);
11512+
11513+
const uint8_t * lut_fp8 = (const uint8_t *) lut->data;
11514+
11515+
// gathered rows: flatten over all ids elements (mirrors get_rows_q)
11516+
const int64_t nr = ggml_nelements(ids);
11517+
GGML_ASSERT(ggml_nrows(dst) == nr);
11518+
11519+
const int64_t ne10 = ids->ne[0];
11520+
const int64_t ne11 = ids->ne[1];
11521+
11522+
const int ith = params->ith;
11523+
const int nth = params->nth;
11524+
const int64_t dr = (nr + nth - 1) / nth;
11525+
const int64_t ir0 = dr * ith;
11526+
const int64_t ir1 = MIN(ir0 + dr, nr);
11527+
11528+
for (int64_t i = ir0; i < ir1; i++) {
11529+
const int64_t i12 = i / (ne11 * ne10);
11530+
const int64_t i11 = (i - i12 * ne11 * ne10) / ne10;
11531+
const int64_t i10 = (i - i12 * ne11 * ne10 - i11 * ne10);
11532+
const int64_t row = *(const int32_t *) ((const char *) ids->data
11533+
+ i10 * ids->nb[0] + i11 * ids->nb[1] + i12 * ids->nb[2]);
11534+
GGML_ASSERT(row >= 0 && row < N);
11535+
11536+
const block_ml8_4 * w_row =
11537+
(const block_ml8_4 *) ((const char *) w->data + row * w->nb[1]);
11538+
float * y_row = (float *) ((char *) dst->data
11539+
+ i10 * dst->nb[1] + i11 * dst->nb[2] + i12 * dst->nb[3]);
11540+
dequantize_row_ml8_4_with_lut(w_row, lut_fp8, y_row, K);
11541+
}
11542+
}
11543+
1148311544
// ─── ggml_compute_forward_ml8_apply_rotation (MAD-223 G.4.g) ──────────────
1148411545
//
1148511546
// Y[:, t] = unflatten(H_a^T @ flatten(X[:, t]) @ H_b, d=a*b)

ggml/src/ggml-cpu/ops.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ void ggml_compute_forward_opt_step_sgd(const struct ggml_compute_params * params
118118
void ggml_compute_forward_ml8_mul_mat(const struct ggml_compute_params * params, struct ggml_tensor * dst);
119119
void ggml_compute_forward_ml8_apply_rotation(const struct ggml_compute_params * params, struct ggml_tensor * dst);
120120
void ggml_compute_forward_ml8_mul_mat_id(const struct ggml_compute_params * params, struct ggml_tensor * dst);
121+
void ggml_compute_forward_ml8_get_rows(const struct ggml_compute_params * params, struct ggml_tensor * dst);
121122
#ifdef __cplusplus
122123
}
123124
#endif

ggml/src/ggml-cuda/ggml-cuda.cu

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3261,6 +3261,9 @@ static bool ggml_cuda_compute_forward(ggml_backend_cuda_context & ctx, struct gg
32613261
case GGML_OP_ML8_MUL_MAT_ID:
32623262
ggml_cuda_op_ml8_mul_mat_id(ctx, dst);
32633263
break;
3264+
case GGML_OP_ML8_GET_ROWS:
3265+
ggml_cuda_op_ml8_get_rows(ctx, dst);
3266+
break;
32643267
case GGML_OP_OUT_PROD:
32653268
ggml_cuda_out_prod(ctx, dst);
32663269
break;
@@ -5582,6 +5585,24 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g
55825585
if (w->ne[2] <= 0) return false;
55835586
return true;
55845587
} break;
5588+
case GGML_OP_ML8_GET_ROWS:
5589+
{
5590+
// MAD-256 ml8-4 native token-embedding gather. Pure dequant
5591+
// gather (no AITER GEMM) — ml8_4 weight, f8_e4m3 centroid LUT,
5592+
// i32 ids, fp32 output. K must be a multiple of QK_ML8=64.
5593+
const ggml_tensor * w = op->src[0];
5594+
const ggml_tensor * cent = op->src[1];
5595+
const ggml_tensor * ids = op->src[2];
5596+
if (!w || !cent || !ids) return false;
5597+
if (w->type != GGML_TYPE_ML8_4) return false;
5598+
if (cent->type != GGML_TYPE_F8_E4M3) return false;
5599+
if (ids->type != GGML_TYPE_I32) return false;
5600+
if (op->type != GGML_TYPE_F32) return false;
5601+
if (w->ne[0] % 64 != 0) return false;
5602+
if (cent->ne[0] != 16) return false;
5603+
if (cent->ne[1] != w->ne[0] / 64) return false;
5604+
return true;
5605+
} break;
55855606
case GGML_OP_OUT_PROD:
55865607
return op->type == GGML_TYPE_F32 && op->src[0]->type == GGML_TYPE_F32 && op->src[1]->type == GGML_TYPE_F32;
55875608
case GGML_OP_GET_ROWS:

ggml/src/ggml-cuda/ml8.cu

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,94 @@ void ggml_cuda_op_ml8_mul_mat(
10121012
#endif // GGML_HIP_AITER
10131013
}
10141014
1015+
// ─────────────────────────────────────────────────────────────────────
1016+
// GGML_OP_ML8_GET_ROWS — native 4-bit token-embedding gather.
1017+
//
1018+
// One CUDA block per gathered row; threads stride over the row's K-groups.
1019+
// For each group: read the per-block fp32 scale + 32 packed nibbles from the
1020+
// native block_ml8_4 layout, index the shared per-group centroid LUT (16 fp8
1021+
// e4m3 each), dequant = centroid * scale, write K fp32. No AITER GEMM, no
1022+
// repack — this is a pure gather so it works on any CUDA/HIP build. Mirrors
1023+
// the CPU ggml_compute_forward_ml8_get_rows math exactly (same LUT, same
1024+
// lo-nibble-first ordering, same e4m3→fp32 helper).
1025+
// ─────────────────────────────────────────────────────────────────────
1026+
static __global__ void ml8_get_rows_kernel(
1027+
const block_ml8_4 * __restrict__ w, // [N rows][n_groups_k blocks] native layout
1028+
const uint8_t * __restrict__ lut, // [n_groups_k, 16] fp8 e4m3 (flat g*16+i)
1029+
const int32_t * __restrict__ ids, // [nr] contiguous
1030+
float * __restrict__ y, // [nr, K] row-major (K contiguous per row)
1031+
int K, int N, int n_groups_k, int64_t nr) {
1032+
1033+
const int64_t i = blockIdx.x; // gathered-row index
1034+
if (i >= nr) return;
1035+
1036+
const int32_t row = ids[i];
1037+
// out-of-range ids would read garbage rows; clamp defensively to 0.
1038+
const int32_t row_safe = (row >= 0 && row < N) ? row : 0;
1039+
1040+
const block_ml8_4 * w_row = w + (int64_t) row_safe * n_groups_k;
1041+
float * y_row = y + i * (int64_t) K;
1042+
1043+
for (int g = threadIdx.x; g < n_groups_k; g += blockDim.x) {
1044+
const block_ml8_4 * blk = &w_row[g];
1045+
const float scale = blk->scale;
1046+
const uint8_t * lut_g = lut + (int64_t) g * 16;
1047+
const int k_base = g * QK_ML8;
1048+
#pragma unroll
1049+
for (int p = 0; p < QK_ML8 / 2; p++) {
1050+
const uint8_t byte = blk->qs[p];
1051+
const uint8_t lo = byte & 0x0F;
1052+
const uint8_t hi = (byte >> 4) & 0x0F;
1053+
y_row[k_base + p * 2] = ml8_fp8_e4m3_to_fp32(lut_g[lo]) * scale;
1054+
y_row[k_base + p * 2 + 1] = ml8_fp8_e4m3_to_fp32(lut_g[hi]) * scale;
1055+
}
1056+
}
1057+
}
1058+
1059+
void ggml_cuda_op_ml8_get_rows(
1060+
ggml_backend_cuda_context & ctx,
1061+
ggml_tensor * dst) {
1062+
const ggml_tensor * w = dst->src[0];
1063+
const ggml_tensor * cent = dst->src[1];
1064+
const ggml_tensor * ids = dst->src[2];
1065+
1066+
GGML_ASSERT(w != nullptr && cent != nullptr && ids != nullptr);
1067+
GGML_ASSERT(w->type == GGML_TYPE_ML8_4);
1068+
GGML_ASSERT(cent->type == GGML_TYPE_F8_E4M3);
1069+
GGML_ASSERT(ids->type == GGML_TYPE_I32);
1070+
GGML_ASSERT(dst->type == GGML_TYPE_F32);
1071+
GGML_ASSERT(ggml_is_contiguous(w));
1072+
GGML_ASSERT(ggml_is_contiguous(cent));
1073+
GGML_ASSERT(ggml_is_contiguous(ids));
1074+
GGML_ASSERT(ggml_is_contiguous(dst));
1075+
1076+
const int32_t K = (int32_t) w->ne[0];
1077+
const int32_t N = (int32_t) w->ne[1];
1078+
GGML_ASSERT(K % QK_ML8 == 0);
1079+
const int32_t n_groups_k = K / QK_ML8;
1080+
GGML_ASSERT(cent->ne[0] == 16);
1081+
GGML_ASSERT(cent->ne[1] == n_groups_k);
1082+
GGML_ASSERT(dst->ne[0] == K);
1083+
1084+
const int64_t nr = ggml_nelements(ids);
1085+
GGML_ASSERT(ggml_nrows(dst) == nr);
1086+
if (nr == 0) {
1087+
return;
1088+
}
1089+
1090+
cudaStream_t stream = ctx.stream();
1091+
1092+
const block_ml8_4 * w_d = (const block_ml8_4 *) w->data;
1093+
const uint8_t * lut_d = (const uint8_t *) cent->data;
1094+
const int32_t * ids_d = (const int32_t *) ids->data;
1095+
float * y_d = (float *) dst->data;
1096+
1097+
const int threads = (n_groups_k < 256) ? ((n_groups_k + 31) / 32) * 32 : 256;
1098+
const dim3 grid((unsigned) nr);
1099+
ml8_get_rows_kernel<<<grid, dim3(threads > 0 ? threads : 32), 0, stream>>>(
1100+
w_d, lut_d, ids_d, y_d, K, N, n_groups_k, nr);
1101+
}
1102+
10151103
// ─────────────────────────────────────────────────────────────────────
10161104
// No-LUT FP8-WMMA mul_mat for scaled-fp8 weights (GGML_TYPE_ML8_FP8).
10171105
//

ggml/src/ggml-cuda/ml8.cuh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,18 @@ void ggml_cuda_op_ml8_mul_mat(
122122
ggml_backend_cuda_context & ctx,
123123
ggml_tensor * dst);
124124

125+
// GGML_OP_ML8_GET_ROWS dispatch — native 4-bit token-embedding gather.
126+
// Unlike ggml_cuda_op_ml8_mul_mat this needs NO AITER GEMM: it gathers row
127+
// ids[i] from the ml8-4 weight and dequantizes via the per-K-group centroid
128+
// LUT directly on device. Available on any CUDA/HIP build.
129+
// dst: fp32 [K, ids->ne0, ids->ne1, ids->ne2]
130+
// dst->src[0]: w — GGML_TYPE_ML8_4, ne[0]=K (mult. of QK_ML8=64), ne[1]=N(vocab)
131+
// dst->src[1]: cent — GGML_TYPE_F8_E4M3, ne[0]=16, ne[1]=K/QK_ML8 (shared LUT)
132+
// dst->src[2]: ids — GGML_TYPE_I32
133+
void ggml_cuda_op_ml8_get_rows(
134+
ggml_backend_cuda_context & ctx,
135+
ggml_tensor * dst);
136+
125137
// Execute a plain GGML_OP_MUL_MAT whose src[0] is a GGML_TYPE_ML8_FP8
126138
// (scaled-fp8) weight, via the no-LUT FP8-WMMA path (WEIGHT_FORMAT=0).
127139
// Unlike GGML_OP_ML8_MUL_MAT there is no centroid sidecar:

ggml/src/ggml-ml8.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,3 +327,33 @@ struct ggml_tensor * ggml_ml8_mul_mat_id(
327327
y->src[3] = ids;
328328
return y;
329329
}
330+
331+
struct ggml_tensor * ggml_ml8_get_rows(
332+
struct ggml_context * ctx,
333+
struct ggml_tensor * w,
334+
struct ggml_tensor * centroids,
335+
struct ggml_tensor * ids) {
336+
GGML_ASSERT(w != NULL);
337+
GGML_ASSERT(centroids != NULL);
338+
GGML_ASSERT(ids != NULL);
339+
GGML_ASSERT(w->type == GGML_TYPE_ML8_4);
340+
GGML_ASSERT(centroids->type == GGML_TYPE_F8_E4M3);
341+
GGML_ASSERT(ids->type == GGML_TYPE_I32);
342+
343+
// w [K, N] (N = vocab rows), gather row ids[i] → K fp32 (one embedding vector).
344+
const int64_t K = w->ne[0];
345+
GGML_ASSERT(K % QK_ML8 == 0 && "K must be a multiple of QK_ML8=64");
346+
347+
const int64_t n_groups_k = K / QK_ML8;
348+
GGML_ASSERT(centroids->ne[0] == 16);
349+
GGML_ASSERT(centroids->ne[1] == n_groups_k);
350+
351+
// Output mirrors ggml_get_rows: [K, ids->ne[0], ids->ne[1], ids->ne[2]].
352+
const int64_t ne[4] = { K, ids->ne[0], ids->ne[1], ids->ne[2] };
353+
struct ggml_tensor * y = ggml_new_tensor(ctx, GGML_TYPE_F32, 4, ne);
354+
y->op = GGML_OP_ML8_GET_ROWS;
355+
y->src[0] = w;
356+
y->src[1] = centroids;
357+
y->src[2] = ids;
358+
return y;
359+
}

ggml/src/ggml.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,9 +1179,10 @@ static const char * GGML_OP_NAME[GGML_OP_COUNT] = {
11791179
"ML8_MUL_MAT",
11801180
"ML8_APPLY_ROTATION",
11811181
"ML8_MUL_MAT_ID",
1182+
"ML8_GET_ROWS",
11821183
};
11831184

1184-
static_assert(GGML_OP_COUNT == 102, "GGML_OP_COUNT != 102");
1185+
static_assert(GGML_OP_COUNT == 103, "GGML_OP_COUNT != 103");
11851186

11861187
static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = {
11871188
"none",
@@ -1296,9 +1297,10 @@ static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = {
12961297
"ml8_mul_mat(w,centroids,x)",
12971298
"ml8_apply_rotation(x,h_a)",
12981299
"ml8_mul_mat_id(w,centroids,x,ids)",
1300+
"ml8_get_rows(w,centroids,ids)",
12991301
};
13001302

1301-
static_assert(GGML_OP_COUNT == 102, "GGML_OP_COUNT != 102");
1303+
static_assert(GGML_OP_COUNT == 103, "GGML_OP_COUNT != 103");
13021304

13031305
static_assert(GGML_OP_POOL_COUNT == 2, "GGML_OP_POOL_COUNT != 2");
13041306

0 commit comments

Comments
 (0)