"""Integration tests for generation API compatibility.
This module tests generation API features including HuggingFace-style ModelOutput
support and TransformerBridge batch dimension compatibility.
"""
import warnings
import pytest
import torch
from transformer_lens import HookedTransformer
from transformer_lens.model_bridge import TransformerBridge
@pytest.fixture(scope="module")
def gpt2_ht():
"""Load GPT-2 HookedTransformer once per module."""
return HookedTransformer.from_pretrained("gpt2", device="cpu")
@pytest.fixture(scope="module")
def gpt2_bridge():
"""Load GPT-2 TransformerBridge once per module."""
bridge = TransformerBridge.boot_transformers("gpt2", device="cpu")
if bridge.tokenizer.pad_token is None:
bridge.tokenizer.pad_token = bridge.tokenizer.eos_token
return bridge
class TestHookedTransformerGenerationModelOutput:
"""Tests for HookedTransformer generation with ModelOutput returns."""
def test_generate_with_output_logits_returns_modeloutput(self, gpt2_ht):
"""Test that output_logits=True returns a ModelOutput with sequences and logits."""
prompt = "The quick brown"
max_new_tokens = 5
result = gpt2_ht.generate(
prompt,
max_new_tokens=max_new_tokens,
do_sample=False,
verbose=False,
output_logits=True,
)
assert hasattr(result, "sequences"), "Result should have sequences attribute"
assert hasattr(result, "logits"), "Result should have logits attribute"
assert isinstance(result.sequences, torch.Tensor), "sequences should be a tensor"
assert result.sequences.ndim == 2, "sequences should be 2D [batch, pos]"
assert isinstance(result.logits, tuple), "logits should be a tuple"
assert (
len(result.logits) == max_new_tokens
), f"logits tuple should have {max_new_tokens} elements"
for i, logit in enumerate(result.logits):
assert isinstance(logit, torch.Tensor), f"logits[{i}] should be a tensor"
assert logit.ndim == 2, f"logits[{i}] should be 2D [batch, vocab]"
assert (
logit.shape[0] == result.sequences.shape[0]
), f"logits[{i}] batch size should match sequences"
assert (
logit.shape[1] == gpt2_ht.cfg.d_vocab
), f"logits[{i}] vocab size should match model config"
def test_generate_without_output_logits_returns_normal(self, gpt2_ht):
"""Test that without output_logits flag, generation returns normal format."""
prompt = "The quick brown"
result = gpt2_ht.generate(
prompt,
max_new_tokens=5,
do_sample=False,
verbose=False,
)
assert isinstance(result, str), "Result should be a string"
assert len(result) > len(prompt), "Generated text should be longer than prompt"
def test_generate_output_logits_with_return_type_tokens(self, gpt2_ht):
"""Test output_logits with return_type='tokens' returns ModelOutput with token sequences."""
prompt = "Hello world"
max_new_tokens = 3
result = gpt2_ht.generate(
prompt,
max_new_tokens=max_new_tokens,
return_type="tokens",
do_sample=False,
verbose=False,
output_logits=True,
)
assert hasattr(result, "sequences"), "Result should have sequences"
assert hasattr(result, "logits"), "Result should have logits"
assert isinstance(result.sequences, torch.Tensor), "sequences should be a tensor"
assert result.sequences.dtype in [
torch.long,
torch.int,
torch.int64,
], "sequences should be integer tokens"
assert len(result.logits) == max_new_tokens, "logits should match max_new_tokens"
def test_return_dict_in_generate_silently_ignored(self, gpt2_ht):
"""Test that return_dict_in_generate is silently ignored without warnings."""
prompt = "Test"
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = gpt2_ht.generate(
prompt,
max_new_tokens=2,
verbose=False,
return_dict_in_generate=True,
)
assert len(w) == 0, "return_dict_in_generate should be silently ignored"
assert isinstance(result, str), "Result should be a string"
def test_unsupported_hf_flags_trigger_warning(self, gpt2_ht):
"""Test that unsupported HF generation kwargs trigger UserWarning."""
prompt = "Test"
with pytest.warns(UserWarning, match="unsupported generation kwargs"):
result = gpt2_ht.generate(
prompt,
max_new_tokens=2,
verbose=False,
output_scores=True,
output_attentions=True,
)
assert isinstance(result, str), "Result should be a string despite warnings"
def test_logits_consistency_with_forward_pass(self, gpt2_ht):
"""Test that logits from generate match those from forward pass."""
prompt = "Hello"
result = gpt2_ht.generate(
prompt,
max_new_tokens=1,
do_sample=False,
verbose=False,
output_logits=True,
)
first_new_token = result.sequences[0, -1]
first_logits = result.logits[0][0]
assert first_logits.argmax() == first_new_token, "Greedy token should match logits argmax"
def test_output_logits_batch_generation(self, gpt2_ht):
"""Test output_logits works with batch inputs."""
prompts = ["Hello", "World"]
max_new_tokens = 3
result = gpt2_ht.generate(
prompts,
max_new_tokens=max_new_tokens,
do_sample=False,
verbose=False,
output_logits=True,
)
assert result.sequences.shape[0] == len(
prompts
), "Batch dimension should match number of prompts"
for logit in result.logits:
assert logit.shape[0] == len(prompts), "Logits batch dimension should match prompts"
class TestTransformerBridgeGenerationModelOutput:
"""Tests for TransformerBridge generation with ModelOutput returns."""
def test_generate_with_output_logits_returns_modeloutput(self, gpt2_bridge):
"""Test that output_logits=True returns a ModelOutput with sequences and logits."""
prompt = "The quick brown"
max_new_tokens = 5
result = gpt2_bridge.generate(
prompt,
max_new_tokens=max_new_tokens,
do_sample=False,
verbose=False,
output_logits=True,
)
assert hasattr(result, "sequences"), "Result should have sequences attribute"
assert hasattr(result, "logits"), "Result should have logits attribute"
assert isinstance(result.sequences, torch.Tensor), "sequences should be a tensor"
assert result.sequences.ndim == 2, "sequences should be 2D [batch, pos]"
assert isinstance(result.logits, tuple), "logits should be a tuple"
assert (
len(result.logits) == max_new_tokens
), f"logits tuple should have {max_new_tokens} elements"
for i, logit in enumerate(result.logits):
assert isinstance(logit, torch.Tensor), f"logits[{i}] should be a tensor"
assert logit.ndim == 2, f"logits[{i}] should be 2D [batch, vocab]"
assert (
logit.shape[0] == result.sequences.shape[0]
), f"logits[{i}] batch size should match sequences"
assert (
logit.shape[1] == gpt2_bridge.cfg.d_vocab
), f"logits[{i}] vocab size should match model config"
def test_generate_without_output_logits_returns_normal(self, gpt2_bridge):
"""Test that without output_logits flag, generation returns normal format."""
prompt = "The quick brown"
result = gpt2_bridge.generate(
prompt,
max_new_tokens=5,
do_sample=False,
verbose=False,
)
assert isinstance(result, str), "Result should be a string"
assert len(result) > len(prompt), "Generated text should be longer than prompt"
def test_generate_output_logits_batch(self, gpt2_bridge):
"""Test output_logits works with batch inputs."""
prompts = ["Hello", "World"]
max_new_tokens = 3
result = gpt2_bridge.generate(
prompts,
max_new_tokens=max_new_tokens,
do_sample=False,
verbose=False,
output_logits=True,
)
assert hasattr(result, "sequences"), "Result should have sequences"
assert hasattr(result, "logits"), "Result should have logits"
assert result.sequences.shape[0] == len(
prompts
), "Batch dimension should match number of prompts"
for logit in result.logits:
assert logit.shape[0] == len(prompts), "Logits batch dimension should match prompts"
class TestTransformerBridgeHFGenerate:
"""Tests for TransformerBridge.hf_generate() with full HF API support."""
def test_hf_generate_with_output_scores(self, gpt2_bridge):
"""Test that output_scores is forwarded to HF model."""
prompt = "Test"
result = gpt2_bridge.hf_generate(
prompt,
max_new_tokens=3,
do_sample=False,
output_scores=True,
)
assert isinstance(result, str), "Result should be a string"
def test_hf_generate_sets_return_dict_in_generate(self, gpt2_bridge):
"""Test that hf_dict_flags automatically set return_dict_in_generate=True."""
prompt = "Hello"
result = gpt2_bridge.hf_generate(
prompt,
max_new_tokens=2,
do_sample=False,
output_logits=True,
)
assert isinstance(result, str), "Result should be generated successfully"
def test_hf_generate_multiple_flags_simultaneously(self, gpt2_bridge):
"""Test that multiple HF-style flags can be passed simultaneously."""
prompt = "Test"
result = gpt2_bridge.hf_generate(
prompt,
max_new_tokens=2,
do_sample=False,
output_logits=True,
output_attentions=True,
output_hidden_states=True,
)
assert isinstance(result, str), "Result should be generated with multiple flags"
def test_hf_generate_return_type_tokens(self, gpt2_bridge):
"""Test return_type='tokens' works with HF flags."""
prompt = "Hello"
result = gpt2_bridge.hf_generate(
prompt,
max_new_tokens=2,
return_type="tokens",
do_sample=False,
output_logits=True,
)
assert result is not None, "Result should not be None"
def test_hf_generate_flags_coerced_to_bool(self, gpt2_bridge):
"""Test that HF flags are properly coerced to boolean values."""
prompt = "Test"
result = gpt2_bridge.hf_generate(
prompt,
max_new_tokens=2,
do_sample=False,
output_logits=1,
output_scores=0,
)
assert isinstance(result, str) or result is not None, "Result should be generated"
def test_hf_generate_batch_generation(self, gpt2_bridge):
"""Test batch generation works with HF-style flags."""
prompts = ["Hello", "World"]
result = gpt2_bridge.hf_generate(
prompts,
max_new_tokens=2,
do_sample=False,
output_logits=True,
)
assert isinstance(result, list), "Batch input should return list"
assert len(result) == len(prompts), "Output list should match input length"
class TestGenerationBackwardCompatibility:
"""Tests to ensure backward compatibility with existing generation usage."""
def test_hooked_transformer_basic_generation_unchanged(self, gpt2_ht):
"""Test that basic generation without new flags works as before."""
prompt = "Hello world"
result = gpt2_ht.generate(
prompt,
max_new_tokens=5,
do_sample=False,
verbose=False,
)
assert isinstance(result, str), "Basic generation should return string"
assert len(result) > len(prompt), "Generated text should be longer"
def test_bridge_basic_generation_unchanged(self, gpt2_bridge):
"""Test that basic bridge generation without new flags works as before."""
prompt = "Hello world"
result = gpt2_bridge.generate(
prompt,
max_new_tokens=5,
do_sample=False,
verbose=False,
)
assert isinstance(result, str), "Basic generation should return string"
assert len(result) > len(prompt), "Generated text should be longer"
def test_hooked_transformer_return_types_unchanged(self, gpt2_ht):
"""Test that all return_type options still work."""
prompt = "Test"
result_str = gpt2_ht.generate(
prompt, max_new_tokens=2, return_type="str", verbose=False, do_sample=False
)
assert isinstance(result_str, str), "return_type='str' should return string"
result_tokens = gpt2_ht.generate(
prompt, max_new_tokens=2, return_type="tokens", verbose=False, do_sample=False
)
assert isinstance(result_tokens, torch.Tensor), "return_type='tokens' should return tensor"
result_embeds = gpt2_ht.generate(
prompt, max_new_tokens=2, return_type="embeds", verbose=False, do_sample=False
)
assert isinstance(result_embeds, torch.Tensor), "return_type='embeds' should return tensor"
assert result_embeds.ndim == 3, "Embeddings should be 3D"
class TestBlockBridgeBatchCompatibility:
"""Tests for BlockBridge tuple return format and batch dimension preservation."""
def test_block_bridge_batched_generation_compatibility(self, gpt2_bridge):
"""Test BlockBridge maintains tuple format and batch dimensions during generation.
This test exercises two critical aspects of improved HF compatibility:
1. BlockBridge.forward() always returns tuples (not bare tensors)
2. Batch dimensions are preserved through multi-block generation pipeline
"""
batch_size = 2
seq_len = 8
hidden_dim = gpt2_bridge.cfg.d_model
hidden_states = torch.randn(batch_size, seq_len, hidden_dim)
first_block = gpt2_bridge.original_model.transformer.h[0]
block_output = first_block(hidden_states)
assert isinstance(
block_output, tuple
), f"BlockBridge must return tuple for HF compatibility, got {type(block_output)}"
assert isinstance(
block_output[0], torch.Tensor
), "First element of BlockBridge output must be a tensor"
assert block_output[0].shape == (
batch_size,
seq_len,
hidden_dim,
), f"Expected shape [{batch_size}, {seq_len}, {hidden_dim}], got {block_output[0].shape}"
assert (
block_output[0].shape[0] == batch_size
), f"Batch dimension lost! Expected {batch_size}, got {block_output[0].shape[0]}"
prompts = ["Hello world", "Goodbye world"]
tokens = gpt2_bridge.to_tokens(prompts, prepend_bos=False, padding_side="left")
output = gpt2_bridge.generate(
tokens,
max_new_tokens=4,
do_sample=False,
use_past_kv_cache=True,
verbose=False,
)
assert output.shape[0] == len(
prompts
), f"Batch size must be preserved through generation. Expected {len(prompts)}, got {output.shape[0]}"
assert (
output.shape[1] > tokens.shape[1]
), "Generation should produce longer sequences than input"
assert not torch.equal(
output[0], output[1]
), "Batch items should be independent - different prompts should produce different outputs"
if __name__ == "__main__":
pytest.main([__file__, "-v"])