import torch import triton import triton.language as tl from triton.tools.tensor_descriptor import TensorDescriptor # ----------------------------------------------------------------------------- # Triton kernel for symmetric matrix multiplication by @byronxu99 @triton.jit def _pid_to_block( pid, M, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, GROUP_SIZE_M: tl.constexpr, ): # Split output matrix into blocks of size (BLOCK_SIZE_M, BLOCK_SIZE_N) num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) num_pid_n = tl.cdiv(M, BLOCK_SIZE_N) # Map PID to a single matrix in batch batch_idx = pid // (num_pid_m * num_pid_n) pid = pid % (num_pid_m * num_pid_n) # Map PID to 2D grid of blocks pid_m = pid // num_pid_n pid_n = pid % num_pid_n pid_m, pid_n = tl.swizzle2d(pid_m, pid_n, num_pid_m, num_pid_n, GROUP_SIZE_M) m_idx = pid_m * BLOCK_SIZE_M n_idx = pid_n * BLOCK_SIZE_N return batch_idx, m_idx, n_idx @triton.jit def XXT_kernel( A_ptr, C_ptr, M, K, a_stride_b, a_stride_r, a_stride_c, c_stride_b, c_stride_r, c_stride_c, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, GROUP_SIZE_M: tl.constexpr, LOWER_UPPER: tl.constexpr, ): pid = tl.program_id(axis=0) batch_idx, m_idx, n_idx = _pid_to_block( pid, M, BLOCK_SIZE_M, BLOCK_SIZE_N, GROUP_SIZE_M ) # Skip blocks that don't need to be computed skip_block_below_diag = (LOWER_UPPER == 0) and (n_idx + BLOCK_SIZE_N <= m_idx) skip_block_above_diag = (LOWER_UPPER != 0) and (m_idx + BLOCK_SIZE_M <= n_idx) if skip_block_below_diag or skip_block_above_diag: return # Index into one matrix of batch A_ptr += batch_idx * a_stride_b C_ptr += batch_idx * c_stride_b # Create pointer arrays for A and A.T offs_m = (m_idx + tl.arange(0, BLOCK_SIZE_M)) % M offs_n = (n_idx + tl.arange(0, BLOCK_SIZE_N)) % M offs_k = tl.arange(0, BLOCK_SIZE_K) # Load A blocks for C[m,n] = A[m,:] @ A[n,:].T # Load A[m, k] -> shape (BM, BK) a_ptrs = A_ptr + (offs_m[:, None] * a_stride_r + offs_k[None, :] * a_stride_c) # Load A[n, k] -> shape (BN, BK). Transpose to get (BK, BN) for accumulation. # Loading (BN, BK) is coalesced because stride_c is 1 (contiguous dim is k). at_ptrs = A_ptr + (offs_n[:, None] * a_stride_r + offs_k[None, :] * a_stride_c) accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) # Accumulate over blocks of K for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K)): k_remaining = K - k * BLOCK_SIZE_K a = tl.load(a_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0) at_temp = tl.load(at_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0) at = tl.trans(at_temp) accumulator = tl.dot(a, at, accumulator) a_ptrs += BLOCK_SIZE_K * a_stride_c at_ptrs += BLOCK_SIZE_K * a_stride_c out_dtype = C_ptr.dtype.element_ty output = accumulator.to(out_dtype) # Store block of C offs_cm = m_idx + tl.arange(0, BLOCK_SIZE_M) offs_cn = n_idx + tl.arange(0, BLOCK_SIZE_N) c_ptrs = C_ptr + (offs_cm[:, None] * c_stride_r + offs_cn[None, :] * c_stride_c) c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < M) tl.store(c_ptrs, output, mask=c_mask) # Store block of C mirrored across the diagonal c_ptrs_t = C_ptr + (offs_cn[:, None] * c_stride_r + offs_cm[None, :] * c_stride_c) c_mask_t = (offs_cn[:, None] < M) & (offs_cm[None, :] < M) tl.store(c_ptrs_t, output.T, mask=c_mask_t) def XXT(A: torch.Tensor, out: torch.Tensor): """ Launch Triton kernel to compute C = A @ A.T """ assert A.ndim == 2 or A.ndim == 3 M, K = A.shape[-2:] assert out.size(-2) == M, "Output matrix has incorrect shape" assert out.size(-1) == M, "Output matrix has incorrect shape" batch_size = A.size(0) if A.ndim == 3 else 1 input_batch_stride = A.stride(0) if A.ndim == 3 else 0 output_batch_stride = out.stride(0) if out.ndim == 3 else 0 # Hardcoded configs based on H100 autotuning if K == 768: BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K = 128, 128, 64 num_stages, num_warps = 4, 8 else: BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K = 64, 128, 128 num_stages, num_warps = 4, 8 grid = (batch_size * triton.cdiv(M, BLOCK_SIZE_M) * triton.cdiv(M, BLOCK_SIZE_N),) XXT_kernel[grid]( A_ptr=A, C_ptr=out, M=M, K=K, a_stride_b=input_batch_stride, a_stride_r=A.stride(-2), a_stride_c=A.stride(-1), c_stride_b=output_batch_stride, c_stride_r=out.stride(-2), c_stride_c=out.stride(-1), BLOCK_SIZE_M=BLOCK_SIZE_M, BLOCK_SIZE_N=BLOCK_SIZE_N, BLOCK_SIZE_K=BLOCK_SIZE_K, GROUP_SIZE_M=8, LOWER_UPPER=1, num_stages=num_stages, num_warps=num_warps, ) return out # ----------------------------------------------------------------------------- # Triton kernel for X.T @ X (tall matrices) # Computes C = A.T @ A where A is (M, K) and output C is (K, K) @triton.jit def XTX_kernel( A_ptr, C_ptr, M, K, a_stride_b, a_stride_r, a_stride_c, c_stride_b, c_stride_r, c_stride_c, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, GROUP_SIZE_M: tl.constexpr, LOWER_UPPER: tl.constexpr, ): """ Compute C = A.T @ A where A is (M, K) and C is (K, K). This is the transpose variant of XXT for tall matrices. The output matrix C is symmetric, so we compute upper triangle and mirror. We iterate over blocks of M (the reduction dimension after transpose). """ pid = tl.program_id(axis=0) # Note: Output is (K, K), so we use K for the output grid batch_idx, k_idx, n_idx = _pid_to_block( pid, K, BLOCK_SIZE_M, BLOCK_SIZE_N, GROUP_SIZE_M ) # Skip blocks that don't need to be computed (symmetry optimization) skip_block_below_diag = (LOWER_UPPER == 0) and (n_idx + BLOCK_SIZE_N <= k_idx) skip_block_above_diag = (LOWER_UPPER != 0) and (k_idx + BLOCK_SIZE_M <= n_idx) if skip_block_below_diag or skip_block_above_diag: return # Index into one matrix of batch A_ptr += batch_idx * a_stride_b C_ptr += batch_idx * c_stride_b # For A.T @ A: # - A.T has shape (K, M), so A.T[k, m] = A[m, k] # - We load blocks from columns k_idx and n_idx of A (which are rows of A.T) # - We reduce over M (the shared dimension) offs_k = (k_idx + tl.arange(0, BLOCK_SIZE_M)) % K # Output row indices (columns of A) offs_n = (n_idx + tl.arange(0, BLOCK_SIZE_N)) % K # Output col indices (columns of A) offs_m = tl.arange(0, BLOCK_SIZE_K) # Reduction dimension (rows of A) # Pointers for loading A[:, k_idx:k_idx+BLOCK] (transposed view is A.T[k_idx:, :]) # at_ptrs loads A.T block: A.T[offs_k, offs_m] = A[offs_m, offs_k] at_ptrs = A_ptr + (offs_m[:, None] * a_stride_r + offs_k[None, :] * a_stride_c) # a_ptrs loads A block for the other factor: A.T[offs_m, offs_n].T = A[offs_m, offs_n] a_ptrs = A_ptr + (offs_m[:, None] * a_stride_r + offs_n[None, :] * a_stride_c) accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) # Accumulate over blocks of M (the reduction dimension) for m in tl.range(0, tl.cdiv(M, BLOCK_SIZE_K)): m_remaining = M - m * BLOCK_SIZE_K # Load A.T[offs_k, offs_m] = A[offs_m, offs_k] -> shape (BLOCK_K, BLOCK_M) at = tl.load(at_ptrs, mask=offs_m[:, None] < m_remaining, other=0.0) # Load A[offs_m, offs_n] -> shape (BLOCK_K, BLOCK_N) a = tl.load(a_ptrs, mask=offs_m[:, None] < m_remaining, other=0.0) # C[k, n] = sum_m A.T[k, m] * A[m, n] = sum_m A[m, k] * A[m, n] # at.T @ a: (BLOCK_M, BLOCK_K) @ (BLOCK_K, BLOCK_N) = (BLOCK_M, BLOCK_N) accumulator = tl.dot(at.T, a, accumulator) at_ptrs += BLOCK_SIZE_K * a_stride_r a_ptrs += BLOCK_SIZE_K * a_stride_r out_dtype = C_ptr.dtype.element_ty output = accumulator.to(out_dtype) # Store block of C offs_ck = k_idx + tl.arange(0, BLOCK_SIZE_M) offs_cn = n_idx + tl.arange(0, BLOCK_SIZE_N) c_ptrs = C_ptr + (offs_ck[:, None] * c_stride_r + offs_cn[None, :] * c_stride_c) c_mask = (offs_ck[:, None] < K) & (offs_cn[None, :] < K) tl.store(c_ptrs, output, mask=c_mask) # Store block of C mirrored across the diagonal (symmetry) c_ptrs_t = C_ptr + (offs_cn[:, None] * c_stride_r + offs_ck[None, :] * c_stride_c) c_mask_t = (offs_cn[:, None] < K) & (offs_ck[None, :] < K) tl.store(c_ptrs_t, output.T, mask=c_mask_t) def XTX(A: torch.Tensor, out: torch.Tensor): """ Launch Triton kernel to compute C = A.T @ A For tall matrices (M > K), this is more efficient than transposing and using XXT because the intermediate products are smaller (K x K vs M x M). Args: A: Input tensor of shape (M, K) or (batch, M, K) out: Output tensor of shape (K, K) or (batch, K, K) Returns: out: The same output tensor, filled with A.T @ A """ assert A.ndim == 2 or A.ndim == 3 M, K = A.shape[-2:] assert out.size(-2) == K, f"Output matrix has incorrect shape: expected ({K}, {K}), got {tuple(out.shape[-2:])}" assert out.size(-1) == K, f"Output matrix has incorrect shape: expected ({K}, {K}), got {tuple(out.shape[-2:])}" batch_size = A.size(0) if A.ndim == 3 else 1 input_batch_stride = A.stride(0) if A.ndim == 3 else 0 output_batch_stride = out.stride(0) if out.ndim == 3 else 0 # Hardcoded configs based on H100 autotuning if K == 768: BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K = 128, 128, 64 num_stages, num_warps = 4, 8 else: BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K = 64, 128, 128 num_stages, num_warps = 4, 8 grid = (batch_size * triton.cdiv(K, BLOCK_SIZE_M) * triton.cdiv(K, BLOCK_SIZE_N),) XTX_kernel[grid]( A_ptr=A, C_ptr=out, M=M, K=K, a_stride_b=input_batch_stride, a_stride_r=A.stride(-2), a_stride_c=A.stride(-1), c_stride_b=output_batch_stride, c_stride_r=out.stride(-2), c_stride_c=out.stride(-1), BLOCK_SIZE_M=BLOCK_SIZE_M, BLOCK_SIZE_N=BLOCK_SIZE_N, BLOCK_SIZE_K=BLOCK_SIZE_K, GROUP_SIZE_M=8, LOWER_UPPER=1, num_stages=num_stages, num_warps=num_warps, ) return out @triton.jit def ba_plus_cAA_kernel( A_ptr, C_ptr, M, a_stride_b, a_stride_r, a_stride_c, c_stride_b, c_stride_r, c_stride_c, alpha, beta, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, GROUP_SIZE_M: tl.constexpr, LOWER_UPPER: tl.constexpr, ): # This is mostly duplicated from XXT_kernel, but also loads and adds a block of A # Performance is slightly slower than XXT_kernel, so we use two separate kernels pid = tl.program_id(axis=0) batch_idx, m_idx, n_idx = _pid_to_block( pid, M, BLOCK_SIZE_M, BLOCK_SIZE_N, GROUP_SIZE_M ) # Skip blocks that don't need to be computed skip_block_below_diag = (LOWER_UPPER == 0) and (n_idx + BLOCK_SIZE_N <= m_idx) skip_block_above_diag = (LOWER_UPPER != 0) and (m_idx + BLOCK_SIZE_M <= n_idx) if skip_block_below_diag or skip_block_above_diag: return # Index into one matrix of batch A_ptr += batch_idx * a_stride_b C_ptr += batch_idx * c_stride_b # Create pointer arrays for A and A.T offs_m = (m_idx + tl.arange(0, BLOCK_SIZE_M)) % M offs_n = (n_idx + tl.arange(0, BLOCK_SIZE_N)) % M offs_k = tl.arange(0, BLOCK_SIZE_K) # Coalesced loads similar to XXT_kernel a_ptrs = A_ptr + (offs_m[:, None] * a_stride_r + offs_k[None, :] * a_stride_c) at_ptrs = A_ptr + (offs_n[:, None] * a_stride_r + offs_k[None, :] * a_stride_c) accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) # Accumulate over blocks of K for k in tl.range(0, tl.cdiv(M, BLOCK_SIZE_K)): k_remaining = M - k * BLOCK_SIZE_K a = tl.load(a_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0) at_temp = tl.load(at_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0) at = tl.trans(at_temp) accumulator = tl.dot(a, at, accumulator) a_ptrs += BLOCK_SIZE_K * a_stride_c at_ptrs += BLOCK_SIZE_K * a_stride_c # Load block of A to add (corresponds to the current block of C) offs_am = m_idx + tl.arange(0, BLOCK_SIZE_M) offs_an = n_idx + tl.arange(0, BLOCK_SIZE_N) a_add_ptrs = A_ptr + (offs_am[:, None] * a_stride_r + offs_an[None, :] * a_stride_c) a_add_mask = (offs_am[:, None] < M) & (offs_an[None, :] < M) a_add = tl.load(a_add_ptrs, mask=a_add_mask, other=0.0).to(tl.float32) # Apply alpha and beta accumulator *= alpha accumulator += a_add * beta out_dtype = C_ptr.dtype.element_ty output = accumulator.to(out_dtype) # Store block of C offs_cm = m_idx + tl.arange(0, BLOCK_SIZE_M) offs_cn = n_idx + tl.arange(0, BLOCK_SIZE_N) c_ptrs = C_ptr + (offs_cm[:, None] * c_stride_r + offs_cn[None, :] * c_stride_c) c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < M) tl.store(c_ptrs, output, mask=c_mask) # Store block of C mirrored across the diagonal c_ptrs_t = C_ptr + (offs_cn[:, None] * c_stride_r + offs_cm[None, :] * c_stride_c) c_mask_t = (offs_cn[:, None] < M) & (offs_cm[None, :] < M) tl.store(c_ptrs_t, output.T, mask=c_mask_t) def ba_plus_cAA(A: torch.Tensor, alpha: float, beta: float, out: torch.Tensor): """ Launch Triton kernel to compute C = alpha * A @ A.T + beta * A """ assert A.ndim == 2 or A.ndim == 3 M, K = A.shape[-2:] assert M == K, "Input matrix must be square" assert out.size(-2) == M assert out.size(-1) == M batch_size = A.size(0) if A.ndim == 3 else 1 input_batch_stride = A.stride(0) if A.ndim == 3 else 0 output_batch_stride = out.stride(0) if out.ndim == 3 else 0 # Hardcoded config based on H100 autotuning (M=768) BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K = 128, 128, 64 num_stages, num_warps = 4, 8 grid = (batch_size * triton.cdiv(M, BLOCK_SIZE_M) * triton.cdiv(M, BLOCK_SIZE_N),) ba_plus_cAA_kernel[grid]( A_ptr=A, C_ptr=out, M=M, a_stride_b=input_batch_stride, a_stride_r=A.stride(-2), a_stride_c=A.stride(-1), c_stride_b=output_batch_stride, c_stride_r=out.stride(-2), c_stride_c=out.stride(-1), alpha=alpha, beta=beta, BLOCK_SIZE_M=BLOCK_SIZE_M, BLOCK_SIZE_N=BLOCK_SIZE_N, BLOCK_SIZE_K=BLOCK_SIZE_K, GROUP_SIZE_M=8, LOWER_UPPER=1, num_stages=num_stages, num_warps=num_warps, ) return out # ----------------------------------------------------------------------------- # Triton kernel for MLP: relu(x @ W1.T)^2, by @andrewbriand, @jrauvola @triton.jit def linear_relu_square_kernel(a_desc, b_desc, c_desc, aux_desc, M, N, K, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, GROUP_SIZE_M: tl.constexpr, NUM_SMS: tl.constexpr, FORWARD: tl.constexpr, ): dtype = tl.bfloat16 start_pid = tl.program_id(axis=0) num_pid_m = tl.cdiv(M, BLOCK_SIZE_M) num_pid_n = tl.cdiv(N, BLOCK_SIZE_N) k_tiles = tl.cdiv(K, BLOCK_SIZE_K) num_tiles = num_pid_m * num_pid_n tile_id_c = start_pid - NUM_SMS num_pid_in_group = GROUP_SIZE_M * num_pid_n for tile_id in tl.range(start_pid, num_tiles, NUM_SMS, flatten=True): pid_m = tile_id // num_pid_n pid_n = tile_id % num_pid_n offs_am = pid_m * BLOCK_SIZE_M offs_bn = pid_n * BLOCK_SIZE_N accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32) for ki in range(k_tiles): offs_k = ki * BLOCK_SIZE_K a = a_desc.load([offs_am, offs_k]) b = b_desc.load([offs_bn, offs_k]) accumulator = tl.dot(a, b.T, accumulator) tile_id_c += NUM_SMS pid_m = tile_id // num_pid_n pid_n = tile_id % num_pid_n offs_am_c = pid_m * BLOCK_SIZE_M offs_bn_c = pid_n * BLOCK_SIZE_N acc = tl.reshape(accumulator, (BLOCK_SIZE_M, 2, BLOCK_SIZE_N // 2)) acc = tl.permute(acc, (0, 2, 1)) acc0, acc1 = tl.split(acc) c0 = acc0.to(dtype) if not FORWARD: c0_pre = aux_desc.load([offs_am_c, offs_bn_c]) c0 = 2 * c0 * tl.where(c0_pre > 0, c0_pre, 0) c_desc.store([offs_am_c, offs_bn_c], c0) if FORWARD: c0_post = tl.maximum(c0, 0) c0_post = c0_post * c0_post aux_desc.store([offs_am_c, offs_bn_c], c0_post) c1 = acc1.to(dtype) if not FORWARD: c1_pre = aux_desc.load([offs_am_c, offs_bn_c + BLOCK_SIZE_N // 2]) c1 = 2 * c1 * tl.where(c1_pre > 0, c1_pre, 0) c_desc.store([offs_am_c, offs_bn_c + BLOCK_SIZE_N // 2], c1) if FORWARD: c1_post = tl.maximum(c1, 0) c1_post = c1_post * c1_post aux_desc.store([offs_am_c, offs_bn_c + BLOCK_SIZE_N // 2], c1_post) def linear_relu_square(a, b, aux=None): M, K = a.shape N, K = b.shape dtype = a.dtype c = torch.empty((M, N), device=a.device, dtype=dtype) FORWARD = False if aux is None: FORWARD = True aux = torch.empty((M, N), device=a.device, dtype=dtype) NUM_SMS = torch.cuda.get_device_properties("cuda").multi_processor_count BLOCK_SIZE_M = 128 BLOCK_SIZE_N = 256 BLOCK_SIZE_K = 64 num_stages = 4 if FORWARD else 3 num_warps = 8 a_desc = TensorDescriptor.from_tensor(a, [BLOCK_SIZE_M, BLOCK_SIZE_K]) b_desc = TensorDescriptor.from_tensor(b, [BLOCK_SIZE_N, BLOCK_SIZE_K]) c_desc = TensorDescriptor.from_tensor(c, [BLOCK_SIZE_M, BLOCK_SIZE_N // 2]) aux_desc = TensorDescriptor.from_tensor(aux, [BLOCK_SIZE_M, BLOCK_SIZE_N // 2]) def grid(META): return (min( NUM_SMS, triton.cdiv(M, BLOCK_SIZE_M) * triton.cdiv(N, BLOCK_SIZE_N), ), ) linear_relu_square_kernel[grid]( a_desc, b_desc, c_desc, aux_desc, M, N, K, BLOCK_SIZE_M=BLOCK_SIZE_M, BLOCK_SIZE_N=BLOCK_SIZE_N, BLOCK_SIZE_K=BLOCK_SIZE_K, GROUP_SIZE_M=1, NUM_SMS=NUM_SMS, FORWARD=FORWARD, num_stages=num_stages, num_warps=num_warps ) if FORWARD: return c, aux else: return c class FusedLinearReLUSquareFunction(torch.autograd.Function): @staticmethod def forward(ctx, x, W1, W2): pre, post = linear_relu_square(x.view((-1, x.shape[-1])), W1) x3 = post @ W2 ctx.save_for_backward(x, W1, W2, pre, post) return x3.view(x.shape) @staticmethod def backward(ctx, grad_output): x, W1, W2, pre, post = ctx.saved_tensors dW2 = post.T @ grad_output dpre = linear_relu_square(grad_output.view((-1, grad_output.shape[-1])), W2, aux=pre) dW1 = dpre.T @ x dx = dpre @ W1 return dx.view(x.shape), dW1, dW2 # ----------------------------------------------------------------------------- # Tiled transpose copy kernel: dst (N, M) = src (M, N).T # Uses coalesced reads from src and coalesced writes to dst via tl.trans(). # Replaces PyTorch's elementwise copy_ which uses a naive 75k-block kernel # with non-coalesced writes, saturating all SMs and blocking NCCL. @triton.jit def _transpose_copy_kernel( src_ptr, dst_ptr, M, N, src_stride_m, src_stride_n, dst_stride_0, dst_stride_1, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, ): pid_m = tl.program_id(0) pid_n = tl.program_id(1) offs_m = (pid_m * BLOCK_M + tl.arange(0, BLOCK_M)).to(tl.int64) offs_n = (pid_n * BLOCK_N + tl.arange(0, BLOCK_N)).to(tl.int64) mask = (offs_m[:, None] < M) & (offs_n[None, :] < N) # Coalesced read from src (M, N) tile = tl.load( src_ptr + offs_m[:, None] * src_stride_m + offs_n[None, :] * src_stride_n, mask=mask, other=0.0, ) # Coalesced write to dst (N, M): dst[n, m] = src[m, n] mask_T = (offs_n[:, None] < N) & (offs_m[None, :] < M) tl.store( dst_ptr + offs_n[:, None] * dst_stride_0 + offs_m[None, :] * dst_stride_1, tl.trans(tile), mask=mask_T, ) def transpose_copy(src: torch.Tensor, dst: torch.Tensor): """Tiled transpose copy: dst = src.T where src is (M, N) and dst is (N, M). Uses a 64x128 tiled Triton kernel with coalesced reads AND writes, achieving near memory-bandwidth-limited performance. """ assert src.ndim == 2 and dst.ndim == 2 M, N = src.shape assert dst.shape == (N, M), f"Expected dst shape ({N}, {M}), got {dst.shape}" BLOCK_M, BLOCK_N = 64, 128 grid = (triton.cdiv(M, BLOCK_M), triton.cdiv(N, BLOCK_N)) _transpose_copy_kernel[grid]( src, dst, M, N, src.stride(0), src.stride(1), dst.stride(0), dst.stride(1), BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, num_warps=8, num_stages=2, ) # ----------------------------------------------------------------------------- # Tiled transpose-add kernel: dst (M, N) += src (N, M).T # Same tiling strategy as transpose_copy but with a fused read-add-write. # Replaces PyTorch's .add_(src.T) which uses the same 75k-block elementwise # kernel with non-coalesced reads from the transposed operand. @triton.jit def _transpose_add_kernel( src_ptr, dst_ptr, M, N, src_stride_m, src_stride_n, dst_stride_0, dst_stride_1, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, ): pid_m = tl.program_id(0) pid_n = tl.program_id(1) offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) mask = (offs_m[:, None] < M) & (offs_n[None, :] < N) # Coalesced read from src (M, N) src_tile = tl.load( src_ptr + offs_m[:, None] * src_stride_m + offs_n[None, :] * src_stride_n, mask=mask, other=0.0, ) # Coalesced read-add-write on dst (N, M): dst[n, m] += src[m, n] mask_T = (offs_n[:, None] < N) & (offs_m[None, :] < M) dst_ptrs = dst_ptr + offs_n[:, None] * dst_stride_0 + offs_m[None, :] * dst_stride_1 dst_tile = tl.load(dst_ptrs, mask=mask_T, other=0.0) tl.store(dst_ptrs, dst_tile + tl.trans(src_tile), mask=mask_T) def transpose_add(src: torch.Tensor, dst: torch.Tensor): """Tiled transpose-add: dst += src.T where src is (M, N) and dst is (N, M). Uses a 32x32 tiled Triton kernel with coalesced access on both src and dst, replacing PyTorch's .add_(src.T) which has non-coalesced reads from the transposed operand. """ assert src.ndim == 2 and dst.ndim == 2 M, N = src.shape assert dst.shape == (N, M), f"Expected dst shape ({N}, {M}), got {dst.shape}" BLOCK_M, BLOCK_N = 32, 32 grid = (triton.cdiv(M, BLOCK_M), triton.cdiv(N, BLOCK_N)) _transpose_add_kernel[grid]( src, dst, M, N, src.stride(0), src.stride(1), dst.stride(0), dst.stride(1), BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, num_warps=4, num_stages=2, ) CE_KERNEL_BLOCK_SIZE = 256 CE_KERNEL_VOCAB_SIZE = 50304 CE_KERNEL_DECLS = f""" constexpr int VOCAB_SIZE = {CE_KERNEL_VOCAB_SIZE}; constexpr int BLOCK_SIZE = {CE_KERNEL_BLOCK_SIZE}; """ CE_KERNEL_SOURCE = """ #include #include #define __nv_fp8_e5m2 char #define uint16_t unsigned short #define uint8_t unsigned char #define int64_t long long __device__ __forceinline__ __nv_fp8_e5m2 f32_to_fp8_e5m2(float x) { uint16_t packed; asm volatile( "cvt.rn.satfinite.e5m2x2.f32 %0, %1, %2;" : "=h"(packed) : "f"(x), "f"(0.0f) ); __nv_fp8_e5m2 result; *reinterpret_cast(&result) = (packed & (0xFF << 8)) >> 8; return result; } struct __align__(16) __nv_bfloat168 { __nv_bfloat16 data[8]; __device__ __nv_bfloat16& operator[](int i) { return data[i]; } __device__ const __nv_bfloat16& operator[](int i) const { return data[i]; } }; struct __align__(8) __nv_fp8_e5m28 { __nv_fp8_e5m2 data[8]; __device__ __nv_fp8_e5m2& operator[](int i) { return data[i]; } __device__ const __nv_fp8_e5m2& operator[](int i) const { return data[i]; } }; template __device__ constexpr T CEIL_DIV(T a, T b) { return (a + b - 1) / b; } //__device__ float sigmoid(float x) { // return 1.0f / (1.0f + __expf(-x)); //} __device__ float sigmoid(float x) { return 0.5f + __tanhf(x * 0.5f) * 0.5f; } extern "C" __launch_bounds__(BLOCK_SIZE, 2) __global__ void ce_fwd_bwd_kernel( const __nv_bfloat16* __restrict__ logits, const int64_t* __restrict__ targets, const float* __restrict__ mtp_weights, float* __restrict__ losses, __nv_fp8_e5m2* grad_input, int batch_size, int n_predict, double A_param, double B_param, double C_param, double grad_s_param, double grad_scale_param) { constexpr int VEC_WIDTH = 8; constexpr int NUM_FULL_LOADS = VOCAB_SIZE / (BLOCK_SIZE * VEC_WIDTH); constexpr int NUM_LOADS = CEIL_DIV(VOCAB_SIZE, BLOCK_SIZE * VEC_WIDTH); float A = (float)A_param; float B = (float)B_param; float C = (float)C_param; float grad_s = (float)grad_s_param; float grad_scale = (float)grad_scale_param; extern __shared__ __nv_bfloat16 smem[]; static_assert(VEC_WIDTH == 8); const __nv_bfloat16 *block_logit_ptr = logits + VOCAB_SIZE * blockIdx.x; float inv_C = 1 / C; float B_div_C = B * inv_C; float thread_max = -CUDART_INF_F; #pragma unroll 25 for (int i = 0; i < NUM_LOADS; i++) { int idx = i * BLOCK_SIZE * VEC_WIDTH + threadIdx.x * VEC_WIDTH; if (i < NUM_FULL_LOADS || idx < VOCAB_SIZE) { __nv_bfloat168 result = *(__nv_bfloat168*)(&block_logit_ptr[idx]); __nv_bfloat168 result_sigmoid; #pragma unroll for (int k = 0; k < VEC_WIDTH; k++) { float tmp = __bfloat162float(result[k]); tmp = sigmoid(tmp * inv_C + B_div_C); result_sigmoid[k] = __float2bfloat16(tmp); tmp = A * tmp; thread_max = max(tmp, thread_max); } *(__nv_bfloat168*)(&smem[idx]) = result_sigmoid; } } constexpr int NUM_WARPS = BLOCK_SIZE / 32; int warp_id = threadIdx.x / 32; __shared__ float block_maxs[NUM_WARPS]; __shared__ float block_sums[NUM_WARPS]; for (int offset = 16; offset > 0; offset >>= 1) thread_max = fmaxf(thread_max, __shfl_down_sync(0xFFFFFFFF, thread_max, offset)); if (threadIdx.x % 32 == 0) { block_maxs[warp_id] = thread_max; } __syncthreads(); float block_max = -CUDART_INF_F; for (int i = 0; i < NUM_WARPS; i++) { block_max = fmaxf(block_max, block_maxs[i]); } float thread_sum = 0.0f; #pragma unroll 2 for (int i = 0; i < NUM_LOADS; i++) { int idx = i * BLOCK_SIZE * VEC_WIDTH + threadIdx.x * VEC_WIDTH; __nv_bfloat168 l; if (i < NUM_FULL_LOADS || idx < VOCAB_SIZE) { l = *(__nv_bfloat168*)(&smem[idx]); } #pragma unroll for (int k = 0; k < VEC_WIDTH; k++) { float tmp = A * __bfloat162float(l[k]); tmp = __expf(tmp - block_max); if (i < NUM_FULL_LOADS || idx < VOCAB_SIZE) { thread_sum += tmp; } } } for (int offset = 16; offset > 0; offset >>= 1) thread_sum += __shfl_down_sync(0xFFFFFFFF, thread_sum, offset); if (threadIdx.x % 32 == 0) { block_sums[warp_id] = thread_sum; } __syncthreads(); float block_sum = 0.0f; for (int i = 0; i < NUM_WARPS; i++) { block_sum += block_sums[i]; } float lse = block_max + __logf(block_sum); if (threadIdx.x == 0) { float total_loss = 0.0f; for (int k = 0; k < n_predict; k++) { int64_t target_idx = blockIdx.x + k; if (target_idx < batch_size) { float weight = mtp_weights[k]; int64_t target = targets[target_idx]; if (target >= 0 && target < VOCAB_SIZE) { float z_target = A * __bfloat162float(smem[target]); total_loss += weight * (lse - z_target); } } } losses[blockIdx.x] = total_loss; } float S_w = 0.0f; for (int i = 0; i < n_predict; i++) { S_w += mtp_weights[i]; } #pragma unroll 4 for (int i = 0; i < NUM_LOADS; i++) { int idx = i * BLOCK_SIZE * VEC_WIDTH + threadIdx.x * VEC_WIDTH; __nv_fp8_e5m28 result; if (i < NUM_FULL_LOADS || idx < VOCAB_SIZE) { __nv_bfloat168 sigmoid_us = *(__nv_bfloat168*)(&smem[idx]); #pragma unroll for (int j = 0; j < VEC_WIDTH; j++) { float sigmoid_u = __bfloat162float(sigmoid_us[j]); float z = A * sigmoid_u; float p = __expf(z - lse); float term1 = S_w * p; float term2 = 0.0f; float grad_z = term1 - term2; float grad_x = grad_scale * (1.0f / C * A) * (1.0f / grad_s) * grad_z * sigmoid_u * (1.0f - sigmoid_u); auto result_tmp = f32_to_fp8_e5m2(grad_x); result[j] = *reinterpret_cast<__nv_fp8_e5m2*>(&result_tmp); } *(__nv_fp8_e5m28*)(&grad_input[blockIdx.x * VOCAB_SIZE + idx]) = result; } } __syncthreads(); if (threadIdx.x < n_predict && blockIdx.x + threadIdx.x < batch_size) { int i = threadIdx.x; int64_t target = targets[blockIdx.x + i]; float sigmoid_u = __bfloat162float(smem[target]); float z = A * sigmoid_u; float p = __expf(z - lse); float term1 = S_w * p; float term2 = 0.0f; #pragma unroll for (int k = 0; k < 3; k++) { int64_t target_idx = blockIdx.x + k; if (target_idx < batch_size && k < n_predict) { if (targets[target_idx] == target) { term2 += mtp_weights[k]; } } } float grad_z = term1 - term2; float grad_x = grad_scale * (1.0f / C * A) * (1.0f / grad_s) * grad_z * sigmoid_u * (1.0f - sigmoid_u); auto result_tmp = f32_to_fp8_e5m2(grad_x); auto result = *reinterpret_cast<__nv_fp8_e5m2*>(&result_tmp); grad_input[blockIdx.x * VOCAB_SIZE + target] = result; } } """ ce_fwd_bwd_kernel = torch.cuda._compile_kernel( CE_KERNEL_DECLS + CE_KERNEL_SOURCE, "ce_fwd_bwd_kernel", compute_capability="90", cuda_include_dirs=['/home/ubuntu/modded-nanogpt/.venv/lib/python3.14/site-packages/triton/backends/nvidia/include', '/home/ubuntu/modded-nanogpt/.venv/lib/python3.14/site-packages/nvidia/cuda_runtime/include'], nvcc_options=["-lineinfo", "--use_fast_math"], ) ce_fwd_bwd_kernel.set_shared_memory_config(CE_KERNEL_VOCAB_SIZE * 2) @torch.library.custom_op("nanogpt::ce_fwd_bwd", mutates_args={"losses", "grad_input"}) def ce_fwd_bwd( logits: torch.Tensor, targets: torch.Tensor, mtp_weights: torch.Tensor, losses: torch.Tensor, grad_input: torch.Tensor, n_rows: int, n_predict: int, A: float, B: float, C: float, grad_s: float, grad_scale: float, ) -> None: grid = (n_rows, 1, 1) ce_fwd_bwd_kernel( grid, (CE_KERNEL_BLOCK_SIZE, 1, 1), (logits, targets, mtp_weights, losses, grad_input, n_rows, n_predict, A, B, C, grad_s, grad_scale), shared_mem=CE_KERNEL_VOCAB_SIZE * 2, ) class FusedSoftcappedCrossEntropy(torch.autograd.Function): @staticmethod def forward(ctx, x, targets, mtp_weights, lm_head_weight, x_s, w_s, grad_s, grad_scale, A=23.0, B=5.0, C=7.5): x_f8 = x.div(x_s).to(torch.float8_e4m3fn) w_f8 = lm_head_weight.div(w_s).to(torch.float8_e4m3fn) w_f8_col_major = w_f8.T.contiguous().T logits = torch._scaled_mm( x_f8, w_f8_col_major, out_dtype=torch.bfloat16, scale_a=x.new_tensor(x_s, dtype=torch.float32), scale_b=x.new_tensor(w_s, dtype=torch.float32), use_fast_accum=True, ) n_rows, n_cols = logits.shape if mtp_weights is None: mtp_weights = torch.tensor([1.0], device=logits.device, dtype=torch.float32) n_predict = mtp_weights.shape[0] losses = torch.empty(n_rows, dtype=torch.float32, device=logits.device) lse = torch.empty(n_rows, dtype=torch.float32, device=logits.device) logits = logits.contiguous() targets = targets.contiguous() mtp_weights = mtp_weights.contiguous() grad_input = torch.empty((n_rows, n_cols), dtype=torch.float8_e5m2, device=logits.device) ce_fwd_bwd(logits, targets, mtp_weights, losses, grad_input, n_rows, n_predict, A, B, C, grad_s, grad_scale) ctx.save_for_backward(logits, targets, mtp_weights, lse, x, lm_head_weight, x_f8, w_f8, grad_input) ctx.params = (A, B, C, x_s, w_s, grad_s) return losses @staticmethod def backward(ctx, grad_output): logits, targets, mtp_weights, lse, x, lm_head_weight, x_f8, w_f8, grad_input = ctx.saved_tensors A, B, C, x_s, w_s, grad_s = ctx.params n_rows, n_cols = logits.shape n_predict = mtp_weights.shape[0] grad_output = grad_output.contiguous() x_scale = grad_input.new_tensor(x_s, dtype=torch.float32) w_scale = grad_input.new_tensor(w_s, dtype=torch.float32) grad_scale = grad_input.new_tensor(grad_s, dtype=torch.float32) grad_x = torch._scaled_mm( grad_input, w_f8.T, out_dtype=torch.bfloat16, scale_a=grad_scale, scale_b=w_scale, use_fast_accum=False, ) x_f8_T = torch.empty((x_f8.shape[1], x_f8.shape[0]), dtype=x_f8.dtype, device=x_f8.device) transpose_copy(x_f8, x_f8_T) # (768, n_rows) row-major grad_input_T = torch.empty((n_cols, n_rows), dtype=grad_input.dtype, device=grad_input.device) transpose_copy(grad_input, grad_input_T) # (50304, n_rows) row-major grad_w = torch._scaled_mm( x_f8_T, # (768, n_rows) row-major grad_input_T.T, # (n_rows, 50304) column-major view out_dtype=torch.float32, scale_a=x_scale, scale_b=grad_scale, use_fast_accum=False, ) return grad_x, None, None, grad_w, None, None, None