qgallouedec HF Staff commited on
Commit
00e29bd
·
verified ·
1 Parent(s): 49cc157

Metrics return sums (num_correct_tokens, entropy_sum) instead of means

Browse files
Files changed (2) hide show
  1. README.md +19 -7
  2. build/torch-cuda/fused_linear_ce.py +26 -17
README.md CHANGED
@@ -38,23 +38,35 @@ fused_linear_cross_entropy(
38
  softcap=None,
39
  reduction="mean", # "mean" | "sum" | "none"
40
  return_metrics=False,
41
- ) -> loss | (loss, {"mean_token_accuracy": ..., "entropy": ...})
42
  ```
43
 
44
  `hidden`, `weight` and `bias` all receive gradients.
45
 
46
  ### Metrics for free
47
 
48
- Trainers that switch to a fused loss normally lose their `mean_token_accuracy` and `entropy`
49
- logging, because there are no logits left to compute them from. This kernel carries both through
50
- the same vocabulary sweep it already performs — the argmax rides along with the running row max,
51
- and the entropy term `sum_j p_j * logit_j` is accumulated with the same online rescaling as the
52
  log-sum-exp:
53
 
54
  ```python
55
  loss, metrics = fused_linear_cross_entropy(hidden, weight, targets, return_metrics=True)
56
- metrics["mean_token_accuracy"] # scalar, over non-ignored positions
57
- metrics["entropy"] # scalar, mean softmax entropy in nats
 
 
 
 
 
 
 
 
 
 
 
 
58
  ```
59
 
60
  Both are forward-only. `return_metrics=True` leaves the loss and the gradients **bit-identical**
 
38
  softcap=None,
39
  reduction="mean", # "mean" | "sum" | "none"
40
  return_metrics=False,
41
+ ) -> loss | (loss, {"num_correct_tokens": ..., "entropy_sum": ...})
42
  ```
43
 
44
  `hidden`, `weight` and `bias` all receive gradients.
45
 
46
  ### Metrics for free
47
 
48
+ Trainers that switch to a fused loss normally lose their token-accuracy and entropy logging,
49
+ because there are no logits left to compute them from. This kernel carries both through the same
50
+ vocabulary sweep it already performs — the argmax rides along with the running row max, and the
51
+ entropy term `sum_j p_j * logit_j` is accumulated with the same online rescaling as the
52
  log-sum-exp:
53
 
54
  ```python
55
  loss, metrics = fused_linear_cross_entropy(hidden, weight, targets, return_metrics=True)
56
+ metrics["num_correct_tokens"] # int64 scalar, count of argmax == target
57
+ metrics["entropy_sum"] # float32 scalar, summed softmax entropy in nats
58
+ ```
59
+
60
+ **Both are sums over non-ignored positions, not means.** Sums are the more general contract: a
61
+ per-rank mean cannot be recombined across distributed ranks holding different token counts, so a
62
+ distributed trainer must gather sums before dividing, and the accuracy count stays an exact
63
+ integer instead of a float that callers have to multiply back up. A single-process caller
64
+ recovers the means by dividing by a count it already has:
65
+
66
+ ```python
67
+ n = (targets != -100).sum()
68
+ mean_token_accuracy = metrics["num_correct_tokens"] / n
69
+ mean_entropy = metrics["entropy_sum"] / n
70
  ```
71
 
72
  Both are forward-only. `return_metrics=True` leaves the loss and the gradients **bit-identical**
build/torch-cuda/fused_linear_ce.py CHANGED
@@ -369,24 +369,24 @@ class _FusedLinearCrossEntropy(torch.autograd.Function):
369
 
370
  if return_metrics:
371
  t = (t_p * alpha).sum(0)
372
- entropy = torch.where(valid, lse - t / s, torch.zeros((), dtype=torch.float32, device=device))
373
- entropy = entropy.sum() / n_valid
374
  argmax = a_p.gather(0, gsrc.unsqueeze(0)).squeeze(0)
375
- accuracy = ((argmax == targets) & valid).sum().float() / n_valid
376
  else:
377
- entropy = None
378
- accuracy = None
379
 
380
  ctx.save_for_backward(hidden, weight, bias, targets, lse, valid, n_valid)
381
  ctx.ignore_index = ignore_index
382
  ctx.logit_scale = logit_scale
383
  ctx.softcap = softcap
384
  ctx.reduction = reduction
385
- ctx.mark_non_differentiable(*[x for x in (entropy, accuracy) if x is not None])
386
- return loss, entropy, accuracy
387
 
388
  @staticmethod
389
- def backward(ctx, dloss, dentropy, daccuracy):
390
  hidden, weight, bias, targets, lse, valid, n_valid = ctx.saved_tensors
391
  N, H = hidden.shape
392
  V = weight.shape[0]
@@ -502,22 +502,31 @@ def fused_linear_cross_entropy(
502
  One of `"mean"` (sum over non-ignored positions divided by their count), `"sum"` or
503
  `"none"` (per-position losses, `0` at ignored positions).
504
  return_metrics (`bool`, *optional*, defaults to `False`):
505
- Whether to also return `mean_token_accuracy` and `entropy`, computed in the same pass
506
- as the loss. Enabling this does not change the loss or the gradients.
 
 
 
 
507
 
508
  Returns:
509
  `torch.Tensor` when `return_metrics=False`, otherwise a `tuple` of the loss and a `dict`
510
- with keys:
511
- - `mean_token_accuracy` (`torch.Tensor`):
512
- Scalar, fraction of non-ignored positions whose argmax logit is the target.
513
- - `entropy` (`torch.Tensor`):
514
- Scalar, mean softmax entropy (in nats) over non-ignored positions.
 
 
515
 
516
  Examples:
517
 
518
  ```python
519
  >>> loss = fused_linear_cross_entropy(hidden, lm_head.weight, labels)
520
  >>> loss, metrics = fused_linear_cross_entropy(hidden, lm_head.weight, labels, return_metrics=True)
 
 
 
521
  ```
522
  """
523
  if hidden.ndim != 2:
@@ -535,9 +544,9 @@ def fused_linear_cross_entropy(
535
  if bias is not None:
536
  bias = bias.contiguous()
537
 
538
- loss, entropy, accuracy = _FusedLinearCrossEntropy.apply(
539
  hidden, weight, targets, bias, ignore_index, logit_scale, softcap, reduction, return_metrics
540
  )
541
  if return_metrics:
542
- return loss, {"mean_token_accuracy": accuracy, "entropy": entropy}
543
  return loss
 
369
 
370
  if return_metrics:
371
  t = (t_p * alpha).sum(0)
372
+ per_token_entropy = torch.where(valid, lse - t / s, torch.zeros((), dtype=torch.float32, device=device))
373
+ entropy_sum = per_token_entropy.sum()
374
  argmax = a_p.gather(0, gsrc.unsqueeze(0)).squeeze(0)
375
+ num_correct_tokens = ((argmax == targets) & valid).sum()
376
  else:
377
+ entropy_sum = None
378
+ num_correct_tokens = None
379
 
380
  ctx.save_for_backward(hidden, weight, bias, targets, lse, valid, n_valid)
381
  ctx.ignore_index = ignore_index
382
  ctx.logit_scale = logit_scale
383
  ctx.softcap = softcap
384
  ctx.reduction = reduction
385
+ ctx.mark_non_differentiable(*[x for x in (entropy_sum, num_correct_tokens) if x is not None])
386
+ return loss, entropy_sum, num_correct_tokens
387
 
388
  @staticmethod
389
+ def backward(ctx, dloss, dentropy_sum, dnum_correct_tokens):
390
  hidden, weight, bias, targets, lse, valid, n_valid = ctx.saved_tensors
391
  N, H = hidden.shape
392
  V = weight.shape[0]
 
502
  One of `"mean"` (sum over non-ignored positions divided by their count), `"sum"` or
503
  `"none"` (per-position losses, `0` at ignored positions).
504
  return_metrics (`bool`, *optional*, defaults to `False`):
505
+ Whether to also return `num_correct_tokens` and `entropy_sum`, computed in the same
506
+ pass as the loss. Both are **sums over non-ignored positions**, not means: a mean
507
+ cannot be recombined across distributed ranks holding different token counts, and the
508
+ accuracy count is exact as an integer. Divide by the number of non-ignored positions
509
+ to recover the per-token means. Enabling this does not change the loss or the
510
+ gradients.
511
 
512
  Returns:
513
  `torch.Tensor` when `return_metrics=False`, otherwise a `tuple` of the loss and a `dict`
514
+ whose values are both **sums over non-ignored positions**:
515
+ - `num_correct_tokens` (`torch.Tensor`):
516
+ Scalar `int64`, the number of non-ignored positions whose argmax logit is the
517
+ target.
518
+ - `entropy_sum` (`torch.Tensor`):
519
+ Scalar `float32`, the summed softmax entropy (in nats) over non-ignored
520
+ positions.
521
 
522
  Examples:
523
 
524
  ```python
525
  >>> loss = fused_linear_cross_entropy(hidden, lm_head.weight, labels)
526
  >>> loss, metrics = fused_linear_cross_entropy(hidden, lm_head.weight, labels, return_metrics=True)
527
+ >>> n = (labels != -100).sum()
528
+ >>> mean_token_accuracy = metrics["num_correct_tokens"] / n
529
+ >>> mean_entropy = metrics["entropy_sum"] / n
530
  ```
531
  """
532
  if hidden.ndim != 2:
 
544
  if bias is not None:
545
  bias = bias.contiguous()
546
 
547
+ loss, entropy_sum, num_correct_tokens = _FusedLinearCrossEntropy.apply(
548
  hidden, weight, targets, bias, ignore_index, logit_scale, softcap, reduction, return_metrics
549
  )
550
  if return_metrics:
551
+ return loss, {"num_correct_tokens": num_correct_tokens, "entropy_sum": entropy_sum}
552
  return loss