sft: assistant-only SFT (ignore-index CE) + chat-prompt greedy eval
Enable assistant-only supervised fine-tuning and a fixed chat-prompt eval path used by the v12 SFT runs: - cross_entropy ignores negative targets (-100 ignore-index), normalizing by valid rows instead of all rows; CUDA fwd/bwd skip t<0 (ops.rs, nn.cu). - Corpus gains optional labels + load_sft_tsv_cached: two-column TSV is formatted as 'User: .. \nAssistant:' + answer + <|endoftext|>, prompt tokens masked to -100 while answer+EOS are supervised; i32 label cache alongside the u16 token cache; sample() retries windows that are fully masked; eval uses target_window so masking applies to val loss too (data.rs, train_loop.rs). - train + train_ddp: --sft-tsv selects the TSV loader, --init-ckpt continues training from a base checkpoint. - greedy_sample: --prompts-file/--prompt/--temperature for fixed chat-prompt generation eval. Test fixtures updated for the new Corpus.labels field; dropout.rs carries incidental rustfmt. Not rebuilt locally (no CUDA toolchain on this checkout); correctness rests on the documented v12 base+SFT runs on the GPU box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -398,7 +398,8 @@ pub fn repeat_kv(kv: &Var, nh: usize, batch: usize) -> Var {
|
||||
}
|
||||
|
||||
/// Cross-entropy mean loss over logits `x:[rows,cols]` with one I32 target per
|
||||
/// row. Returns a scalar [`Var`]. Backward: `dx = (probs - onehot)/rows`,
|
||||
/// row. Negative targets are ignored, which is useful for assistant-only SFT
|
||||
/// masks. Returns a scalar [`Var`]. Backward: `dx = (probs - onehot)/valid_rows`,
|
||||
/// scaled by the upstream scalar grad.
|
||||
pub fn cross_entropy(x: &Var, target: &Tensor) -> Var {
|
||||
// CE math is fp32 (cross_entropy upcasts bf16 logits internally + caches fp32
|
||||
@@ -407,10 +408,22 @@ pub fn cross_entropy(x: &Var, target: &Tensor) -> Var {
|
||||
// fp32 logits buffer) is a real activation-memory saving at large vocab.
|
||||
let logit_dtype = x.value().dtype();
|
||||
let (probs, per_row) = x.value().cross_entropy(target);
|
||||
let rows = x.value().shape()[0];
|
||||
let cols = x.value().shape()[1] as i32;
|
||||
let target_host = target.to_device(xtrain_tensor::Device::Cpu);
|
||||
let valid_rows = target_host
|
||||
.as_slice::<i32>()
|
||||
.iter()
|
||||
.filter(|&&t| {
|
||||
if t >= cols {
|
||||
panic!("cross_entropy target {t} out of vocab range {cols}");
|
||||
}
|
||||
t >= 0
|
||||
})
|
||||
.count()
|
||||
.max(1);
|
||||
// Mean loss as a host scalar wrapped back into a [1] tensor.
|
||||
let mean = per_row.to_device(xtrain_tensor::Device::Cpu);
|
||||
let mean_val: f32 = mean.as_slice::<f32>().iter().sum::<f32>() / rows as f32;
|
||||
let mean_val: f32 = mean.as_slice::<f32>().iter().sum::<f32>() / valid_rows as f32;
|
||||
let loss = Tensor::from_slice(&[mean_val], &[1]).to_device(x.value().device());
|
||||
|
||||
let target = target.clone();
|
||||
@@ -420,7 +433,7 @@ pub fn cross_entropy(x: &Var, target: &Tensor) -> Var {
|
||||
Box::new(move |d, parents| {
|
||||
// `d` is the scalar upstream grad (1.0 when this is the loss root).
|
||||
let upstream = d.to_device(xtrain_tensor::Device::Cpu).as_slice::<f32>()[0];
|
||||
let scale = upstream / rows as f32;
|
||||
let scale = upstream / valid_rows as f32;
|
||||
let dx = Tensor::cross_entropy_backward(&probs, &target, scale);
|
||||
Var::push_grad(&parents[0], dx.to_dtype(logit_dtype));
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user