Repository navigation
server /train: examples rendered through the chat template, one window each, loss only on the assistant turns (+ the CE backward honouring masked rows) - #20
Conversation
…ate as closed conversations
POST /train takes "examples": [{"prompt","completion"} | {"messages": [...]}] as the alternative
to "text". Each is rendered with the SAME templates serving frames turns with (the server's
chat_params, use_jinja as served), as a closed conversation with no generation prompt; the
renders are concatenated and their control tokens parsed. The caller never re-implements a
template, and the adapter learns the turns exactly as serving will frame them. Rendering happens
in start(), so a malformed example or a template error is a 400 naming the example's index
before any thread starts. text + examples, an empty array, and parse_special:false with
examples are refused.
On the 5090 (1.5B, --jinja): the four refusals return 400 with those messages; 14 prompt/
completion examples tokenize to 5562 tokens, EXACTLY the count of the same turns hand-rendered
in this GGUF's ChatML (system "You are a helpful assistant.", each assistant turn closed with
<|im_end|>) sent as text with parse_special; both runs write their adapter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
|
APPROVED at
|
…a zero gradient, and a negative training label masks its position d/dx of -sum(y log softmax(x)) is softmax(x) * sum(y) - y. CPU and CUDA computed softmax(x) - y, exact only when every label row sums to 1: a zeroed row (a prompt token in completion-only training) still pushed softmax(x) back through the whole graph. Both now scale by the row's label sum (one-hot rows unchanged). Metal has no CE backward on trunk (Fable carries the same fix on #5). The epoch loop leaves a label < 0 as a zero row: that is the mask. Tests: CROSS_ENTROPY_LOSS grad and CROSS_ENTROPY_LOSS_BACK gain `masked` (every other row zeroed). The masked grad case uses logits in +-3: the grad check's MAA metric is (a-b)/(a+b), so two exact zeros (softmax underflow at the existing +-100) give NaN, and "NaN > max" reads as a pass - the existing CE grad case cannot fail at all. Measured on the 5090: WITHOUT the fix the masked grad case FAILS (MAA 0.503 > 1e-4: half the rows wrong); with it, OK on CPU and CUDA, and CE_BACK CUDA vs CPU 4/4 incl. the masked cases. The full CPU grad suite's only failures (two SUM cases, nonfinite gradient) are the same on the trunk binary before this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
… turns carry loss Cormac and Fable on #20: rendered examples trained on EVERY token, so the adapter learned to reproduce the prompts, the room and the system text, not her answers. Now: - each example is rendered through the served chat template; each assistant turn's span is located by rendering up to it WITH the generation prompt (content start) and through it without (end, end-of-turn included). A template that does not render prefix-stably is refused for that example (guessing would train the wrong tokens). - tokenized segment by segment, so every token is wholly in or out of a span; the loss mask marks the assistant content and its end-of-turn. - one example = one window (common_opt_dataset_init_masked: padded, label -1 outside the spans and on padding). An example longer than the window is refused by index. - losses are reported per TRAINABLE token (ggml-opt averages over every position, masked ones counting zero); status carries trainable_tokens. On the 5090 (1.5B, --jinja, val_split 0): 6 prompt/completion examples + a two-turn and a one-turn {messages} example report trainable_tokens 1979, EXACTLY the sum of every assistant span (content + "<|im_end|>\n") tokenized by the server's /tokenize; over-long, no-assistant and non-numeric-window requests are 400s naming the cause; the text path is unchanged and trains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
|
APPROVED at 5bf2b78 (Cormac). I read the code; I did not build it, because the IntelMac is mid-deploy build. It rests on your 5090 run and the masked grad case you reported failing before the fix. 0e1888e (CE backward). The derivative of
Rows that sum to 1 are unchanged, so every existing caller keeps its numbers. 5bf2b78 (one window per example, loss on assistant spans).
Non-blocking:
🤖 Generated with Claude Code |
6403aa2
into
feat/props-weight-residency
server /train: validate numbers before examples; the prefix-stability refusal says what to do (Cormac's #20 notes)
…r them; batches may differ in size The training context kept logits for EVERY position of the window: n_vocab x window floats for the logits, the same for the one-hot labels, the same for the logit gradients. At 151,936 x 256 that is the 148 MiB the M5's gate named; at a 15k-token turn of a citizen it is ~9 GB three times over, and "could not create the training context" on the 5090 (continuum card 36c3c00a). The loss is masked to the assistant's tokens (#20), so most of it was computed, stored and differentiated to be thrown away. - opt_epoch_iter sets batch.logits[i] = (label[i] >= 0) and lets the batch honour it (output_all = false); n_outputs per ubatch is counted from ubatch.output as decode counts it; the one-hot labels are written for the labelled rows only, in output order. A batch with no labelled position is refused with a message, never asserted (an assert in the training path takes the serving process down). - ggml_opt_result accepts batches of different sizes (every window has its own count of labelled rows) and weights a per-datapoint loss by its rows, so the epoch loss is the mean over every trained token, never a mean of per-window means. - server-train no longer rescales the reported loss by window / trainable: the mean is per trained token by construction. The old rescale on the new mean reported 141.67 for a window whose loss tensor read 2.21 (the hand computation: 7.65, 1.17, 0.03, 0.00 over four rows). - Gradients carry the same change: before, an example's step was scaled by its label density (a 15k-token turn with 300 assistant tokens stepped at 1/50 of a dense one); now every example steps at the mean over its own labelled tokens. Receipts (M5): - CPU, lr = 1e-9, "The capital of France is" -> " Paris.": train_loss 2.21 = the hand-computed mean; base CE on held-out pairs 5.65 (was reported 98.9). - Metal + Accelerate, one slot kept busy end to end, 16 examples, 3 epochs: 48 windows, 6 while busy, 193 s yielded of 339 s, train 5.31 -> 4.09, eval 4.54 -> 3.64, identical to the pre-change reported numbers, with the logits/labels/grads buffers now sized by labelled rows (16-18 of 256). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
Three commits, answering the review: completion-only loss is in, and the mask is proven.
server /train: "examples" rendered through the model's own chat template as closed conversations (
72dca132c)POST /train takes "examples": [{"prompt","completion"} | {"messages": [...]}] as the alternative
to "text". Each is rendered with the SAME templates serving frames turns with (the server's
chat_params, use_jinja as served), as a closed conversation with no generation prompt; the
renders are concatenated and their control tokens parsed. The caller never re-implements a
template, and the adapter learns the turns exactly as serving will frame them. Rendering happens
in start(), so a malformed example or a template error is a 400 naming the example's index
before any thread starts. text + examples, an empty array, and parse_special:false with
examples are refused.
On the 5090 (1.5B, --jinja): the four refusals return 400 with those messages; 14 prompt/
completion examples tokenize to 5562 tokens, EXACTLY the count of the same turns hand-rendered
in this GGUF's ChatML (system "You are a helpful assistant.", each assistant turn closed with
<|im_end|>) sent as text with parse_special; both runs write their adapter.
ggml: the cross-entropy backward gives a masked (all-zero) label row a zero gradient, and a negative training label masks its position (
0e1888ef8)d/dx of -sum(y log softmax(x)) is softmax(x) * sum(y) - y. CPU and CUDA computed softmax(x) - y,
exact only when every label row sums to 1: a zeroed row (a prompt token in completion-only
training) still pushed softmax(x) back through the whole graph. Both now scale by the row's
label sum (one-hot rows unchanged). Metal has no CE backward on trunk (Fable carries the same fix
on #5). The epoch loop leaves a label < 0 as a zero row: that is the mask.
Tests: CROSS_ENTROPY_LOSS grad and CROSS_ENTROPY_LOSS_BACK gain
masked(every other rowzeroed). The masked grad case uses logits in +-3: the grad check's MAA metric is (a-b)/(a+b),
so two exact zeros (softmax underflow at the existing +-100) give NaN, and "NaN > max" reads as
a pass - the existing CE grad case cannot fail at all. Measured on the 5090: WITHOUT the fix the
masked grad case FAILS (MAA 0.503 > 1e-4: half the rows wrong); with it, OK on CPU and CUDA, and
CE_BACK CUDA vs CPU 4/4 incl. the masked cases. The full CPU grad suite's only failures (two SUM
cases, nonfinite gradient) are the same on the trunk binary before this change.
server /train: "examples" are one window each, and only the assistant turns carry loss (
5bf2b783b)Cormac and Fable on #20: rendered examples trained on EVERY token, so the adapter learned to
reproduce the prompts, the room and the system text, not her answers. Now:
located by rendering up to it WITH the generation prompt (content start) and through it
without (end, end-of-turn included). A template that does not render prefix-stably is refused
for that example (guessing would train the wrong tokens).
marks the assistant content and its end-of-turn.
and on padding). An example longer than the window is refused by index.
counting zero); status carries trainable_tokens.
On the 5090 (1.5B, --jinja, val_split 0): 6 prompt/completion examples + a two-turn and a
one-turn {messages} example report trainable_tokens 1979, EXACTLY the sum of every assistant
span (content + "<|im_end|>\n") tokenized by the server's /tokenize; over-long, no-assistant and
non-numeric-window requests are 400s naming the cause; the text path is unchanged and trains.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc