changh95's picture
tt-model push diffusion-planner-p150 (container)
4d9b003 verified
Raw History Blame Contribute Delete
21.2 kB
# SPDX-License-Identifier: Apache-2.0
"""C20: attention on the p150 through ``ttnn.transformer.scaled_dot_product_attention`` (PLAN.md 1.2 C20, probe P7).
SDPA has three silent traps (common/PROBES.md P7; ``probes/g4-attention-gridsample-msda.md``):
- ``is_causal`` defaults to True: with Sq == Sk it silently applies a causal mask, with Sq != Sk it is rejected;
- the default ``scale`` is ``1/sqrt(padded head_dim)``: after a 16 -> 32 head padding it is wrong (PCC 0.979);
- the default program config (32/32 chunks) is 2.5-7x slower everywhere and inaccurate over >= 32k keys.
:func:`sdpa` is the only way the ports call the op: ``is_causal=False`` always, ``scale`` is a required keyword,
and a ``program_config`` is always passed (chunk sizes from :func:`chunk_config`, the P7 table). Without a mask,
pass the logical, unaligned Sq / Sk: the op pads to its chunks and masks its own padding element by element. A user
mask is an additive bf16 TILE tensor **in DRAM**, ``[1|B, 1|H, Sq, Sk]``; ``-inf`` is safe, also when a whole first
key chunk is masked (P7).
**A user mask needs a tile-aligned Sk** (found by the Diffusion Planner port, 2026-10-07): with a mask, the reader
fills only the key *tiles* past ``ceil(Sk / 32)`` with ``-inf`` and reads the last partial tile from the mask
itself, whose tile padding is 0 after a host tilize (``reader_interleaved.cpp``, "Mask read"). The up to 31 padded
keys then join the softmax with score ``q . k_pad`` and value ``v_pad``: rel-L2 0.18 instead of 0.025 at 321 x 321
with 89 valid keys, 0.06 at P7's masked 564 x 564. :func:`sdpa` therefore refuses a mask with ``Sk % 32 != 0``:
give K / V a tile-aligned key count (:func:`aligned_keys`; the extra keys are masked with ``-inf`` in the mask,
``key_bias(padded_validity, sq)``), which costs nothing because the tiles are padded anyway.
Head layout. The op wants ``[B, H, S, D]`` bf16 TILE interleaved tensors with ``D`` a multiple of 32 and no padding on
B / H / D. :func:`split_heads` / :func:`split_qkv` / :func:`split_q_kv` turn projection outputs ``[B, 1, S, H*D]``
into heads (``ttnn.experimental.nlp_create_qkv_heads``, one program, logical S kept), and ``sdpa(...,
concat_heads=True)`` (the op's ``output_concat_heads``) or :func:`merge_heads` go back to ``[B, 1, S, H*D]``.
A head dim below 32 (16 in TransFusion / BEVFusion / PTv3) is zero-padded *in the weights*
(:func:`pad_head_columns` on the Q / K / V projections, :func:`pad_head_rows` on the output projection): exact
(padded output columns are 0, P7) and free at run time; pass ``scale=1/sqrt(logical head dim)``.
Masks. :func:`key_bias` builds ``[1, 1, Sq, Sk]`` (0 for valid keys, ``-inf`` otherwise) on the host; inside a
trace :func:`expand_key_bias` repeats a persistent ``[1, 1, 1, Sk]`` bias row (:func:`key_bias_row`) to
``[1, 1, Sq, Sk]`` in DRAM (one program, the upload shrinks from Sq * Sk to Sk values).
Fallback. :func:`attention_matmul` is plain ``softmax(scale Q K^T + bias) V`` with matmuls (for dtypes or layouts the
op rejects, e.g. fp32 attention); it needs a tile-aligned Sk (pad the keys and mask them with ``-inf``).
:func:`attention_reference` is the float64 numpy oracle of both.
Importing this module never imports ttnn (ttaw rule); ttnn is imported inside the functions that need it.
"""
from __future__ import annotations
import math
from typing import Any, Dict, Optional, Sequence, Tuple
import numpy as np
__all__ = [
"aligned_keys",
"HEAD_DIM_MULTIPLE",
"PROGRAM_CONFIG_MIN_SK",
"CHUNK_TABLE",
"padded_head_dim",
"chunk_config",
"sdpa_program_config",
"sdpa_compute_config",
"sdpa",
"split_heads",
"split_qkv",
"split_q_kv",
"merge_heads",
"key_bias",
"key_bias_row",
"expand_key_bias",
"pad_head_dim",
"pad_head_columns",
"pad_head_rows",
"attention_reference",
"attention_matmul",
]
HEAD_DIM_MULTIPLE = 32 # SDPA rejects a padded head dim ("Padding is not supported on the head_dim dimension")
TILE = 32
# P7: from about 1,000 keys on, the default 32/32 chunks are 2.5-7x slower; this wrapper passes a config always.
PROGRAM_CONFIG_MIN_SK = 1000
# Exact (Sq, Sk) shapes measured on the p150b (ETH, 12x10): (q_chunk, k_chunk) and the evidence. Shapes not listed
# use the Sk-range rules of :func:`chunk_config`. Ports add their measured shapes here (with the log).
CHUNK_TABLE: Dict[Tuple[int, int], Tuple[int, int, str]] = {
(500, 500): (128, 128, "P7 tf_self 0.029 ms (default 0.047)"),
(512, 512): (512, 512, "P7 pt_patch_s0/s4 0.172-0.222 ms (q128 k128 0.413-0.571)"),
(564, 564): (64, 64, "P7 dp_fusion_masked 0.058 ms (default 0.094, q128 k128 0.158)"),
(321, 564): (64, 64, "P7 dp_cross 0.029 ms (default 0.030)"),
(576, 576): (64, 64, "DP fusion, masked: 0.061-0.092 ms (q32 k64 0.117, q32 k32 0.129, q128 k128 0.275)"),
(352, 352): (64, 64, "DP DiT self, masked: 0.063 ms (q64 k128 0.059, q32 k32 0.080, q128 k128 0.071)"),
(352, 564): (64, 64, "DP DiT cross: 0.033-0.043 ms (q32 k32 0.035, q128 k128 0.052, q64 k128 0.077)"),
(900, 1668): (64, 128, "P7 sp_self 0.052 ms (default 0.139)"),
(900, 6000): (64, 512, "P7 sp_cross 0.106 ms (default 0.469)"),
}
def padded_head_dim(head_dim: int, multiple: int = HEAD_DIM_MULTIPLE) -> int:
"""The head dim SDPA sees: ``head_dim`` rounded up to a multiple of 32."""
if head_dim <= 0:
raise ValueError(f"head_dim must be positive, got {head_dim}")
return -(-int(head_dim) // multiple) * multiple
def aligned_keys(n: int, multiple: int = TILE) -> int:
"""The tile-aligned key count a masked attention over ``n`` real keys runs on (the extra keys are masked)."""
if n <= 0:
raise ValueError(f"key count must be positive, got {n}")
return -(-int(n) // multiple) * multiple
def chunk_config(sq: int, sk: int) -> Tuple[int, int]:
"""``(q_chunk, k_chunk)`` for logical ``Sq`` x ``Sk`` (multiples of 32, as the op requires).
Exact shapes come from :data:`CHUNK_TABLE`. Otherwise, by key length (P7: chunk sizes are what matters for
long keys; q64 keeps ``B * H * Sq / 64`` work units near the 120 cores at Sq ~ 500-900):
Sk >= 16,384 -> q64 k1024 (P7 tf / bf / pt cross 0.45-0.88 ms; q64 k512 + ``fp32_acc`` halves the max error at
~1.4x the time); Sk >= 4,096 -> q64 k512; Sk >= 1,000 -> q64 k128; shorter keys -> q64 k64 (32 when the
sequence fits one tile).
"""
sq, sk = int(sq), int(sk)
if sq <= 0 or sk <= 0:
raise ValueError(f"sequence lengths must be positive, got Sq={sq}, Sk={sk}")
if (sq, sk) in CHUNK_TABLE:
q, k, _ = CHUNK_TABLE[(sq, sk)]
return q, k
q = 32 if sq <= TILE else 64
if sk >= 16384:
return q, 1024
if sk >= 4096:
return q, 512
if sk >= PROGRAM_CONFIG_MIN_SK:
return q, 128
return q, (32 if sk <= TILE else 64)
def sdpa_program_config(device: Any, sq: int, sk: int, *, q_chunk: Optional[int] = None, k_chunk: Optional[int] = None,
exp_approx_mode: Optional[bool] = None):
"""``ttnn.SDPAProgramConfig`` over the device's whole compute grid (read from the device, never hard-coded)
with the :func:`chunk_config` chunks unless ``q_chunk`` / ``k_chunk`` are given."""
import ttnn
q_default, k_default = chunk_config(sq, sk)
q_chunk = int(q_chunk or q_default)
k_chunk = int(k_chunk or k_default)
for name, value in (("q_chunk", q_chunk), ("k_chunk", k_chunk)):
if value <= 0 or value % TILE:
raise ValueError(f"{name}={value}: must be a positive multiple of {TILE}")
return ttnn.SDPAProgramConfig(compute_with_storage_grid_size=device.compute_with_storage_grid_size(),
q_chunk_size=q_chunk, k_chunk_size=k_chunk, exp_approx_mode=exp_approx_mode)
def sdpa_compute_config(*, fp32_acc: bool = False, fidelity: str = "HiFi2", approx: bool = True):
"""Explicit SDPA compute config. The default equals the op's own (HiFi2, bf16 DST, approximate exp): P7 found
neither HiFi4 nor exact exp more accurate; ``fp32_acc=True`` (the op's standard instead of its streaming kernel)
halves the max error (0.11 -> 0.056 over 36k keys) at 1.35-1.45x the time."""
from ..precision import compute_kernel_config
return compute_kernel_config(fidelity, fp32_acc=fp32_acc, approx=approx)
def _seq(t: Any) -> int:
return int(t.shape[-2])
def sdpa(q: Any, k: Any, v: Any, *, scale: float, attn_mask: Any = None, program_config: Any = "auto",
compute_kernel_config: Any = None, fp32_acc: bool = False, concat_heads: bool = False,
memory_config: Any = None):
"""Non-causal scaled dot-product attention ``softmax(scale * Q K^T + attn_mask) V``.
``q`` ``[B, H, Sq, D]``, ``k`` / ``v`` ``[B, Hkv, Sk, D]`` (bf16 / bfp8 / bfp4, TILE, interleaved, D a multiple of
32); ``scale`` is required: ``1/sqrt(logical head dim)`` (never the op's padded-head default). ``attn_mask``:
additive ``[1|B, 1|H, Sq, Sk]`` bf16 TILE tensor in DRAM, or None; with a mask ``Sk`` must be a multiple of 32
(module docstring). ``program_config``: ``"auto"`` (the
:func:`chunk_config` chunks over the whole grid), an ``SDPAProgramConfig``, or None (op default; refused once
Sk >= :data:`PROGRAM_CONFIG_MIN_SK`, P7). ``compute_kernel_config`` overrides :func:`sdpa_compute_config`
(``fp32_acc``). ``concat_heads=True`` returns ``[B, 1, Sq, H*D]`` instead of ``[B, H, Sq, D]`` (no separate
concat program). Returns the device tensor.
"""
import ttnn
scale = float(scale)
if not math.isfinite(scale) or scale <= 0.0:
raise ValueError(f"scale must be a positive finite float, got {scale!r}")
if len(q.shape) != 4 or len(k.shape) != 4 or len(v.shape) != 4:
raise ValueError(f"sdpa expects [B, H, S, D] tensors, got q{tuple(q.shape)} k{tuple(k.shape)} "
f"v{tuple(v.shape)}")
d = int(q.shape[-1])
if d % HEAD_DIM_MULTIPLE:
raise ValueError(f"head dim {d} is not a multiple of {HEAD_DIM_MULTIPLE}: zero-pad it in the projection "
"weights (pad_head_columns / pad_head_rows) and pass scale=1/sqrt(logical head dim)")
sq, sk = _seq(q), _seq(k)
if attn_mask is not None and sk % TILE:
raise ValueError(f"a user mask needs a tile-aligned key count (Sk={sk}): the op masks the padded keys of the "
"last partial tile only through the mask's own tile padding (0 after a host tilize), so "
f"they would join the softmax. Run on aligned_keys({sk}) = {aligned_keys(sk)} keys and mask "
"the extra ones with -inf (key_bias on the padded validity)")
if isinstance(program_config, str):
if program_config != "auto":
raise ValueError(f"program_config={program_config!r}: expected 'auto', a config object or None")
program_config = sdpa_program_config(q.device(), sq, sk)
elif program_config is None and sk >= PROGRAM_CONFIG_MIN_SK:
raise ValueError(f"Sk={sk} >= {PROGRAM_CONFIG_MIN_SK}: the op's default 32/32 chunks are slow and inaccurate "
"for long keys (probe P7); pass program_config='auto' or an SDPAProgramConfig")
if compute_kernel_config is None:
compute_kernel_config = sdpa_compute_config(fp32_acc=fp32_acc)
kwargs: Dict[str, Any] = {}
if concat_heads:
kwargs["output_concat_heads"] = True
return ttnn.transformer.scaled_dot_product_attention(
q, k, v, attn_mask=attn_mask, is_causal=False, scale=scale, memory_config=memory_config,
program_config=program_config, compute_kernel_config=compute_kernel_config, **kwargs)
# ---------------------------------------------------------------------------------------------------- heads
def _check_4d_heads(x: Any, what: str) -> None:
if len(x.shape) != 4 or int(x.shape[1]) != 1:
raise ValueError(f"{what} must be [B, 1, S, width] (a 4-D projection output), got {tuple(x.shape)}")
def split_heads(x: Any, num_heads: int, *, memory_config: Any = None):
"""``[B, 1, S, H*D]`` -> ``[B, H, S, D]`` (one ``nlp_create_qkv_heads`` program with no K / V heads; the logical
S is kept, so unaligned sequences stay unaligned for :func:`sdpa`)."""
import ttnn
_check_4d_heads(x, "split_heads input")
width = int(x.shape[-1])
if width % num_heads or (width // num_heads) % HEAD_DIM_MULTIPLE:
raise ValueError(f"width {width} does not split into {num_heads} heads of a multiple of {HEAD_DIM_MULTIPLE}")
out, _, _ = ttnn.experimental.nlp_create_qkv_heads(x, num_heads=num_heads, num_kv_heads=0,
transpose_k_heads=False, memory_config=memory_config)
return out
def split_qkv(qkv: Any, num_heads: int, *, num_kv_heads: Optional[int] = None, memory_config: Any = None):
"""Fused ``[B, 1, S, (H + 2 Hkv) * D]`` (Q | K | V column blocks, heads contiguous) -> ``(q, k, v)``
``[B, H|Hkv, S, D]`` in one program (K not transposed)."""
import ttnn
_check_4d_heads(qkv, "split_qkv input")
kv_heads = num_heads if num_kv_heads is None else int(num_kv_heads)
width = int(qkv.shape[-1])
if width % (num_heads + 2 * kv_heads) or (width // (num_heads + 2 * kv_heads)) % HEAD_DIM_MULTIPLE:
raise ValueError(f"width {width} does not split into {num_heads} + 2 x {kv_heads} heads of a multiple of "
f"{HEAD_DIM_MULTIPLE}")
return ttnn.experimental.nlp_create_qkv_heads(qkv, num_heads=num_heads, num_kv_heads=kv_heads,
transpose_k_heads=False, memory_config=memory_config)
def split_q_kv(q: Any, kv: Any, num_heads: int, *, num_kv_heads: Optional[int] = None, memory_config: Any = None):
"""Separate ``q`` ``[B, 1, S, H*D]`` and ``kv`` ``[B, 1, S, 2*Hkv*D]`` (K | V column blocks) with the SAME S
(self-attention with Q and K / V from different inputs, e.g. a pre-LN Q next to an un-normalised K / V)
-> ``(q, k, v)`` heads in one program. Cross-attention with another key length: :func:`split_heads` each."""
import ttnn
_check_4d_heads(q, "split_q_kv q")
_check_4d_heads(kv, "split_q_kv kv")
if int(q.shape[-2]) != int(kv.shape[-2]):
raise ValueError(f"split_q_kv needs the same sequence length (Q {q.shape[-2]}, K/V {kv.shape[-2]})")
kv_heads = num_heads if num_kv_heads is None else int(num_kv_heads)
return ttnn.experimental.nlp_create_qkv_heads(q, kv, num_heads=num_heads, num_kv_heads=kv_heads,
transpose_k_heads=False, memory_config=memory_config)
def merge_heads(x: Any, *, memory_config: Any = None):
"""``[B, H, S, D]`` -> ``[B, 1, S, H*D]`` (``nlp_concat_heads``). Prefer ``sdpa(..., concat_heads=True)``."""
import ttnn
if len(x.shape) != 4:
raise ValueError(f"merge_heads expects [B, H, S, D], got {tuple(x.shape)}")
return ttnn.experimental.nlp_concat_heads(x, memory_config=memory_config)
# ---------------------------------------------------------------------------------------------------- masks
def key_bias_row(valid: Sequence[bool], *, dtype: Any = np.float32) -> np.ndarray:
"""``[1, 1, 1, Sk]`` additive key bias: 0 for valid keys, ``-inf`` for masked ones (bf16-exact values)."""
v = np.asarray(valid, dtype=bool).reshape(-1)
if v.size == 0:
raise ValueError("key_bias_row: empty validity vector")
if not v.any():
raise ValueError("key_bias_row: every key is masked (the softmax of a fully masked row is undefined)")
return np.where(v, 0.0, -np.inf).astype(dtype).reshape(1, 1, 1, -1)
def key_bias(valid: Sequence[bool], sq: int, *, dtype: Any = np.float32) -> np.ndarray:
"""``[1, 1, Sq, Sk]`` additive key mask for :func:`sdpa` (identical rows), built on the host. Upload it as a
bf16 TILE tensor in DRAM (the op's requirement), or keep a :func:`key_bias_row` input and expand it on the
device with :func:`expand_key_bias`."""
row = key_bias_row(valid, dtype=dtype)
return np.ascontiguousarray(np.broadcast_to(row, (1, 1, int(sq), row.shape[-1])))
def expand_key_bias(row: Any, sq: int, *, memory_config: Any = None):
"""Device-side :func:`key_bias`: repeat a ``[1, 1, 1, Sk]`` bf16 TILE bias row to ``[1, 1, Sq, Sk]`` in DRAM
(one program; traceable)."""
import ttnn
if len(row.shape) != 4 or int(row.shape[-2]) != 1:
raise ValueError(f"expand_key_bias expects a [1, 1, 1, Sk] row, got {tuple(row.shape)}")
return ttnn.repeat(row, [1, 1, int(sq), 1],
memory_config=ttnn.DRAM_MEMORY_CONFIG if memory_config is None else memory_config)
# ---------------------------------------------------------------------------------------------------- head padding
def pad_head_dim(x: Any, padded: Optional[int] = None) -> np.ndarray:
"""Zero-pad the last (head) dim of a host array ``[..., D]`` to ``padded`` (default :func:`padded_head_dim`)."""
a = np.asarray(x)
d = a.shape[-1]
p = padded_head_dim(d) if padded is None else int(padded)
if p < d:
raise ValueError(f"padded head dim {p} < head dim {d}")
if p == d:
return a
return np.pad(a, [(0, 0)] * (a.ndim - 1) + [(0, p - d)])
def pad_head_columns(w: Any, b: Any, num_heads: int, head_dim: int,
padded: Optional[int] = None) -> Tuple[np.ndarray, Optional[np.ndarray]]:
"""Projection ``[in, H*D]`` (``y = x @ w + b``) -> ``[in, H*Dp]`` with zero columns after each head's D (and the
bias likewise), so the projection itself emits padded heads (exact: the padded columns are 0)."""
w = np.asarray(w)
p = padded_head_dim(head_dim) if padded is None else int(padded)
if w.shape[-1] != num_heads * head_dim:
raise ValueError(f"weight has {w.shape[-1]} columns, expected {num_heads} x {head_dim}")
wh = w.reshape(w.shape[:-1] + (num_heads, head_dim))
wp = np.zeros(w.shape[:-1] + (num_heads, p), w.dtype)
wp[..., :head_dim] = wh
bp = None
if b is not None:
b = np.asarray(b).reshape(num_heads, head_dim)
bp = np.zeros((num_heads, p), b.dtype)
bp[:, :head_dim] = b
bp = bp.reshape(-1)
return wp.reshape(w.shape[:-1] + (num_heads * p,)), bp
def pad_head_rows(w: Any, num_heads: int, head_dim: int, padded: Optional[int] = None) -> np.ndarray:
"""Output projection ``[H*D, out]`` -> ``[H*Dp, out]`` with zero rows at each head's padded positions, matching
the ``[.., H*Dp]`` concatenated-heads output of a padded attention."""
w = np.asarray(w)
p = padded_head_dim(head_dim) if padded is None else int(padded)
if w.shape[0] != num_heads * head_dim:
raise ValueError(f"weight has {w.shape[0]} rows, expected {num_heads} x {head_dim}")
wp = np.zeros((num_heads, p) + w.shape[1:], w.dtype)
wp[:, :head_dim] = w.reshape((num_heads, head_dim) + w.shape[1:])
return wp.reshape((num_heads * p,) + w.shape[1:])
# ---------------------------------------------------------------------------------------------------- oracle / fallback
def attention_reference(q: Any, k: Any, v: Any, *, scale: float, bias: Any = None) -> np.ndarray:
"""float64 numpy ``softmax(scale Q K^T + bias) V`` over ``[B, H, S, D]`` arrays (K / V heads broadcast when
``Hkv`` divides ``H``); ``bias`` broadcasts to ``[B, H, Sq, Sk]`` (``-inf`` allowed)."""
q = np.asarray(q, np.float64)
k = np.asarray(k, np.float64)
v = np.asarray(v, np.float64)
if k.shape[1] != q.shape[1]:
rep = q.shape[1] // k.shape[1]
k, v = np.repeat(k, rep, axis=1), np.repeat(v, rep, axis=1)
s = np.einsum("bhqd,bhkd->bhqk", q, k) * float(scale)
if bias is not None:
s = s + np.asarray(bias, np.float64)
s = s - s.max(axis=-1, keepdims=True)
p = np.exp(s)
p /= p.sum(axis=-1, keepdims=True)
return np.einsum("bhqk,bhkd->bhqd", p, v)
def attention_matmul(q: Any, k: Any, v: Any, *, scale: float, attn_mask: Any = None,
compute_kernel_config: Any = None):
"""Small-MHA fallback for what :func:`sdpa` rejects (e.g. fp32 Q / K / V): ``softmax(scale Q K^T + mask) V``
with two matmuls. ``q`` ``[B, H, Sq, D]``, ``k`` / ``v`` ``[B, H, Sk, D]`` with a tile-aligned ``Sk`` (pad the
keys and mask the padding with ``-inf``: a softmax over tile padding would include it). Materialises the
``[B, H, Sq, Sk]`` scores, so keep it for small shapes."""
import ttnn
from ..precision import compute_kernel_config as ckc
sk = int(k.shape[-2])
if sk % TILE:
raise ValueError(f"attention_matmul needs a tile-aligned Sk (got {sk}): pad the keys and mask them")
cfg = compute_kernel_config if compute_kernel_config is not None else ckc("HiFi4", fp32_acc=True)
scores = ttnn.matmul(q, k, transpose_b=True, compute_kernel_config=cfg)
scores = ttnn.multiply(scores, float(scale))
if attn_mask is not None:
scores = ttnn.add(scores, attn_mask)
probs = ttnn.softmax(scores, dim=-1, numeric_stable=True, compute_kernel_config=cfg)
return ttnn.matmul(probs, v, compute_kernel_config=cfg)