Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Optimizing Loops for Softmax and FlashAttention in Triton

Tech Nov 3 1

Puzzle 8: Long Softmax

This puzzle implements a batched softmax operation. Given a batch of logits of shape [N0, T], compute the softmax for each row. The spec is:

def softmax_spec(x: Float32[4, 200]) -> Float32[4, 200]:
    x_max = x.max(1, keepdim=True)[0]
    x = x - x_max
    x_exp = x.exp()
    return x_exp / x_exp.sum(1, keepdim=True)

The challenge requires two solutions: a brrute-force three-loop version and an optimized two-loop version using the online softmax trick.

Brute-Force Approach (Three Loops)

  1. Loop 1: Find the maximum value in each row.
  2. Loop 2: Subtract the row maximum from each element, exponentiate, and accumulate the sum.
  3. Loop 3: Compute the final softmax by dividing each exponentiated value by the row sum.

Implementation:

@triton.jit
def exp_approx(x):
    return tl.exp2(1.44269504 * x)

@triton.jit
def softmax_kernel_brute_force(x_ptr, z_ptr, N0, N1, T, B0: tl.constexpr, B1: tl.constexpr):
    block_id_i = tl.program_id(0)
    offset_x = block_id_i * B0 + tl.arange(0, B0)
    mask_x = offset_x < N0

    row_max = tl.zeros([B0, 1], dtype=tl.float32)

    # Loop 1: compute row max
    for idj in tl.range(0, T, B1):
        offset_y = idj + tl.arange(0, B1)
        mask_y = offset_y < T
        offset_xy = offset_x[:, None] * T + offset_y[None, :]
        mask_xy = mask_x[:, None] & mask_y[None, :]
        block_value = tl.load(x_ptr + offset_xy, mask_xy, other=float('-inf'))
        row_max = tl.maximum(row_max, tl.max(block_value, axis=1, keep_dims=True))

    row_sum_exp = tl.zeros([B0, 1], dtype=tl.float32)

    # Loop 2: subtract max and compute exp sum
    for idj in tl.range(0, T, B1):
        offset_y = idj + tl.arange(0, B1)
        mask_y = offset_y < T
        offset_xy = offset_x[:, None] * T + offset_y[None, :]
        mask_xy = mask_x[:, None] & mask_y[None, :]
        block_value = tl.load(x_ptr + offset_xy, mask_xy, other=float('-inf'))
        block_value -= row_max
        row_sum_exp += tl.sum(exp_approx(block_value), axis=1, keep_dims=True)

    # Loop 3: compute final softmax
    for idj in tl.range(0, T, B1):
        offset_y = idj + tl.arange(0, B1)
        mask_y = offset_y < T
        offset_xy = offset_x[:, None] * T + offset_y[None, :]
        mask_xy = mask_x[:, None] & mask_y[None, :]
        block_value = tl.load(x_ptr + offset_xy, mask_xy, other=float('-inf'))
        softmax_value = exp_approx(block_value - row_max) / row_sum_exp
        tl.store(z_ptr + offset_xy, softmax_value, mask_xy)

    return

Optimized Approach (Two Loops with On line Softmax)

The online softmax algorithm combines maximum update and sum accumulation in one pass. The recurrence is:

$$\begin{aligned} m_0 &\leftarrow -\infty \ d_0 &\leftarrow 0 \ \text{for } j &\leftarrow 1, V \text{ do} \ &\quad m_j \leftrarow \max(m_{j-1}, x_j) \ &\quad d_j \leftarrow d_{j-1} \times e^{m_{j-1}-m_j} + e^{x_j-m_j} \ \text{end for} \ \text{for } i &\leftarrow 1, V \text{ do} \ &\quad y_i \leftarrow \frac{e^{x_i-m_V}}{d_V} \ \text{end for}\end{aligned}$$

Implementation:

@triton.jit
def exp_approx(x):
    return tl.exp2(1.44269504 * x)

@triton.jit
def softmax_kernel(x_ptr, z_ptr, N0, N1, T, B0: tl.constexpr, B1: tl.constexpr):
    block_id_i = tl.program_id(0)
    offset_x = block_id_i * B0 + tl.arange(0, B0)
    mask_x = offset_x < N0

    row_max = tl.zeros([B0, 1], dtype=tl.float32)
    row_sum_exp = tl.zeros([B0, 1], dtype=tl.float32)

    for idj in tl.range(0, T, B1):
        offset_y = idj + tl.arange(0, B1)
        mask_y = offset_y < T
        offset_xy = offset_x[:, None] * T + offset_y[None, :]
        mask_xy = mask_x[:, None] & mask_y[None, :]
        block_value = tl.load(x_ptr + offset_xy, mask_xy, other=float('-inf'))

        new_max = tl.maximum(tl.max(block_value, axis=1, keep_dims=True), row_max)
        row_sum_exp = row_sum_exp * exp_approx(row_max - new_max) + tl.sum(exp_approx(block_value - new_max), axis=1, keep_dims=True)
        row_max = new_max

    for idj in tl.range(0, T, B1):
        offset_y = idj + tl.arange(0, B1)
        mask_y = offset_y < T
        offset_xy = offset_x[:, None] * T + offset_y[None, :]
        mask_xy = mask_x[:, None] & mask_y[None, :]
        block_value = tl.load(x_ptr + offset_xy, mask_xy, other=float('-inf'))
        softmax_value = exp_approx(block_value - row_max) / row_sum_exp
        tl.store(z_ptr + offset_xy, softmax_value, mask_xy)

    return

Puzzle 9: Simple FlashAttention

This puzzle implements a scalar version of FlashAttention. Given query row q, keys K, values V, compute:

$$z_i = \sum_{j=1}^{T} \text{softmax}(q_i \cdot k_1, \ldots, q_i \cdot k_T)_j \cdot v_j$$

The online softmax trick extends to also accumulate the weighted sum of values in a single loop. The recurrence is:

$$\begin{aligned} x_i &\leftarrow Q[k,:] \cdot K^T[:,i] \ m_i &\leftarrow \max(m_{i-1}, x_i) \ d_i' &\leftarrow d_{i-1}' \cdot e^{m_{i-1} - m_i} + e^{x_i - m_i} \ O_i' &\leftarrow O_{i-1}' \cdot \frac{d_{i-1}'}{d_i'} \cdot e^{m_{i-1} - m_i} + \frac{e^{x_i - m_i}}{d_i'} \cdot V[i,:] \ \end{aligned}$$

Implementation:

@triton.jit
def exp_approx(x):
    return tl.exp2(1.44269504 * x)

@triton.jit
def flashatt_kernel(
    q_ptr, k_ptr, v_ptr, z_ptr, N0, T, B0: tl.constexpr, B1: tl.constexpr
):
    block_id_i = tl.program_id(0)
    off_i = block_id_i * B0 + tl.arange(0, B0)
    mask_i = off_i < N0
    inf = 1.0e6

    q = tl.load(q_ptr + off_i, mask=mask_i)

    exp_sum = tl.zeros((B0,), dtype=tl.float32)
    qk_max = tl.full((B0,), -inf, dtype=tl.float32)
    z = tl.zeros((B0,), dtype=tl.float32)

    for id_j in tl.range(0, T, B1):
        off_j = id_j + tl.arange(0, B1)
        mask_j = off_j < T
        mask_ij = mask_i[:, None] & mask_j[None, :]

        k = tl.load(k_ptr + off_j, mask=mask_j)
        qk = q[:, None] * k[None, :] + tl.where(mask_ij, 0, -inf)

        new_max = tl.maximum(tl.max(qk, axis=1), qk_max)
        qk_exp = exp_approx(qk - new_max[:, None])
        factor = exp_approx(qk_max - new_max)
        new_exp_sum = exp_sum * factor + tl.sum(qk_exp, axis=1)
        
        v = tl.load(v_ptr + off_j, mask=mask_j, other=0.0)
        z = z * factor + tl.sum(qk_exp * v[None, :], axis=1)

        qk_max = new_max
        exp_sum = new_exp_sum

    z = z / exp_sum
    tl.store(z_ptr + off_i, z, mask=mask_i)
    return

References

  1. Tri Dao, et al. "FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness." arXiv:2205.14135.
  2. Online Softmax: https://arxiv.org/pdf/1805.02867
  3. Triton Official Tutorials

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.