Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
bdd3cd5
train: one rule for the recurrent states a graph keeps, so training r…
joelteply Oct 13, 2026
e966b47
metal: OUT_PROD runs batched, broadcast and narrow shapes on the GPU,…
joelteply Oct 14, 2026
7468826
metal: a native OUT_PROD kernel reads the weight in place; the compos…
joelteply Oct 14, 2026
22f7556
metal: left padding, strided norm/softmax backward and GET_ROWS_BACK,…
joelteply Oct 14, 2026
32b8657
batch: a per-token label makes a position a training row, carried int…
joelteply Oct 14, 2026
0b8ddce
llama: a labelled ubatch is a training step inside decode; train_chun…
joelteply Oct 14, 2026
725c893
llama: the trainable layer applies to labelled rows only, and a train…
joelteply Oct 14, 2026
ee9b179
llama: the exact walk's K/V gradient holds the reply being trained, n…
joelteply Oct 14, 2026
0e6e0d9
ggml: cross-entropy takes one target index per row, so training build…
joelteply Oct 14, 2026
e231baa
ggml-opt: a training step's head is computed a slice of rows at a time
joelteply Oct 14, 2026
88c5c1e
ggml: the attention backward is one op (ggml_flash_attn_back), on CPU…
joelteply Oct 14, 2026
999d682
llama: connect labelled decode to physical cache row gradients
joelteply Oct 14, 2026
87a0f17
llama: keep serving indices and compose training admission in memory …
joelteply Oct 14, 2026
44abe56
test: compare multi-sequence label steps and isolate recompute
joelteply Oct 14, 2026
07af30f
ggml: recount graph uses after checkpoint rewriting
joelteply Oct 14, 2026
343f369
llama: separate labelled head rows from public outputs
joelteply Oct 14, 2026
eda8574
ggml: acc_or_set's zero is a node, not a full-size leaf
joelteply Oct 14, 2026
8ec4f9d
metal: the attention backward runs on simdgroup matrices, tiled, with…
joelteply Oct 14, 2026
22c112c
metal: the tiled attention backward is chosen only when its tiles fit…
joelteply Oct 15, 2026
6fc7bc6
llama: llama_opt_learn trains one reply on a live sequence of a servi…
joelteply Oct 15, 2026
4c0ee17
llama: llama_opt_learn refuses null inputs and an overlong reply befo…
joelteply Oct 15, 2026
e740f89
metal: the tiled attention backward needs simdgroup matrices (Apple7+…
joelteply Oct 15, 2026
018ab27
server: POST /learn trains a reply on the slot that served it, in the…
joelteply Oct 15, 2026
56918a7
Merge fable/metal-train-ops (native OUT_PROD, PAD left padding, strid…
joelteply Oct 15, 2026
3ee6d39
server: the learn restore runs under her own adapter set
joelteply Oct 15, 2026
e39b2b6
server: learning takes at most a tenth of the loop while another slot…
joelteply Oct 15, 2026
b75293d
ggml-cpu: the attention backward converts each K and V row once per b…
joelteply Oct 15, 2026
264f54b
Merge remote-tracking branch 'origin/cormac/cpu-attn-back' into fable…
joelteply Oct 15, 2026
85cac9d
server: /learn takes the served conversation and checks her slot stil…
joelteply Oct 15, 2026
e77b823
ggml: cross-entropy over target indices takes a weight per row
joelteply Oct 15, 2026
0135932
train: the attention backward computes K/V gradients for the run's ro…
joelteply Oct 15, 2026
8bbdf01
ggml-cpu: OUT_PROD with a quantized src0 dequantizes each src0 tile o…
joelteply Oct 15, 2026
f0045c0
ggml-cpu: OUT_PROD with an f16 src0 takes the same blocked kernel; th…
joelteply Oct 15, 2026
a0705b0
train: the training graph no longer grows with the window
joelteply Oct 15, 2026
cde8ffe
Merge remote-tracking branch 'origin/cormac/cpu-out-prod' into fable/…
joelteply Oct 15, 2026
5b8500e
ggml-opt: recompute never clones an in-place write into a buffer the …
joelteply Oct 15, 2026
c010269
tests: the CPU grad suite runs further: EXPM1 grad cases in a measura…
joelteply Oct 15, 2026
de1d97d
ggml: ggml_node_has_backward is the one answer to "can a backward run…
joelteply Oct 15, 2026
0fbbae2
Merge remote-tracking branch 'origin/cormac/grad-suite-fix' into fabl…
joelteply Oct 15, 2026
3a1c7e2
server: /learn builds the served prompt through the same parser /v1/c…
joelteply Oct 15, 2026
56507cc
server: one serving tokenizer for chat and /learn; /learn refuses rep…
joelteply Oct 15, 2026
b7c92db
llama: a labelled position carries a weight in the loss (llama_batch.…
joelteply Oct 15, 2026
9a10993
server: a completion can carry a learn, run on its own slot before th…
joelteply Oct 15, 2026
0cb014e
server: a learn the request could not run (an empty prompt) is report…
joelteply Oct 15, 2026
d5a4c77
server: every final-response format carries the learn outcome; the An…
joelteply Oct 15, 2026
cc2e05f
server tests: learn-every-format.py imports what it uses (the committ…
joelteply Oct 15, 2026
35b8eed
server: every training path refuses repacked CPU weights alike (conti…
joelteply Oct 15, 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
13 changes: 13 additions & 0 deletions common/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2401,8 +2401,21 @@ bool common_lora_write_fresh(const llama_model * model, const std::string & path
std::vector<shape> shapes;
const int32_t n_layer = llama_model_n_layer(model);
const int32_t first = top_layers > 0 && top_layers < n_layer ? n_layer - top_layers : 0;
for (const auto & target : targets) {
if (target == "token_embd.weight") {
LOG_ERR("%s: embedding adapters require the embedding-specific layout\n", __func__);
return false;
}
const ggml_tensor * w = llama_model_get_tensor(model, target.c_str());
if (w != nullptr && ggml_n_dims(w) == 2) {
shapes.push_back({ target, w->ne[0], w->ne[1] });
}
}
for (int32_t il = first; il < n_layer; ++il) {
for (const auto & target : targets) {
if (llama_model_get_tensor(model, target.c_str()) != nullptr) {
continue;
}
const std::string name = "blk." + std::to_string(il) + "." + target + ".weight";
const ggml_tensor * w = llama_model_get_tensor(model, name.c_str());
if (w != nullptr && ggml_n_dims(w) == 2) {
Expand Down
2 changes: 2 additions & 0 deletions common/speculative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl {
/*.n_seq_id =*/ nullptr,
/*.seq_id =*/ nullptr,
/*.logits =*/ nullptr,
/*.label =*/ nullptr,
};
const int32_t rc = llama_encode(ctx_dft, enc_batch);
if (rc != 0) {
Expand Down Expand Up @@ -1126,6 +1127,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
/*.n_seq_id =*/ nullptr,
/*.seq_id =*/ nullptr,
/*.logits =*/ nullptr,
/*.label =*/ nullptr,
};

int32_t rc = llama_encode(ctx_dft, enc_batch);
Expand Down
2 changes: 2 additions & 0 deletions examples/parallel/parallel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,8 @@ int main(int argc, char ** argv) {
batch.n_seq_id + i,
batch.seq_id + i,
batch.logits + i,
batch.label ? batch.label + i : nullptr,
batch.label && batch.label_weight ? batch.label_weight + i : nullptr,
};

const int ret = llama_decode(ctx, batch_view);
Expand Down
54 changes: 53 additions & 1 deletion ggml/include/ggml-opt.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ extern "C" {
GGML_OPT_LOSS_TYPE_SUM,
GGML_OPT_LOSS_TYPE_CROSS_ENTROPY,
GGML_OPT_LOSS_TYPE_MEAN_SQUARED_ERROR,
// GGML_OPT_LOSS_TYPE_CROSS_ENTROPY with one I32 target per output row (-1: no loss) in place of a one-hot
// row as wide as the outputs: what a language model's training step needs (its outputs are
// vocabulary-wide logits)
GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_TARGETS,
};

// ====== Dataset ======
Expand Down Expand Up @@ -154,7 +158,13 @@ extern "C" {
// if not using static graphs these pointers become invalid with the next call to ggml_opt_alloc
GGML_API struct ggml_tensor * ggml_opt_inputs( ggml_opt_context_t opt_ctx); // forward graph input tensor
GGML_API struct ggml_tensor * ggml_opt_outputs( ggml_opt_context_t opt_ctx); // forward graph output tensor
GGML_API struct ggml_tensor * ggml_opt_labels( ggml_opt_context_t opt_ctx); // labels to compare outputs against
GGML_API struct ggml_tensor * ggml_opt_labels( ggml_opt_context_t opt_ctx);
// the per-row weights of a weighted cross-entropy step (F32, one per label), or NULL when the
// graph prepared last is unweighted (ggml_opt_set_next_label_weights)
GGML_API struct ggml_tensor * ggml_opt_label_weights(ggml_opt_context_t opt_ctx);
// whether the next graph's cross-entropy weighs each row (by ggml_opt_label_weights); an
// unweighted step keeps the unweighted loss, whose kernels every backend has
GGML_API void ggml_opt_set_next_label_weights(ggml_opt_context_t opt_ctx, bool weighted); // labels to compare outputs against
GGML_API struct ggml_tensor * ggml_opt_loss( ggml_opt_context_t opt_ctx); // scalar tensor that contains the loss
GGML_API struct ggml_tensor * ggml_opt_pred( ggml_opt_context_t opt_ctx); // predictions made by outputs
GGML_API struct ggml_tensor * ggml_opt_ncorrect(ggml_opt_context_t opt_ctx); // number of matching predictions between outputs and labels
Expand Down Expand Up @@ -188,6 +198,48 @@ extern "C" {
struct ggml_tensor * inputs,
struct ggml_tensor * outputs);

// The same, for an `outputs` that is a row-wise function of `hidden` (a language model's
// head: logits = W x, with its adapters and scales; MUL_MAT, MUL, ADD and SCALE nodes): that
// head is taken out of gf and computed for `rows` rows of hidden at a time, and the loss is
// the mean over every row (GGML_OPT_LOSS_TYPE_CROSS_ENTROPY_TARGETS only). Under recompute
// (checkpoint_prefix) only one slice's outputs and their gradient exist at a time, never the
// [n_outputs x rows] tensor and its gradient. A head with any other node is not sliced.
// Returns the number of slices (1: not sliced; accuracy is reported only then).
GGML_API int ggml_opt_prepare_alloc_head(
ggml_opt_context_t opt_ctx,
struct ggml_context * ctx_compute,
struct ggml_cgraph * gf,
struct ggml_tensor * inputs,
struct ggml_tensor * hidden,
struct ggml_tensor * outputs,
int64_t rows);

// Project the existing row-wise head before consumers such as sampling are built.
// Row indices address hidden's token rows. A null public_rows requests no public logits.
// Returns zero without changing gf when the head cannot be replayed.
GGML_API int ggml_opt_project_head(
struct ggml_context * ctx_compute,
struct ggml_cgraph * gf,
struct ggml_tensor * hidden,
struct ggml_tensor * outputs,
struct ggml_tensor * shadow_rows,
struct ggml_tensor * public_rows,
struct ggml_tensor * label_rows,
struct ggml_tensor * public_shadow_rows,
struct ggml_tensor ** slice_shadow_rows,
int64_t rows,
struct ggml_tensor ** public_outputs,
struct ggml_tensor ** slices,
size_t slices_capacity);

GGML_API void ggml_opt_prepare_alloc_slices(
ggml_opt_context_t opt_ctx,
struct ggml_context * ctx_compute,
struct ggml_cgraph * gf,
struct ggml_tensor * inputs,
struct ggml_tensor ** slices,
size_t n_slices);

// allocate the next graph for evaluation, either forward or forward + backward
// must be called exactly once prior to calling ggml_opt_eval
// returns false when the graph could not be allocated (device memory): the graph is then NOT
Expand Down
57 changes: 54 additions & 3 deletions ggml/include/ggml.h
Original file line number Diff line number Diff line change
Expand Up @@ -2480,14 +2480,47 @@ extern "C" {
struct ggml_tensor * a,
struct ggml_tensor * sinks);

// TODO: needs to be adapted to ggml_flash_attn_ext
// The gradients of ggml_flash_attn_ext(q, k, v, mask, scale) (no ALiBi, softcap or sinks), given
// d, the gradient of its output in the output's layout [Dv, H, N]. One op, recomputing the
// scores per block instead of keeping them: memory is the gradients alone. The result is F32:
// dQ ([D, N, H]), dK (k's shape), dV (v's shape), each contiguous at
// ggml_flash_attn_back_offset, then each query row's log-sum-exp and D = sum_j p_ij dp_ij,
// which the kernels pass between their passes, and the power of two dO is scaled by in a
// kernel's reduced-precision tiles (ggml_flash_attn_back_scale).
// dK and dV cover the key rows [kv0, M) only (row j at j - kv0): keys before kv0 are constants
// whose gradient nobody reads (a frozen served prefix), so it is neither computed nor stored.
// dQ always sums over every key.
GGML_API struct ggml_tensor * ggml_flash_attn_back(
struct ggml_context * ctx,
struct ggml_tensor * q,
struct ggml_tensor * k,
struct ggml_tensor * v,
struct ggml_tensor * mask,
struct ggml_tensor * d,
bool masked);
float scale,
int64_t kv0);

// byte offset in a ggml_flash_attn_back result of: 0 dQ, 1 dK, 2 dV, 3 the per-row scratch
// the power of two that brings the largest |dO| to [4, 8): training gradients sit far below half
// precision's normal range (6.1e-5) and lose their precision in half tiles; 1 when dO is zero
// (the Metal kernel computes the same rule, kernel_flash_attn_back_scale)
GGML_API float ggml_flash_attn_back_scale(float max_abs_do);
GGML_API size_t ggml_flash_attn_back_offset(const struct ggml_tensor * back, int part);

// Where FLASH_ATTN_EXT's K/V gradient goes when only the last rows of its K/V can carry one:
// k_tap / v_tap (or NULL) and k_cur / v_cur, each [D, Hkv, rows], hold the values of K/V's last
// rows in order, ending at M (a run's cached rows, then the chunk's own). The backward computes
// dK/dV from the first of those rows only and adds each part to its sink; the K/V the attention
// reads (the whole window) get no gradient. The sinks are not read by the forward.
GGML_API void ggml_flash_attn_ext_set_grad_sinks(
struct ggml_tensor * a,
struct ggml_tensor * k_tap,
struct ggml_tensor * k_cur,
struct ggml_tensor * v_tap,
struct ggml_tensor * v_cur);

// the backward of FLASH_ATTN_EXT as the one op above (ggml_flash_attn_ext_set_back_tile)
#define GGML_FA_BACK_NATIVE (-1)

GGML_API struct ggml_tensor * ggml_ssm_conv(
struct ggml_context * ctx,
Expand Down Expand Up @@ -2779,7 +2812,21 @@ extern "C" {
int n_tasks,
void * userdata);

// loss function
// loss function: the mean over rows of -sum(labels * log softmax(logits)). The labels are
// either a distribution per row (F32, the logits' shape) or one target index per row (I32,
// one per row of the logits, -1 for a row that carries no loss): a target index never builds
// a vocabulary-wide one-hot tensor beside the logits.

GGML_API bool ggml_cross_entropy_labels_ok(const struct ggml_tensor * logits, const struct ggml_tensor * labels);

// The same with one target index per row (I32) and a weight per row (F32, one per row): row i
// contributes w_i * -log softmax(logits_i)[t_i] / rows, and its gradient scales by w_i. How much a
// row counts is the caller's to say (a mind's relevance on what it perceived), not only whether.
GGML_API struct ggml_tensor * ggml_cross_entropy_loss_weighted(
struct ggml_context * ctx,
struct ggml_tensor * logits,
struct ggml_tensor * targets,
struct ggml_tensor * weights);

GGML_API struct ggml_tensor * ggml_cross_entropy_loss(
struct ggml_context * ctx,
Expand Down Expand Up @@ -2852,6 +2899,10 @@ extern "C" {
struct ggml_cgraph * cgraph,
struct ggml_tensor * tensor);

// whether a backward can be built through this node (ggml_build_backward_expand aborts on one
// that cannot: an op without a backward, or an in-place write)
GGML_API bool ggml_node_has_backward(const struct ggml_tensor * tensor);

GGML_API void ggml_build_backward_expand(
struct ggml_context * ctx, // context for gradient computation
struct ggml_cgraph * cgraph,
Expand Down
29 changes: 9 additions & 20 deletions ggml/src/ggml-cpu/ggml-cpu.c
Original file line number Diff line number Diff line change
Expand Up @@ -2003,10 +2003,7 @@ static void ggml_compute_forward(struct ggml_compute_params * params, struct ggm
} break;
case GGML_OP_FLASH_ATTN_BACK:
{
int32_t t = ggml_get_op_params_i32(tensor, 0);
GGML_ASSERT(t == 0 || t == 1);
bool masked = t != 0;
ggml_compute_forward_flash_attn_back(params, masked, tensor);
ggml_compute_forward_flash_attn_back(params, tensor);
} break;
case GGML_OP_SSM_CONV:
{
Expand Down Expand Up @@ -2887,9 +2884,9 @@ struct ggml_cplan ggml_graph_plan(
} break;
case GGML_OP_OUT_PROD:
{
if (ggml_is_quantized(node->src[0]->type) ||
node->src[0]->type == GGML_TYPE_F16) {
cur = ggml_type_size(GGML_TYPE_F32) * node->src[0]->ne[0] * n_tasks;
if (ggml_is_quantized(node->src[0]->type) || node->src[0]->type == GGML_TYPE_F16) {
// the blocked kernel's tile and gathered values (ops.h)
cur = ggml_type_size(GGML_TYPE_F32) * ggml_out_prod_cpu_wsize(node->src[0]->ne[0]) * n_tasks;
}
} break;
case GGML_OP_SET_ROWS:
Expand Down Expand Up @@ -2975,19 +2972,11 @@ struct ggml_cplan ggml_graph_plan(
} break;
case GGML_OP_FLASH_ATTN_BACK:
{
const int64_t D = node->src[0]->ne[0];
const int64_t ne11 = ggml_up(node->src[1]->ne[1], GGML_SOFT_MAX_UNROLL);
const int64_t mxDn = MAX(D, ne11) * 2; // *2 because of S and SM in ggml_compute_forward_flash_attn_back
if (node->src[1]->type == GGML_TYPE_F32) {
cur = sizeof(float)*mxDn*n_tasks; // TODO: this can become (n_tasks-1)
cur += sizeof(float)*mxDn*n_tasks; // this is overestimated by x2
} else if (node->src[1]->type == GGML_TYPE_F16) {
cur = sizeof(float)*mxDn*n_tasks; // TODO: this can become (n_tasks-1)
cur += sizeof(float)*mxDn*n_tasks; // this is overestimated by x2
} else if (node->src[1]->type == GGML_TYPE_BF16) {
cur = sizeof(float)*mxDn*n_tasks; // TODO: this can become (n_tasks-1)
cur += sizeof(float)*mxDn*n_tasks; // this is overestimated by x2
}
// per thread: the blocked kernel's scratch (ops.h)
const int64_t D = node->src[0]->ne[0];
const int64_t M = node->src[1]->ne[1];
const int64_t Dv = node->src[2]->ne[0];
cur = sizeof(float)*ggml_flash_attn_back_cpu_wsize(D, Dv, M)*n_tasks;
} break;

case GGML_OP_CROSS_ENTROPY_LOSS:
Expand Down
2 changes: 1 addition & 1 deletion ggml/src/ggml-cpu/ggml-cpu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ static bool ggml_backend_cpu_device_supports_op(ggml_backend_dev_t dev, const st
return src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16;
case GGML_OP_OUT_PROD:
return (src0->type == GGML_TYPE_F32 ||
((src0->type == GGML_TYPE_F16 || ggml_is_quantized(src0->type)) && src0->ne[2] == src1->ne[2] && src0->ne[3] == src1->ne[3])) &&
((src0->type == GGML_TYPE_F16 || ggml_is_quantized(src0->type)) && src1->ne[2] % src0->ne[2] == 0 && src1->ne[3] % src0->ne[3] == 0)) &&
src1->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32;
case GGML_OP_CONV_2D:
return ggml_is_contiguous(op->src[0]);
Expand Down
Loading
Loading