Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions tests/unit_tests/backends/torchtitan/test_llama3_turbo_attention.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
###############################################################################
# Copyright (c) 2026, Advanced Micro Devices, Inc. All rights reserved.
#
# See LICENSE for license information.
###############################################################################

"""
Unit tests for the LLaMA3 Primus-Turbo Attention mirror (``forward``).

These tests need torchtitan (to build the real ``wq``/``wk``/``wv``/``wo``
linears via the base ``Attention.__init__``) and are auto-skipped otherwise.
The heavy ``inner_attention`` (TurboAttention) is replaced with a lightweight
stub so the tests exercise only the Primus ``forward`` override: projection,
reshape (no ``repeat_kv``/transpose), rotary embedding application, and the
final output projection.
"""

import pytest
import torch
import torch.nn as nn


@pytest.fixture
def llama3_args():
pytest.importorskip("torchtitan")
from torchtitan.models.llama3.model.args import TransformerModelArgs

return TransformerModelArgs(
dim=32,
n_heads=4,
n_kv_heads=2,
vocab_size=64,
n_layers=1,
max_seq_len=16,
)


class _IdentityInnerAttention(nn.Module):
"""Stand-in for TurboAttention: (bs, seqlen, n_heads, head_dim) in and out."""

def __init__(self):
super().__init__()

def forward(self, xq, xk, xv):
return xq


class _CaptureInnerAttention(nn.Module):
def __init__(self):
super().__init__()
self.shapes = {}

def forward(self, xq, xk, xv):
self.shapes["xq_shape"] = tuple(xq.shape)
self.shapes["xk_shape"] = tuple(xk.shape)
self.shapes["xv_shape"] = tuple(xv.shape)
return xq


class TestLlama3TurboAttentionForward:
def test_forward_output_shape(self, llama3_args):
from primus.backends.torchtitan.models.llama3.model.model import Attention

attn = Attention(llama3_args)
attn.inner_attention = _IdentityInnerAttention()

bs, seqlen = 2, 8
x = torch.randn(bs, seqlen, llama3_args.dim)
freqs_cis = torch.ones(seqlen, llama3_args.dim // llama3_args.n_heads // 2, dtype=torch.complex64)

out = attn.forward(x, freqs_cis, attention_masks=None)

assert out.shape == (bs, seqlen, llama3_args.dim)

def test_inner_attention_receives_unrepeated_untransposed_heads(self, llama3_args):
# The Primus forward skips `repeat_kv` and the (bs, heads, seq, dim)
# transpose entirely -- TurboAttention consumes (bs, seq, heads, dim)
# directly and expands GQA internally. Assert xk/xv keep n_kv_heads
# (not repeated up to n_heads) and nothing is transposed.
from primus.backends.torchtitan.models.llama3.model.model import Attention

attn = Attention(llama3_args)
stub = _CaptureInnerAttention()
attn.inner_attention = stub

bs, seqlen = 2, 8
head_dim = llama3_args.dim // llama3_args.n_heads
x = torch.randn(bs, seqlen, llama3_args.dim)
freqs_cis = torch.ones(seqlen, head_dim // 2, dtype=torch.complex64)

attn.forward(x, freqs_cis, attention_masks=None)

assert stub.shapes["xq_shape"] == (bs, seqlen, llama3_args.n_heads, head_dim)
# GQA: kv heads stay at n_kv_heads, i.e. NOT repeated up to n_heads.
assert stub.shapes["xk_shape"] == (bs, seqlen, llama3_args.n_kv_heads, head_dim)
assert stub.shapes["xv_shape"] == (bs, seqlen, llama3_args.n_kv_heads, head_dim)
Comment on lines +93 to +96

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestIntel PR Steward: Thanks — I looked at whether this test can actually see the failure mode you describe.

This test instantiates Attention(llama3_args) directly with no tensor-parallel mesh or sharding applied anywhere in the test (no DeviceMesh, no parallelize_module, no distributed process group). wq/wk/wv are therefore always built with the global n_heads/n_kv_heads from llama3_args, so xq.view(bs, seqlen, -1, head_dim) always resolves to llama3_args.n_heads here — the -1 and the global arg are the same number in this unit test's scope. So the assertions against llama3_args.n_heads/n_kv_heads are correct for what this test actually exercises.

Deriving the expected counts from wq(x)/wk(x)/wv(x) shapes instead would be a defensive nice-to-have (guards against a future test that does add TP sharding), but it doesn't change correctness today, and this PR's scope is pinning the existing GQA-layout behavior for the test gap (PRPUNDIT-20), not adding TP coverage. Leaving the assertions as-is; happy to revisit if a follow-up test adds actual sharded execution.


def test_forward_passes_positions_to_rotary_emb(self, llama3_args, monkeypatch):
import primus.backends.torchtitan.models.llama3.model.model as llama3_mirror

attn = llama3_mirror.Attention(llama3_args)
attn.inner_attention = _IdentityInnerAttention()

captured = {}
real_apply_rotary_emb = llama3_mirror.apply_rotary_emb

def _spy_apply_rotary_emb(xq, xk, freqs_cis, positions=None):
captured["positions"] = positions
return real_apply_rotary_emb(xq, xk, freqs_cis=freqs_cis, positions=positions)

monkeypatch.setattr(llama3_mirror, "apply_rotary_emb", _spy_apply_rotary_emb)

bs, seqlen = 1, 4
head_dim = llama3_args.dim // llama3_args.n_heads
x = torch.randn(bs, seqlen, llama3_args.dim)
freqs_cis = torch.ones(seqlen, head_dim // 2, dtype=torch.complex64)
positions = torch.arange(seqlen).unsqueeze(0)

attn.forward(x, freqs_cis, attention_masks=None, positions=positions)

assert captured["positions"] is positions

def test_forward_defaults_positions_to_none(self, llama3_args):
from primus.backends.torchtitan.models.llama3.model.model import Attention

attn = Attention(llama3_args)
attn.inner_attention = _IdentityInnerAttention()

bs, seqlen = 1, 4
head_dim = llama3_args.dim // llama3_args.n_heads
x = torch.randn(bs, seqlen, llama3_args.dim)
freqs_cis = torch.ones(seqlen, head_dim // 2, dtype=torch.complex64)

# Should not raise even though `positions` is omitted.
out = attn.forward(x, freqs_cis, attention_masks=None)
assert out.shape == (bs, seqlen, llama3_args.dim)

def test_forward_uses_wo_projection(self, llama3_args):
# Sanity check that the mirror still routes through the base
# `Attention.wo` output projection rather than returning the raw
# inner_attention output.
from primus.backends.torchtitan.models.llama3.model.model import Attention

attn = Attention(llama3_args)
attn.inner_attention = _IdentityInnerAttention()

bs, seqlen = 1, 4
head_dim = llama3_args.dim // llama3_args.n_heads
x = torch.randn(bs, seqlen, llama3_args.dim)
freqs_cis = torch.ones(seqlen, head_dim // 2, dtype=torch.complex64)

out = attn.forward(x, freqs_cis, attention_masks=None)

xq = attn.wq(x).view(bs, seqlen, -1, head_dim)
expected = attn.wo(xq.contiguous().view(bs, seqlen, -1))
# RoPE is a no-op here since freqs_cis is all-ones (zero phase), so the
# identity inner_attention path reduces to wo(view(wq(x))).
assert torch.allclose(out, expected, atol=1e-5)
Loading