Download pssa_kernels.py from NSC64/pssa-100m: direct link, hf CLI and curl.
- Browser
- Download file 6.86 kB
-
https://huggingface.co/NSC64/pssa-100m/resolve/main/pssa_kernels.py
- Command line
-
hf download hf://NSC64/pssa-100m/pssa_kernels.py
-
curl -L -o pssa_kernels.py https://huggingface.co/NSC64/pssa-100m/resolve/main/pssa_kernels.py
6.86 kB
| """Fused Triton scan for the PSSA selective diagonal state-space recurrence. | |
| h_t = exp(delta_t * A) * h_{t-1} + delta_t * b_t * x_t | |
| y_t = <h_t, c_t> | |
| `A` is (D, DS) negative, `delta` is (B, T, D) positive, `b`/`c` are (B, T, DS). | |
| The whole (forward + backward) runs as one kernel launch per chunk, with the | |
| per-token state `h_t` materialised so the backward pass is a single reverse | |
| sweep. Carries `h0` across chunks and returns `hT`. | |
| """ | |
| import torch | |
| import triton | |
| import triton.language as tl | |
| __all__ = ["ScanFn", "scan_reference", "selftest"] | |
| def scan_fwd(x_ptr, d_ptr, bp_ptr, cp_ptr, A_ptr, h0_ptr, y_ptr, hT_ptr, H_ptr, | |
| T: tl.constexpr, D: tl.constexpr, DS: tl.constexpr, BD: tl.constexpr): | |
| b = tl.program_id(0) | |
| pd = tl.program_id(1) | |
| od = pd * BD + tl.arange(0, BD) | |
| os_ = tl.arange(0, DS) | |
| A = tl.load(A_ptr + od[:, None] * DS + os_[None, :]) | |
| h = tl.load(h0_ptr + b * D * DS + od[:, None] * DS + os_[None, :]) | |
| for t in range(T): | |
| x = tl.load(x_ptr + b * T * D + t * D + od).to(tl.float32) | |
| dl = tl.load(d_ptr + b * T * D + t * D + od).to(tl.float32) | |
| bp = tl.load(bp_ptr + b * T * DS + t * DS + os_).to(tl.float32) | |
| cp = tl.load(cp_ptr + b * T * DS + t * DS + os_).to(tl.float32) | |
| h = tl.exp(dl[:, None] * A) * h + dl[:, None] * bp[None, :] * x[:, None] | |
| tl.store(y_ptr + b * T * D + t * D + od, tl.sum(h * cp[None, :], axis=1)) | |
| tl.store(H_ptr + b * T * D * DS + t * D * DS + od[:, None] * DS + os_[None, :], h) | |
| tl.store(hT_ptr + b * D * DS + od[:, None] * DS + os_[None, :], h) | |
| def scan_bwd(dy_ptr, x_ptr, d_ptr, bp_ptr, cp_ptr, A_ptr, H_ptr, h0_ptr, | |
| gh0_ptr, gx_ptr, gd_ptr, gbp_ptr, gcp_ptr, gA_ptr, | |
| T: tl.constexpr, D: tl.constexpr, DS: tl.constexpr, BD: tl.constexpr): | |
| b = tl.program_id(0) | |
| pd = tl.program_id(1) | |
| od = pd * BD + tl.arange(0, BD) | |
| os_ = tl.arange(0, DS) | |
| A = tl.load(A_ptr + od[:, None] * DS + os_[None, :]) | |
| g = tl.zeros((BD, DS), tl.float32) | |
| gA = tl.zeros((BD, DS), tl.float32) | |
| for t in range(T - 1, -1, -1): | |
| dy = tl.load(dy_ptr + b * T * D + t * D + od).to(tl.float32) | |
| x = tl.load(x_ptr + b * T * D + t * D + od).to(tl.float32) | |
| dl = tl.load(d_ptr + b * T * D + t * D + od).to(tl.float32) | |
| bp = tl.load(bp_ptr + b * T * DS + t * DS + os_).to(tl.float32) | |
| cp = tl.load(cp_ptr + b * T * DS + t * DS + os_).to(tl.float32) | |
| h = tl.load(H_ptr + b * T * D * DS + t * D * DS + od[:, None] * DS + os_[None, :]) | |
| if t < T - 1: | |
| dln = tl.load(d_ptr + b * T * D + (t + 1) * D + od).to(tl.float32) | |
| g = dy[:, None] * cp[None, :] + tl.exp(dln[:, None] * A) * g | |
| else: | |
| g = dy[:, None] * cp[None, :] | |
| if t > 0: | |
| hp = tl.load(H_ptr + b * T * D * DS + (t - 1) * D * DS + od[:, None] * DS + os_[None, :]) | |
| else: | |
| hp = tl.load(h0_ptr + b * D * DS + od[:, None] * DS + os_[None, :]) | |
| a = tl.exp(dl[:, None] * A) | |
| ga = g * hp | |
| gd = tl.sum(ga * a * A, axis=1) + tl.sum(g * bp[None, :] * x[:, None], axis=1) | |
| gbp = tl.sum(g * dl[:, None] * x[:, None], axis=0) | |
| gcp = tl.sum(dy[:, None] * h, axis=0) | |
| gx = tl.sum(g * dl[:, None] * bp[None, :], axis=1) | |
| gA += ga * a * dl[:, None] | |
| if t == 0: | |
| tl.store(gh0_ptr + b * D * DS + od[:, None] * DS + os_[None, :], a * g) | |
| tl.store(gx_ptr + b * T * D + t * D + od, gx) | |
| tl.store(gd_ptr + b * T * D + t * D + od, gd) | |
| tl.atomic_add(gbp_ptr + b * T * DS + t * DS + os_, gbp) | |
| tl.atomic_add(gcp_ptr + b * T * DS + t * DS + os_, gcp) | |
| tl.atomic_add(gA_ptr + od[:, None] * DS + os_[None, :], gA) | |
| class ScanFn(torch.autograd.Function): | |
| """Differentiable fused scan. Returns (y, hT); hT is not differentiated.""" | |
| def forward(ctx, x, delta, bp, cp, A, h0, bd=16): | |
| B, T, D = x.shape | |
| DS = A.shape[1] | |
| y = torch.empty(B, T, D, device=x.device, dtype=torch.float32) | |
| hT = torch.empty(B, D, DS, device=x.device, dtype=torch.float32) | |
| H = torch.empty(B, T, D, DS, device=x.device, dtype=torch.float32) | |
| scan_fwd[(B, triton.cdiv(D, bd))]( | |
| x, delta, bp, cp, A, h0, y, hT, H, T=T, D=D, DS=DS, BD=bd) | |
| ctx.save_for_backward(x, delta, bp, cp, A, H, h0) | |
| ctx.bd = bd | |
| ctx.mark_non_differentiable(hT) | |
| return y, hT | |
| def backward(ctx, dy, dhT): | |
| x, delta, bp, cp, A, H, h0 = ctx.saved_tensors | |
| B, T, D = x.shape | |
| DS = A.shape[1] | |
| bd = ctx.bd | |
| gx = torch.empty(B, T, D, device=x.device, dtype=torch.float32) | |
| gd = torch.empty_like(delta, dtype=torch.float32) | |
| gbp = torch.zeros_like(bp, dtype=torch.float32) | |
| gcp = torch.zeros_like(cp, dtype=torch.float32) | |
| gA = torch.zeros_like(A, dtype=torch.float32) | |
| gh0 = torch.zeros(B, D, DS, device=x.device, dtype=torch.float32) | |
| scan_bwd[(B, triton.cdiv(D, bd))]( | |
| dy.contiguous(), x, delta, bp, cp, A, H, h0, gh0, | |
| gx, gd, gbp, gcp, gA, T=T, D=D, DS=DS, BD=bd) | |
| return gx.to(x.dtype), gd, gbp, gcp, gA, gh0.to(h0.dtype), None | |
| def scan_reference(x, delta, bp, cp, A, h0): | |
| """Plain PyTorch loop, for tests. Returns (y, H, hT).""" | |
| B, T, D = x.shape | |
| h = h0 | |
| ys, hs = [], [] | |
| for t in range(T): | |
| h = torch.exp(delta[:, t, :, None] * A) * h + delta[:, t, :, None] * bp[:, t, None, :] * x[:, t, :, None] | |
| ys.append((h * cp[:, t, None, :]).sum(-1)) | |
| hs.append(h) | |
| return torch.stack(ys, 1), torch.stack(hs, 1), h | |
| def selftest(ds=16, d=64, b=2, t=7, verbose=True): | |
| torch.manual_seed(1) | |
| dev = "cuda" | |
| x = torch.randn(b, t, d, device=dev, requires_grad=True) | |
| delta = torch.nn.functional.softplus(torch.randn(b, t, d, device=dev)).requires_grad_(True) | |
| bp = torch.randn(b, t, ds, device=dev, requires_grad=True) | |
| cp = torch.randn(b, t, ds, device=dev, requires_grad=True) | |
| A = (-torch.nn.functional.softplus(torch.randn(d, ds, device=dev))).requires_grad_(True) | |
| h0 = (torch.randn(b, d, ds, device=dev) * 0.5).requires_grad_(True) | |
| yr, _, _ = scan_reference(x, delta, bp, cp, A, h0) | |
| y, hT = ScanFn.apply(x, delta, bp, cp, A, h0) | |
| gy = torch.randn_like(yr) | |
| gref = torch.autograd.grad(yr, (x, delta, bp, cp, A, h0), gy) | |
| gtri = torch.autograd.grad(y, (x, delta, bp, cp, A, h0), gy) | |
| out = {"fwd": ((y - yr).abs().max() / yr.abs().max()).item()} | |
| for nm, a, bb in zip(["dx", "dd", "dbp", "dcp", "dA", "dh0"], gtri, gref): | |
| out[nm] = ((a - bb).abs().max() / bb.abs().max()).item() | |
| if verbose: | |
| print("scan selftest rel errs: " + " ".join(f"{k}={v:.1e}" for k, v in out.items())) | |
| return out | |
| if __name__ == "__main__": | |
| selftest() | |