Q-TensorFormer / tests /test_model.py
Premchandyadav369
feat(hf): turnkey custom model with TensorTrainLinear, AdaptiveKVCache, PID controller, NVML profiler, OpenAI server, and drop-in compressor
15a4381
Raw History Blame Contribute Delete
6.4 kB
"""
Unit and integration test suite for Q-TensorFormer custom Hugging Face model.
Tests:
1. test_forward_pass_shape: Output logits shape == (batch_size, seq_len, vocab_size).
2. test_autoregressive_generation: model.generate() produces finite, valid sequences.
3. test_pid_controller_stability: 100 consecutive entropy steps suppress chattering via hysteresis.
4. test_attention_sink_retention: AdaptiveKVCache retains sink tokens during long generation.
5. test_tensor_train_linear_contraction: Einsum contraction matches reconstructed matrix and supports rank slicing.
6. test_gqa_attention: Grouped-Query Attention preserves head dimensions and causality.
"""
import math
import pytest
import torch
from configuration_qtensorformer import QTensorFormerConfig, factorize_dim
from modeling_qtensorformer import (
TensorTrainLinear,
AdaptiveKVCache,
PIDResourceController,
QTensorFormerAttention,
QTensorFormerForCausalLM,
)
@pytest.fixture
def small_config():
"""Lightweight configuration for rapid unit testing."""
return QTensorFormerConfig(
vocab_size=1000,
hidden_size=128,
num_hidden_layers=2,
num_attention_heads=4,
num_key_value_heads=2,
intermediate_size=256,
max_position_embeddings=256,
tt_ranks=[1, 4, 4, 1],
attention_sink_size=4,
kv_cache_window_size=16,
hysteresis_threshold=0.15,
)
def test_factorize_dim():
"""Verify that factorize_dim decomposes numbers into exact products."""
for dim in [128, 256, 512, 2048, 5632]:
factors = factorize_dim(dim, num_cores=3)
assert len(factors) == 3
assert math.prod(factors) == dim
def test_tensor_train_linear_contraction():
"""Verify TTLinear contraction against reconstructed dense matrix."""
torch.manual_seed(42)
in_features, out_features = 64, 128
tt_ranks = [1, 4, 4, 1]
layer = TensorTrainLinear(
in_features=in_features,
out_features=out_features,
tt_ranks=tt_ranks,
bias=True,
)
x = torch.randn(2, 5, in_features)
# 1. Direct TT forward contraction
out_tt = layer(x)
assert out_tt.shape == (2, 5, out_features)
assert torch.isfinite(out_tt).all()
# 2. Reconstructed dense matrix comparison
w_dense = layer.reconstruct_weight()
out_dense = torch.matmul(x, w_dense) + layer.bias
assert torch.allclose(out_tt, out_dense, atol=1e-4)
# 3. Dynamic rank-slicing test
out_sliced = layer(x, active_rank=2)
assert out_sliced.shape == (2, 5, out_features)
assert torch.isfinite(out_sliced).all()
def test_pid_controller_stability():
"""Simulate 100 consecutive random entropy tokens and ensure chattering is suppressed."""
torch.manual_seed(42)
controller = PIDResourceController(
kp=0.1,
ki=0.01,
kd=0.05,
hysteresis_threshold=0.15,
target_budget=1.0,
)
rank_history = []
chattering_suppression_count = 0
for step in range(100):
# Fluctuating cost around the target budget
cost = 1.0 + 0.3 * math.sin(step * 0.2) + (torch.rand(1).item() - 0.5) * 0.1
info = controller(measured_cost=cost)
rank_history.append(info["active_rank"])
if info["chattering_suppressed"]:
chattering_suppression_count += 1
# Ensure rank didn't change at every single step (hysteresis worked)
switches = sum(1 for i in range(1, len(rank_history)) if rank_history[i] != rank_history[i - 1])
assert switches < 50, f"Too many rapid switches ({switches}/100), hysteresis failed!"
assert chattering_suppression_count > 0, "No chattering was suppressed!"
def test_attention_sink_retention():
"""Verify that AdaptiveKVCache permanently preserves the first 4 tokens."""
sink_size = 4
window_size = 8
cache = AdaptiveKVCache(
sink_size=sink_size,
window_size=window_size,
quantize_cold=True,
quant_bits=4,
)
bsz, num_heads, head_dim = 1, 2, 16
# 1. Fill initial tokens (including sink)
init_k = torch.arange(10, dtype=torch.float32).view(1, 1, 10, 1).repeat(bsz, num_heads, 1, head_dim)
init_v = init_k.clone()
k_out, v_out = cache.update(init_k, init_v, layer_idx=0)
# 2. Append 20 more tokens one-by-one (exceeding window_size + sink_size)
for t in range(10, 30):
step_k = torch.tensor([[[[float(t)] * head_dim]]]).repeat(bsz, num_heads, 1, 1)
step_v = step_k.clone()
k_out, v_out = cache.update(step_k, step_v, layer_idx=0)
# Verify sink tokens are permanently preserved at the beginning
first_tokens = k_out[:, :, :sink_size, 0]
expected_sink = torch.arange(sink_size, dtype=torch.float32).view(1, 1, sink_size).repeat(bsz, num_heads, 1)
assert torch.allclose(first_tokens, expected_sink), "Sink tokens were lost during eviction!"
def test_gqa_attention(small_config):
"""Verify Grouped-Query Attention dimensions and caching."""
layer = QTensorFormerAttention(small_config, layer_idx=0)
x = torch.randn(2, 8, small_config.hidden_size)
# Prefill forward
out, _, past = layer(x, use_cache=True)
assert out.shape == (2, 8, small_config.hidden_size)
assert torch.isfinite(out).all()
def test_forward_pass_shape(small_config):
"""Verify model forward pass output shape matches (B, S, V)."""
model = QTensorFormerForCausalLM(small_config)
model.eval()
bsz, seq_len = 2, 16
input_ids = torch.randint(0, small_config.vocab_size, (bsz, seq_len))
with torch.no_grad():
outputs = model(input_ids)
logits = outputs.logits
assert logits.shape == (bsz, seq_len, small_config.vocab_size)
assert torch.isfinite(logits).all(), "Logits contain NaN or Inf values!"
def test_autoregressive_generation(small_config):
"""Verify standard Hugging Face model.generate() works seamlessly."""
model = QTensorFormerForCausalLM(small_config)
model.eval()
prompt_ids = torch.randint(0, small_config.vocab_size, (1, 4))
gen_tokens = 6
with torch.no_grad():
generated = model.generate(
prompt_ids,
max_new_tokens=gen_tokens,
do_sample=False,
use_cache=True,
)
assert generated.shape == (1, 4 + gen_tokens)
assert (generated >= 0).all() and (generated < small_config.vocab_size).all()