Module neuralops.tests.test_adaptive_sparse_attention

Functions

def test_causal_mask_before_topk()
Expand source code
def test_causal_mask_before_topk():
    # In decoder mode, future tokens must receive -10000.0 before sort, meaning attention weights to future tokens are strictly zero
    p = AdaptiveSparseAttention(d_model=64, num_heads=4, is_decoder=True, is_causal=True, sparsity_ratio=0.5)
    L = 6
    x = torch.randn(1, L, 64)
    _, attn = p(x, output_attentions=True)
    assert attn is not None
    # For any query i, key j > i must have attention weight exactly 0
    for i in range(L):
        for j in range(i + 1, L):
            assert torch.all(attn[:, :, i, j] == 0.0), f"Future position ({i}, {j}) has non-zero attention {attn[:, :, i, j]}"
def test_default_sparsity_per_family()
Expand source code
def test_default_sparsity_per_family():
    # 1. Encoder model_type (e.g., 'bert', 'roberta') defaults to 0.1
    cfg_enc = SimpleNamespace(model_type="bert", num_attention_heads=4, hidden_size=64, is_decoder=False)
    p_enc = AdaptiveSparseAttention(config=cfg_enc)
    assert p_enc.default_sparsity == pytest.approx(0.1, abs=1e-3)
    assert not p_enc.is_decoder

    # 2. Decoder model_type (e.g., 'gpt2', 'qwen') defaults to 0.5
    cfg_dec = SimpleNamespace(model_type="gpt2", num_attention_heads=4, hidden_size=64, is_decoder=True)
    p_dec = AdaptiveSparseAttention(config=cfg_dec)
    assert p_dec.default_sparsity == pytest.approx(0.5, abs=1e-3)
    assert p_dec.is_decoder

    # 3. Explicit sparsity_ratio passed in comp_config or kwargs overrides defaults
    comp_cfg = SimpleNamespace(adaptive_sparsity_ratio=0.35)
    p_override = AdaptiveSparseAttention(config=cfg_enc, comp_config=comp_cfg)
    assert p_override.default_sparsity == pytest.approx(0.35, abs=1e-3)

    # 4. adaptive_sparse_encoder_sparsity config parameter
    p_custom_enc = AdaptiveSparseAttention(config=cfg_enc, adaptive_sparse_encoder_sparsity=0.15)
    assert p_custom_enc.default_sparsity == pytest.approx(0.15, abs=1e-3)

    # Verify sparsity_logits is learnable (requires_grad=True)
    assert p_enc.sparsity_logits.requires_grad
    assert p_dec.sparsity_logits.requires_grad
def test_head_mask_shapes()
Expand source code
def test_head_mask_shapes():
    p = AdaptiveSparseAttention(d_model=64, num_heads=4)
    x = torch.randn(2, 8, 64)
    H = 4
    B = 2

    # Shape [H]
    hm_1d = torch.ones(H)
    out1, _ = p(x, head_mask=hm_1d)
    assert out1.shape == (B, 8, 64)

    # Shape [B, H]
    hm_2d = torch.ones(B, H)
    out2, _ = p(x, head_mask=hm_2d)
    assert out2.shape == (B, 8, 64)

    # Shape [B, H, 1, 1]
    hm_4d = torch.ones(B, H, 1, 1)
    out4, _ = p(x, head_mask=hm_4d)
    assert out4.shape == (B, 8, 64)
def test_injection_safety()
Expand source code
def test_injection_safety():
    class MissingProjectionsLayer(nn.Module):
        def __init__(self):
            super().__init__()
            self.dummy = nn.Linear(64, 64)

    # Layer lacks attention weights
    layer = MissingProjectionsLayer()
    res = inject_adaptive_sparse(layer=layer)
    assert res is None
def test_lora_freezing()
Expand source code
def test_lora_freezing():
    class DummyAttn(nn.Module):
        def __init__(self):
            super().__init__()
            self.q_proj = nn.Linear(64, 64)
            self.k_proj = nn.Linear(64, 64)
            self.v_proj = nn.Linear(64, 64)
            self.out_proj = nn.Linear(64, 64)

    old = DummyAttn()
    p = AdaptiveSparseAttention(d_model=64, num_heads=4, pretrained_attn=old, lora_rank=16)
    for mod in (p.q_proj, p.k_proj, p.v_proj, p.out_proj):
        assert not mod.weight.requires_grad
        if mod.bias is not None:
            assert not mod.bias.requires_grad
        assert mod.lora_A.requires_grad
        assert mod.lora_B.requires_grad
def test_nested_encoder_bypass()
Expand source code
def test_nested_encoder_bypass():
    class MockBertSelfAttention(nn.Module):
        def __init__(self):
            super().__init__()
            self.query = nn.Linear(64, 64)
            self.key = nn.Linear(64, 64)
            self.value = nn.Linear(64, 64)
            # Notice: no out_proj, no dense, no c_proj

    src = MockBertSelfAttention()
    p = AdaptiveSparseAttention(d_model=64, num_heads=4, pretrained_attn=src)
    assert p.is_nested_encoder is True

    # Forward should return context directly without crashing
    x = torch.randn(2, 8, 64)
    out, _ = p(x)
    assert out.shape == (2, 8, 64)
def test_rope_fallback_both_signatures()
Expand source code
def test_rope_fallback_both_signatures():
    # 1. Signature with (x, seq_len)
    class MockRotarySeqLen(nn.Module):
        def forward(self, x, seq_len=None):
            B, H, N, d = x.shape
            cos = torch.ones(B, 1, N, d)
            sin = torch.zeros(B, 1, N, d)
            return cos, sin

    # 2. Signature with (x, position_ids)
    class MockRotaryPosIds(nn.Module):
        def forward(self, x, position_ids=None):
            B, H, N, d = x.shape
            cos = torch.ones(B, 1, N, d)
            sin = torch.zeros(B, 1, N, d)
            return cos, sin

    p1 = AdaptiveSparseAttention(d_model=64, num_heads=4)
    p1.rotary_emb = MockRotarySeqLen()
    x = torch.randn(2, 8, 64)
    out1, _ = p1(x, position_embeddings=None)
    assert out1.shape == (2, 8, 64)
    assert torch.isfinite(out1).all()

    p2 = AdaptiveSparseAttention(d_model=64, num_heads=4)
    p2.rotary_emb = MockRotaryPosIds()
    out2, _ = p2(x, position_embeddings=None)
    assert out2.shape == (2, 8, 64)
    assert torch.isfinite(out2).all()
def test_topk_threshold_correctness()
Expand source code
def test_topk_threshold_correctness():
    sparsity = 0.5
    L = 10
    H = 4
    # Bidirectional encoder with fixed 50% sparsity
    p = AdaptiveSparseAttention(d_model=64, num_heads=H, is_decoder=False, is_causal=False, sparsity_ratio=sparsity)
    p.eval()  # Set eval mode to avoid dropout zeroing entries
    x = torch.randn(2, L, 64)
    _, attn = p(x, output_attentions=True)
    assert attn is not None
    # For each row of each head, top-k entries carry dominant mass (k = 5)
    expected_k = int(round((1.0 - sparsity) * L))
    topk_mass = torch.topk(attn, expected_k, dim=-1).values.sum(dim=-1)
    assert torch.all(topk_mass > 0.65)
    assert (attn < 0.05).float().mean().item() > 0.25
def test_weight_copy_all_four_architectures()
Expand source code
def test_weight_copy_all_four_architectures():
    hidden_size = 64
    num_heads = 4
    head_dim = 16

    # 1. DistilBERT style: q_lin, k_lin, v_lin, out_lin
    class MockDistilBertAttn(nn.Module):
        def __init__(self):
            super().__init__()
            self.q_lin = nn.Linear(hidden_size, hidden_size)
            self.k_lin = nn.Linear(hidden_size, hidden_size)
            self.v_lin = nn.Linear(hidden_size, hidden_size)
            self.out_lin = nn.Linear(hidden_size, hidden_size)

    d_src = MockDistilBertAttn()
    d_block = AdaptiveSparseAttention(d_model=hidden_size, num_heads=num_heads, pretrained_attn=d_src)
    for name, mod in [("q", d_block.q_proj), ("k", d_block.k_proj), ("v", d_block.v_proj), ("o", d_block.out_proj)]:
        assert float(torch.norm(mod.weight).item()) > 1e-4

    # 2. RoBERTa style: self.query, self.key, self.value, output.dense
    class MockRobertaSelf(nn.Module):
        def __init__(self):
            super().__init__()
            self.query = nn.Linear(hidden_size, hidden_size)
            self.key = nn.Linear(hidden_size, hidden_size)
            self.value = nn.Linear(hidden_size, hidden_size)

    class MockRobertaOutput(nn.Module):
        def __init__(self):
            super().__init__()
            self.dense = nn.Linear(hidden_size, hidden_size)

    class MockRobertaAttn(nn.Module):
        def __init__(self):
            super().__init__()
            self.self = MockRobertaSelf()
            self.output = MockRobertaOutput()

    r_src = MockRobertaAttn()
    r_block = AdaptiveSparseAttention(d_model=hidden_size, num_heads=num_heads, pretrained_attn=r_src)
    for name, mod in [("q", r_block.q_proj), ("k", r_block.k_proj), ("v", r_block.v_proj), ("o", r_block.out_proj)]:
        assert float(torch.norm(mod.weight).item()) > 1e-4

    # 3. GPT-2 style: c_attn [hidden_size, 3 * hidden_size], c_proj [hidden_size, hidden_size]
    class MockConv1D(nn.Module):
        def __init__(self, in_f, out_f):
            super().__init__()
            self.weight = nn.Parameter(torch.randn(in_f, out_f))
            self.bias = nn.Parameter(torch.zeros(out_f))

    class MockGPT2Attn(nn.Module):
        def __init__(self):
            super().__init__()
            self.c_attn = MockConv1D(hidden_size, 3 * hidden_size)
            self.c_proj = MockConv1D(hidden_size, hidden_size)

    g_src = MockGPT2Attn()
    g_block = AdaptiveSparseAttention(d_model=hidden_size, num_heads=num_heads, pretrained_attn=g_src)
    for name, mod in [("q", g_block.q_proj), ("k", g_block.k_proj), ("v", g_block.v_proj), ("o", g_block.out_proj)]:
        assert float(torch.norm(mod.weight).item()) > 1e-4

    # 4. Qwen / LLaMA style: q_proj, k_proj, v_proj, o_proj with GQA (num_kv_heads=2 < num_heads=4)
    class MockQwenAttn(nn.Module):
        def __init__(self):
            super().__init__()
            self.head_dim = head_dim
            self.num_heads = num_heads
            self.num_key_value_heads = 2
            self.q_proj = nn.Linear(hidden_size, num_heads * head_dim)
            self.k_proj = nn.Linear(hidden_size, 2 * head_dim)
            self.v_proj = nn.Linear(hidden_size, 2 * head_dim)
            self.o_proj = nn.Linear(num_heads * head_dim, hidden_size)

    q_src = MockQwenAttn()
    cfg_qwen = SimpleNamespace(model_type="qwen2", num_attention_heads=num_heads, num_key_value_heads=2, hidden_size=hidden_size)
    q_block = AdaptiveSparseAttention(config=cfg_qwen, pretrained_attn=q_src)
    assert q_block.num_kv_heads == 2
    assert q_block.kv_repeat == 2
    for name, mod in [("q", q_block.q_proj), ("k", q_block.k_proj), ("v", q_block.v_proj), ("o", q_block.out_proj)]:
        assert float(torch.norm(mod.weight).item()) > 1e-4