Monody-Exp / tiny_gdn /fused_ops.py
kerzgrr's picture
Monody-Exp: monody-pass-7 EMA weights
5c63f8f verified
Raw History Blame Contribute Delete
21.5 kB
"""Fused CUDA kernels for memory-bound blocks: SiTU-GLU, the LM loss, token log-probs."""
from __future__ import annotations
import torch
import torch.nn.functional as F
import triton
import triton.language as tl
from triton.language.extra import libdevice
from tiny_gdn.nn_common import (
linear_cross_entropy_reference,
linear_token_logprobs_reference,
situ_glu_reference,
)
BLOCK_SIZE = 1024
VOCAB_BLOCK_SIZE = 4096
LOGITS_CHUNK_BYTES = 1 << 30
@triton.jit
def _situ_glu_forward_kernel(
gate_up,
output,
total,
width,
gate_cap,
up_cap,
BLOCK: tl.constexpr,
):
offsets = tl.program_id(0).to(tl.int64) * BLOCK + tl.arange(0, BLOCK)
mask = offsets < total
row = offsets // width
gate_offsets = offsets + row * width
gate = tl.load(gate_up + gate_offsets, mask=mask, other=0.0).to(tl.float32)
up = tl.load(gate_up + gate_offsets + width, mask=mask, other=0.0).to(tl.float32)
gated = gate_cap * libdevice.tanh(gate / gate_cap) * tl.sigmoid(gate)
result = gated * up_cap * libdevice.tanh(up / up_cap)
tl.store(output + offsets, result.to(output.dtype.element_ty), mask=mask)
@triton.jit
def _situ_glu_backward_kernel(
gate_up,
grad_output,
grad_gate_up,
total,
width,
gate_cap,
up_cap,
BLOCK: tl.constexpr,
):
offsets = tl.program_id(0).to(tl.int64) * BLOCK + tl.arange(0, BLOCK)
mask = offsets < total
row = offsets // width
gate_offsets = offsets + row * width
gate = tl.load(gate_up + gate_offsets, mask=mask, other=0.0).to(tl.float32)
up = tl.load(gate_up + gate_offsets + width, mask=mask, other=0.0).to(tl.float32)
grad = tl.load(grad_output + offsets, mask=mask, other=0.0).to(tl.float32)
gate_tanh = libdevice.tanh(gate / gate_cap)
up_tanh = libdevice.tanh(up / up_cap)
sigmoid = tl.sigmoid(gate)
gated = gate_cap * gate_tanh * sigmoid
capped_up = up_cap * up_tanh
gate_slope = sigmoid * ((1.0 - gate_tanh * gate_tanh) + gate_cap * gate_tanh * (1.0 - sigmoid))
grad_gate = grad * capped_up * gate_slope
grad_up = grad * gated * (1.0 - up_tanh * up_tanh)
dtype = grad_gate_up.dtype.element_ty
tl.store(grad_gate_up + gate_offsets, grad_gate.to(dtype), mask=mask)
tl.store(grad_gate_up + gate_offsets + width, grad_up.to(dtype), mask=mask)
def _launch_grid(total: int) -> tuple[int]:
return (triton.cdiv(total, BLOCK_SIZE),)
class _SiTUGLUFunction(torch.autograd.Function):
@staticmethod
def forward(ctx, gate_up: torch.Tensor, gate_cap: float, up_cap: float) -> torch.Tensor:
gate_up = gate_up.contiguous()
width = gate_up.shape[-1] // 2
output = gate_up.new_empty((*gate_up.shape[:-1], width))
total = output.numel()
if total:
_situ_glu_forward_kernel[_launch_grid(total)](
gate_up, output, total, width, gate_cap, up_cap, BLOCK=BLOCK_SIZE
)
ctx.save_for_backward(gate_up)
ctx.caps = (gate_cap, up_cap)
return output
@staticmethod
def backward(ctx, grad_output: torch.Tensor) -> tuple[torch.Tensor, None, None]:
(gate_up,) = ctx.saved_tensors
gate_cap, up_cap = ctx.caps
grad_output = grad_output.contiguous()
grad_gate_up = torch.empty_like(gate_up)
width = gate_up.shape[-1] // 2
total = grad_output.numel()
if total:
_situ_glu_backward_kernel[_launch_grid(total)](
gate_up,
grad_output,
grad_gate_up,
total,
width,
gate_cap,
up_cap,
BLOCK=BLOCK_SIZE,
)
return grad_gate_up, None, None
def situ_glu(gate_up: torch.Tensor, gate_cap: float, up_cap: float) -> torch.Tensor:
"""``softcap(gate) * sigmoid(gate) * softcap(up)`` over ``[..., gate | up]``.
The CUDA path evaluates in FP32 and rounds once; backward recomputes the
activations from ``gate_up`` instead of saving intermediates.
"""
if gate_up.shape[-1] % 2:
raise ValueError("gate_up must have an even last dimension")
if not gate_up.is_cuda:
return situ_glu_reference(gate_up, gate_cap, up_cap)
return _SiTUGLUFunction.apply(gate_up, gate_cap, up_cap)
@triton.jit
def _cross_entropy_rows_kernel(
logits,
targets,
losses,
log_partitions,
valid_count,
row_stride,
vocab_size,
z_loss_coefficient,
ignore_index,
COMPUTE_GRAD: tl.constexpr,
BLOCK: tl.constexpr,
):
"""Per row: logsumexp and NLL; optionally overwrite the row with d(mean loss)/d(logits)."""
row = tl.program_id(0).to(tl.int64)
row_logits = logits + row * row_stride
target = tl.load(targets + row)
columns = tl.arange(0, BLOCK)
if target == ignore_index:
tl.store(losses + row, 0.0)
tl.store(log_partitions + row, 0.0)
if COMPUTE_GRAD:
for start in range(0, vocab_size, BLOCK):
offsets = start + columns
zeros = tl.zeros((BLOCK,), dtype=logits.dtype.element_ty)
tl.store(row_logits + offsets, zeros, mask=offsets < vocab_size)
else:
running_max = tl.full((), float("-inf"), tl.float32)
running_sum = tl.zeros((), dtype=tl.float32)
for start in range(0, vocab_size, BLOCK):
offsets = start + columns
values = tl.load(
row_logits + offsets, mask=offsets < vocab_size, other=float("-inf")
).to(tl.float32)
new_max = tl.maximum(running_max, tl.max(values, axis=0))
running_sum = running_sum * tl.exp(running_max - new_max) + tl.sum(
tl.exp(values - new_max), axis=0
)
running_max = new_max
log_partition = running_max + tl.log(running_sum)
target_logit = tl.load(row_logits + target).to(tl.float32)
tl.store(losses + row, log_partition - target_logit)
tl.store(log_partitions + row, log_partition)
if COMPUTE_GRAD:
grad_scale = 1.0 / tl.load(valid_count).to(tl.float32)
softmax_scale = grad_scale * (1.0 + 2.0 * z_loss_coefficient * log_partition)
for start in range(0, vocab_size, BLOCK):
offsets = start + columns
in_vocab = offsets < vocab_size
values = tl.load(row_logits + offsets, mask=in_vocab, other=0.0).to(tl.float32)
grad = tl.exp(values - log_partition) * softmax_scale
grad = tl.where(offsets == target, grad - grad_scale, grad)
tl.store(row_logits + offsets, grad.to(logits.dtype.element_ty), mask=in_vocab)
class _LinearCrossEntropyFunction(torch.autograd.Function):
"""Chunked ``states @ weight.T`` + cross-entropy + z-loss with gradients precomputed.
Each logits chunk is consumed in place, so peak extra memory is one chunk
instead of ``[tokens, vocab]`` logits plus their softmax for backward.
"""
@staticmethod
def forward(
ctx,
states: torch.Tensor,
weight: torch.Tensor,
targets: torch.Tensor,
z_loss_coefficient: float,
ignore_index: int,
compute_grad: bool,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
states = states.contiguous()
targets = targets.contiguous()
rows, vocab_size = states.shape[0], weight.shape[0]
valid = targets.ne(ignore_index)
valid_count = valid.sum(dtype=torch.float32)
losses = states.new_empty(rows, dtype=torch.float32)
log_partitions = states.new_empty(rows, dtype=torch.float32)
grad_states = torch.empty_like(states) if compute_grad else None
grad_weight = (
torch.zeros(weight.shape, dtype=torch.float32, device=weight.device)
if compute_grad
else None
)
chunk_rows = max(1, LOGITS_CHUNK_BYTES // (vocab_size * states.element_size()))
for start in range(0, rows, chunk_rows):
end = min(start + chunk_rows, rows)
chunk = states[start:end]
logits = chunk @ weight.t()
_cross_entropy_rows_kernel[(end - start,)](
logits,
targets[start:end],
losses[start:end],
log_partitions[start:end],
valid_count,
logits.stride(0),
vocab_size,
z_loss_coefficient,
ignore_index,
COMPUTE_GRAD=compute_grad,
BLOCK=VOCAB_BLOCK_SIZE,
num_warps=8,
)
if compute_grad:
torch.mm(logits, weight, out=grad_states[start:end])
if chunk.dtype == torch.float32:
grad_weight.addmm_(logits.t(), chunk)
else:
torch.addmm(
grad_weight, logits.t(), chunk, out_dtype=torch.float32, out=grad_weight
)
cross_entropy = losses.sum() / valid_count
z_loss = log_partitions.square().sum() / valid_count
objective = cross_entropy + z_loss_coefficient * z_loss
if compute_grad:
ctx.save_for_backward(grad_states, grad_weight)
ctx.weight_dtype = weight.dtype
ctx.mark_non_differentiable(cross_entropy, z_loss)
return objective, cross_entropy, z_loss
@staticmethod
def backward(
ctx, grad_objective: torch.Tensor, *_: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, None, None, None, None]:
grad_states, grad_weight = ctx.saved_tensors
scale = grad_objective.to(torch.float32)
grad_states = (grad_states * scale).to(grad_states.dtype)
grad_weight = (grad_weight * scale).to(ctx.weight_dtype)
return grad_states, grad_weight, None, None, None, None
class _WeightedLinearCrossEntropyFunction(torch.autograd.Function):
"""``Σ_i w_i (CE_i + z log Z_i²)`` over chunked ``states @ weight.T``, gradients precomputed.
The row kernel runs with unit scale and each row's gradient is then
multiplied by its weight, so per-token weights cost one in-place multiply.
"""
@staticmethod
def forward(
ctx,
states: torch.Tensor,
weight: torch.Tensor,
targets: torch.Tensor,
token_weights: torch.Tensor,
z_loss_coefficient: float,
compute_grad: bool,
excluded_token_id: int | None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
states = states.contiguous()
targets = targets.contiguous()
token_weights = token_weights.float().contiguous()
rows, vocab_size = states.shape[0], weight.shape[0]
unit = torch.ones((), dtype=torch.float32, device=states.device)
losses = states.new_empty(rows, dtype=torch.float32)
log_partitions = states.new_empty(rows, dtype=torch.float32)
grad_states = torch.empty_like(states) if compute_grad else None
grad_weight = (
torch.zeros(weight.shape, dtype=torch.float32, device=weight.device)
if compute_grad
else None
)
chunk_rows = max(1, LOGITS_CHUNK_BYTES // (vocab_size * states.element_size()))
for start in range(0, rows, chunk_rows):
end = min(start + chunk_rows, rows)
chunk = states[start:end]
logits = chunk @ weight.t()
if excluded_token_id is not None:
logits[:, excluded_token_id] = float("-inf")
_cross_entropy_rows_kernel[(end - start,)](
logits,
targets[start:end],
losses[start:end],
log_partitions[start:end],
unit,
logits.stride(0),
vocab_size,
z_loss_coefficient,
-100,
COMPUTE_GRAD=compute_grad,
BLOCK=VOCAB_BLOCK_SIZE,
num_warps=8,
)
if compute_grad:
logits.mul_(token_weights[start:end, None].to(logits.dtype))
torch.mm(logits, weight, out=grad_states[start:end])
if chunk.dtype == torch.float32:
grad_weight.addmm_(logits.t(), chunk)
else:
torch.addmm(
grad_weight, logits.t(), chunk, out_dtype=torch.float32, out=grad_weight
)
objective = (token_weights * (losses + z_loss_coefficient * log_partitions.square())).sum()
if compute_grad:
ctx.save_for_backward(grad_states, grad_weight)
ctx.weight_dtype = weight.dtype
ctx.mark_non_differentiable(losses, log_partitions)
return objective, losses, log_partitions
@staticmethod
def backward(
ctx, grad_objective: torch.Tensor, *_: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, None, None, None, None, None]:
grad_states, grad_weight = ctx.saved_tensors
scale = grad_objective.to(torch.float32)
grad_states = (grad_states * scale).to(grad_states.dtype)
grad_weight = (grad_weight * scale).to(ctx.weight_dtype)
return grad_states, grad_weight, None, None, None, None, None
def weighted_linear_cross_entropy(
states: torch.Tensor,
weight: torch.Tensor,
targets: torch.Tensor,
token_weights: torch.Tensor,
*,
z_loss_coefficient: float = 0.0,
excluded_token_id: int | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""``(Σ_i w_i (CE_i + z log Z_i²), CE [rows], log Z [rows])`` without full logits.
Every row is scored; only the weighted sum carries gradients.
``excluded_token_id`` gets probability zero (its logit is -inf).
"""
if states.ndim != 2 or targets.shape != states.shape[:1] or token_weights.shape != targets.shape:
raise ValueError("states must be [tokens, hidden], targets and token_weights [tokens]")
if not states.is_cuda:
logits = F.linear(states, weight).float()
if excluded_token_id is not None:
logits[:, excluded_token_id] = float("-inf")
log_partitions = torch.logsumexp(logits, dim=-1)
losses = log_partitions - logits.gather(1, targets[:, None]).squeeze(1)
weights = token_weights.float()
objective = (weights * (losses + z_loss_coefficient * log_partitions.square())).sum()
return objective, losses.detach(), log_partitions.detach()
compute_grad = torch.is_grad_enabled() and (states.requires_grad or weight.requires_grad)
return _WeightedLinearCrossEntropyFunction.apply(
states, weight, targets, token_weights, z_loss_coefficient, compute_grad, excluded_token_id
)
def linear_cross_entropy(
states: torch.Tensor,
weight: torch.Tensor,
targets: torch.Tensor,
*,
z_loss_coefficient: float = 0.0,
ignore_index: int = -100,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Mean ``cross_entropy(states @ weight.T, targets)`` without full logits.
Returns ``(objective, cross_entropy, z_loss)``. Only ``objective =
cross_entropy + z_loss_coefficient * z_loss`` carries gradients; ``z_loss``
is the mean squared log-partition over non-ignored targets.
"""
if states.ndim != 2 or targets.shape != states.shape[:1]:
raise ValueError("states must be [tokens, hidden] and targets [tokens]")
if not states.is_cuda:
return linear_cross_entropy_reference(
states,
weight,
targets,
z_loss_coefficient=z_loss_coefficient,
ignore_index=ignore_index,
)
compute_grad = torch.is_grad_enabled() and (states.requires_grad or weight.requires_grad)
return _LinearCrossEntropyFunction.apply(
states, weight, targets, z_loss_coefficient, ignore_index, compute_grad
)
@triton.jit
def _token_logprob_grad_rows_kernel(
logits,
targets,
log_partitions,
row_grads,
row_stride,
vocab_size,
ignore_index,
BLOCK: tl.constexpr,
):
"""Overwrite each row with ``g * (onehot(target) - softmax)``, the gradient of ``g * log p``."""
row = tl.program_id(0).to(tl.int64)
row_logits = logits + row * row_stride
target = tl.load(targets + row)
columns = tl.arange(0, BLOCK)
if target == ignore_index:
for start in range(0, vocab_size, BLOCK):
offsets = start + columns
zeros = tl.zeros((BLOCK,), dtype=logits.dtype.element_ty)
tl.store(row_logits + offsets, zeros, mask=offsets < vocab_size)
else:
scale = tl.load(row_grads + row).to(tl.float32)
log_partition = tl.load(log_partitions + row)
for start in range(0, vocab_size, BLOCK):
offsets = start + columns
in_vocab = offsets < vocab_size
values = tl.load(row_logits + offsets, mask=in_vocab, other=0.0).to(tl.float32)
grad = -scale * tl.exp(values - log_partition)
grad = tl.where(offsets == target, grad + scale, grad)
tl.store(row_logits + offsets, grad.to(logits.dtype.element_ty), mask=in_vocab)
class _LinearTokenLogprobFunction(torch.autograd.Function):
"""Per-row ``log_softmax(states @ weight.T)[target]`` without keeping logits.
Preference losses weight every sequence differently, so the gradient
cannot be precomputed in forward like the mean LM loss. Forward keeps one
log-partition per row; backward recomputes each logits chunk and turns it
into the scaled softmax gradient in place.
"""
@staticmethod
def forward(
ctx,
states: torch.Tensor,
weight: torch.Tensor,
targets: torch.Tensor,
ignore_index: int,
) -> torch.Tensor:
states = states.contiguous()
targets = targets.contiguous()
rows, vocab_size = states.shape[0], weight.shape[0]
negative_logprobs = states.new_empty(rows, dtype=torch.float32)
log_partitions = states.new_empty(rows, dtype=torch.float32)
unused_count = states.new_ones((), dtype=torch.float32)
chunk_rows = max(1, LOGITS_CHUNK_BYTES // (vocab_size * states.element_size()))
for start in range(0, rows, chunk_rows):
end = min(start + chunk_rows, rows)
logits = states[start:end] @ weight.t()
_cross_entropy_rows_kernel[(end - start,)](
logits,
targets[start:end],
negative_logprobs[start:end],
log_partitions[start:end],
unused_count,
logits.stride(0),
vocab_size,
0.0,
ignore_index,
COMPUTE_GRAD=False,
BLOCK=VOCAB_BLOCK_SIZE,
num_warps=8,
)
ctx.save_for_backward(states, weight, targets, log_partitions)
ctx.ignore_index = ignore_index
ctx.chunk_rows = chunk_rows
return negative_logprobs.neg_()
@staticmethod
def backward(
ctx, grad_logprobs: torch.Tensor
) -> tuple[torch.Tensor | None, torch.Tensor | None, None, None]:
states, weight, targets, log_partitions = ctx.saved_tensors
row_grads = grad_logprobs.to(torch.float32).contiguous()
rows, vocab_size = states.shape[0], weight.shape[0]
grad_states = torch.empty_like(states) if ctx.needs_input_grad[0] else None
grad_weight = (
torch.zeros(weight.shape, dtype=torch.float32, device=weight.device)
if ctx.needs_input_grad[1]
else None
)
for start in range(0, rows, ctx.chunk_rows):
end = min(start + ctx.chunk_rows, rows)
chunk = states[start:end]
logits = chunk @ weight.t()
_token_logprob_grad_rows_kernel[(end - start,)](
logits,
targets[start:end],
log_partitions[start:end],
row_grads[start:end],
logits.stride(0),
vocab_size,
ctx.ignore_index,
BLOCK=VOCAB_BLOCK_SIZE,
num_warps=8,
)
if grad_states is not None:
torch.mm(logits, weight, out=grad_states[start:end])
if grad_weight is not None:
if chunk.dtype == torch.float32:
grad_weight.addmm_(logits.t(), chunk)
else:
torch.addmm(
grad_weight, logits.t(), chunk, out_dtype=torch.float32, out=grad_weight
)
return (
grad_states,
None if grad_weight is None else grad_weight.to(weight.dtype),
None,
None,
)
def linear_token_logprobs(
states: torch.Tensor,
weight: torch.Tensor,
targets: torch.Tensor,
*,
ignore_index: int = -100,
) -> torch.Tensor:
"""fp32 ``log_softmax(states @ weight.T)[row, target]`` per row without full logits.
Rows whose target is ``ignore_index`` return 0 and receive no gradient.
"""
if states.ndim != 2 or targets.shape != states.shape[:1]:
raise ValueError("states must be [tokens, hidden] and targets [tokens]")
if not states.is_cuda:
return linear_token_logprobs_reference(
states, weight, targets, ignore_index=ignore_index
)
return _LinearTokenLogprobFunction.apply(states, weight, targets, ignore_index)