forked from tinygrad/tinygrad
Autogen / In-tree Autogen (comgr 2) (push) Successful in 47s
Unit Tests / Torch Backend Tests (push) Failing after 1m3s
Unit Tests / Linters (push) Failing after 1m27s
Unit Tests / Python Backend (push) Failing after 3m8s
Unit Tests / Fuzzing (push) Failing after 21s
Unit Tests / Docs (push) Successful in 3m32s
Unit Tests / Null Tests (push) Successful in 4m8s
Unit Tests / Unit Tests (push) Successful in 3m30s
Unit Tests / CL IMAGE Tests (push) Failing after 44s
Unit Tests / openpilot Compile Tests (push) Failing after 47s
Unit Tests / Test LLM (push) Failing after 13s
Unit Tests / SPEC=2 (1) (push) Successful in 3m53s
Deploy Docs / deploy (push) Successful in 5m1s
Unit Tests / Optimization Tests (push) Failing after 49s
Unit Tests / SPEC=2 (2) (push) Successful in 3m42s
Unit Tests / Linux (DEV=CL) (push) Failing after 38s
Unit Tests / Models (push) Successful in 1m39s
Unit Tests / Linux (DEV=CPU:X86) (push) Failing after 23s
Unit Tests / Linux (DSP) (push) Successful in 1m47s
Unit Tests / hcq2 (push) Failing after 48s
Unit Tests / Linux (am) (push) Failing after 52s
Unit Tests / Linux (DEV=CPU:LLVM) (push) Successful in 3m13s
Unit Tests / Linux (DEV=CPU:LVP) (push) Successful in 3m6s
Unit Tests / AMD ASM IDE (push) Failing after 2m46s
Unit Tests / Linux (DEV=CPU:CLANG) (push) Successful in 4m19s
Unit Tests / ONNX (CPU) Tests (push) Successful in 5m24s
Autogen / In-tree Autogen (push) Successful in 10m40s
Unit Tests / Linux (DEV=WEBGPU) (push) Failing after 5m0s
Unit Tests / Linux (amdllvm gfx1100) (push) Successful in 3m40s
Unit Tests / Linux (amd gfx1100) (push) Successful in 4m13s
Unit Tests / Linux (amdllvm gfx1201) (push) Successful in 3m35s
Unit Tests / Compile-only (DEV=NULL:IR3:a630) (push) Failing after 30s
Unit Tests / Linux (amd gfx1201) (push) Successful in 4m10s
Unit Tests / Compile-only (DEV=NULL:NAK:sm_120) (push) Failing after 28s
Unit Tests / Compile-only (DEV=NULL:QCOMCL:a630) (push) Failing after 40s
Unit Tests / Linux (amdllvm gfx950) (push) Successful in 3m36s
Unit Tests / Linux (ptx) (push) Failing after 3m17s
Unit Tests / Linux (amd gfx950) (push) Successful in 5m25s
Unit Tests / Linux (nv) (push) Failing after 5m17s
Autogen / In-tree Autogen (macos) (push) Canceled after 0s
Benchmarks / Mac pytest (push) Canceled after 0s
Benchmarks / LLM (DEV=AMD) (push) Canceled after 0s
Benchmarks / LLM (DEV=METAL) (push) Canceled after 0s
Benchmarks / LLM (DEV=NV) (push) Canceled after 0s
Benchmarks / HLB-CIFAR10 (DEV=AMD) (push) Canceled after 0s
Benchmarks / HLB-CIFAR10 (DEV=METAL) (push) Canceled after 0s
Benchmarks / HLB-CIFAR10 (DEV=NV) (push) Canceled after 0s
Benchmarks / MLPerf (AMD) (push) Canceled after 0s
Benchmarks / MLPerf (NV) (push) Canceled after 0s
Benchmarks / Stable Diffusion (DEV=AMD) (push) Canceled after 0s
Benchmarks / Stable Diffusion (DEV=METAL) (push) Canceled after 0s
Benchmarks / Stable Diffusion (DEV=NV) (push) Canceled after 0s
Benchmarks / Tests (DEV=AMD) (push) Canceled after 0s
Benchmarks / Tests (DEV=METAL) (push) Canceled after 0s
Benchmarks / Tests (DEV=NV) (push) Canceled after 0s
Benchmarks / UsbGPU Benchmark (push) Canceled after 0s
Benchmarks / comma Benchmark (0.11.2) (push) Canceled after 0s
Benchmarks / comma Benchmark (0.11.0) (push) Canceled after 0s
Benchmarks / DSP Benchmark (push) Canceled after 0s
Benchmarks / UsbGPU Benchmark (comma) (push) Canceled after 0s
Benchmarks / PCI Driver Benchmark (DEV=AMD) (push) Canceled after 0s
Benchmarks / PCI Driver Benchmark (DEV=NV) (push) Canceled after 0s
Benchmarks / LLVM Speed (push) Canceled after 0s
Platform Tests / MacOS (unit) (push) Canceled after 0s
Platform Tests / MacOS (unit, mock) (push) Canceled after 0s
Platform Tests / MacOS (DEV=METAL) (1) (push) Canceled after 0s
Platform Tests / MacOS (DEV=METAL) (2) (push) Canceled after 0s
Platform Tests / MacOS (DEV=CPU:CLANG) (push) Canceled after 0s
Platform Tests / MacOS (DEV=CPU:LLVM) (push) Canceled after 0s
Platform Tests / MacOS (DEV=CPU:LVP) (push) Canceled after 0s
Platform Tests / MacOS (DEV=WEBGPU) (push) Canceled after 0s
Platform Tests / Windows (DEV=CPU:CLANG) (push) Canceled after 0s
Platform Tests / Windows (DEV=CPU:LLVM) (push) Canceled after 0s
Platform Tests / Windows (DEV=CPU:X86) (push) Canceled after 0s
Platform Tests / Windows (DEV=WEBGPU) (push) Canceled after 0s
* more lil llm improvements * default float
216 lines
8.7 KiB
Python
216 lines
8.7 KiB
Python
import unittest
|
|
from unittest.mock import patch
|
|
from tinygrad import Tensor, UOp
|
|
from tinygrad.schedule import schedule_cache
|
|
from tinygrad.llm.model import Transformer, TransformerConfig
|
|
from tinygrad.llm.serve import StreamRouter
|
|
|
|
TEST_CONFIG = TransformerConfig(num_blocks=1, dim=64, hidden_dim=128, n_heads=2, n_kv_heads=2,
|
|
norm_eps=1e-5, vocab_size=100, head_dim=32, rope_theta=10000.0, rope_dim=32, v_head_dim=32, max_context=32)
|
|
V_START_POS = UOp.variable("start_pos", 0, TEST_CONFIG.max_context-1)
|
|
V_TOKS = UOp.variable("toks", 1, 32) # 32 is the default chunk_size in generate
|
|
|
|
class TestTransformerGenerate(unittest.TestCase):
|
|
def test_warmup(self):
|
|
model, calls = Transformer(TEST_CONFIG), []
|
|
def generate(tokens, **kwargs):
|
|
calls.append(tokens)
|
|
yield from (1, 2)
|
|
with patch.object(model, "generate", generate): model.warmup()
|
|
self.assertEqual(calls, [[0], [0]])
|
|
|
|
def test_warmup_then_generate_with_default_chunk(self):
|
|
# warmup must not capture JIT graphs that generate()'s default chunk_size then rejects
|
|
model = Transformer(TEST_CONFIG)
|
|
model.warmup()
|
|
self.assertIsInstance(next(model.generate([5, 6, 7, 8])), int)
|
|
|
|
def test_first_recurrent_generate_before_state_init(self):
|
|
model = Transformer(TEST_CONFIG)
|
|
model.has_recurrent_block = True
|
|
with patch.object(Transformer, '__call__', return_value=Tensor([[42]])):
|
|
self.assertEqual(next(model.generate([0])), 42)
|
|
|
|
def test_recurrent_live_state_reuse(self):
|
|
model = Transformer(TEST_CONFIG)
|
|
model.has_recurrent_block = True
|
|
model._cached_tokens = [1, 2, 3, 4, 5]
|
|
self.assertEqual(model.get_start_pos([1, 2, 3, 4, 5, 42, 10]), 5)
|
|
calls = []
|
|
def mock_call(self, tokens, start_pos, temperature, **kwargs):
|
|
calls.append((tokens.shape, start_pos))
|
|
return Tensor([[42]])
|
|
with patch.object(Transformer, '__call__', mock_call):
|
|
next(model.generate([1, 2, 3, 4, 5, 42, 10]))
|
|
self.assertEqual(calls, [((1, 1), V_START_POS.bind(5)), ((1, 1), V_START_POS.bind(6))])
|
|
|
|
def test_recurrent_divergent_prompt_restarts(self):
|
|
model, calls = Transformer(TEST_CONFIG), []
|
|
model.has_recurrent_block, model._cached_tokens = True, [1, 2, 9]
|
|
def mock_call(self, tokens, start_pos, temperature):
|
|
calls.append(start_pos)
|
|
return Tensor([[42]])
|
|
with patch.object(Transformer, '__call__', mock_call): next(model.generate([1, 2, 10, 11]))
|
|
self.assertEqual(calls[0], V_START_POS.bind(0))
|
|
|
|
def test_template_starts_reasoning(self):
|
|
router = StreamRouter(reasoning=True)
|
|
self.assertEqual(list(router.route("reasoning</think>answer")),
|
|
[("reasoning_content", "reasoning"), ("content", "answer")])
|
|
|
|
def test_kv_cache_reuse(self):
|
|
"""Test that generate reuses the KV cache when tokens extend the cached prefix."""
|
|
model = Transformer(TEST_CONFIG)
|
|
|
|
captured_inputs = []
|
|
def mock_call(self, tokens, start_pos, temperature, **kwargs):
|
|
captured_inputs.append((tokens.shape, start_pos))
|
|
return Tensor([[42]])
|
|
|
|
with patch.object(Transformer, '__call__', mock_call):
|
|
# first conversation: prefill 5 tokens + 1 decode
|
|
tokens = [1, 2, 3, 4, 5]
|
|
gen = model.generate(tokens)
|
|
next(gen) # prefill
|
|
next(gen) # decode
|
|
|
|
# second call extends the conversation — cached prefix should be reused
|
|
captured_inputs.clear()
|
|
tokens = [1, 2, 3, 4, 5, 42, 42, 10, 11, 12]
|
|
gen = model.generate(tokens)
|
|
next(gen)
|
|
|
|
# should process tokens[6:] = [42, 10, 11, 12] since first 6 have cached k/v
|
|
self.assertEqual(captured_inputs, [((1, V_TOKS.bind(4)), V_START_POS.bind(6))])
|
|
|
|
def test_kv_cache_invalidation(self):
|
|
"""Test that generate invalidates the KV cache when tokens diverge from the cached prefix."""
|
|
model = Transformer(TEST_CONFIG)
|
|
|
|
captured_inputs = []
|
|
def mock_call(self, tokens, start_pos, temperature, **kwargs):
|
|
captured_inputs.append((tokens.shape, start_pos))
|
|
return Tensor([[42]])
|
|
|
|
with patch.object(Transformer, '__call__', mock_call):
|
|
# first conversation
|
|
gen = model.generate([1, 2, 3, 4, 5])
|
|
next(gen)
|
|
|
|
# completely different prompt — KV cache should be invalidated
|
|
captured_inputs.clear()
|
|
gen = model.generate([10, 20, 30])
|
|
next(gen)
|
|
|
|
# should process all 3 tokens from start
|
|
self.assertEqual(captured_inputs, [((1, V_TOKS.bind(3)), V_START_POS.bind(0))])
|
|
|
|
def test_two_prompts_schedule_cache(self):
|
|
"""Third prompt should hit the schedule cache, not miss (first two warm up both jits: prefill + decode)."""
|
|
from dataclasses import replace
|
|
model = Transformer(replace(TEST_CONFIG, max_context=64))
|
|
|
|
# first two prompts warm up both jits (prefill + decode)
|
|
ids = list(range(1, 6))
|
|
gen = model.generate(ids)
|
|
for _ in range(3): next(gen)
|
|
|
|
ids += list(range(10, 15))
|
|
gen = model.generate(ids)
|
|
for _ in range(3): next(gen)
|
|
cache_size_after_warmup = len(schedule_cache)
|
|
|
|
# third prompt should reuse the same schedule cache entries, not create new ones
|
|
ids += list(range(20, 25))
|
|
gen = model.generate(ids)
|
|
for _ in range(3): next(gen)
|
|
|
|
self.assertEqual(cache_size_after_warmup, len(schedule_cache),
|
|
f"third prompt added {len(schedule_cache) - cache_size_after_warmup} new schedule cache entries (expected 0)")
|
|
|
|
def test_chunked_prefill(self):
|
|
"""When prompt > chunk_size, all chunks should be prefill"""
|
|
from tinygrad.uop.ops import resolve
|
|
from dataclasses import replace
|
|
model = Transformer(replace(TEST_CONFIG, max_context=64))
|
|
|
|
def get_prefill_flags(tokens, chunk_size):
|
|
is_prefill = []
|
|
def mock_call(self, tokens, start_pos, temperature, **kwargs):
|
|
is_prefill.append(resolve(tokens.shape[1] != 1))
|
|
return Tensor([[42]])
|
|
with patch.object(Transformer, '__call__', mock_call):
|
|
gen = model.generate(tokens, chunk_size=chunk_size)
|
|
for _ in range(3): next(gen)
|
|
model._cached_tokens = []
|
|
return is_prefill
|
|
|
|
# 8 tokens, chunk_size=4 -> 2 prefill chunks
|
|
self.assertEqual(get_prefill_flags(list(range(8)), 4), [True, True, False, False])
|
|
# 9 tokens, chunk_size=4 -> 3 prefill chunks (4+4+1)
|
|
self.assertEqual(get_prefill_flags(list(range(9)), 4), [True, True, True, False, False])
|
|
# 4 tokens, chunk_size=4 -> 1 prefill chunk
|
|
self.assertEqual(get_prefill_flags(list(range(4)), 4), [True, False, False])
|
|
|
|
def test_kv_cache_resume_matches_fresh(self):
|
|
model = Transformer(TEST_CONFIG)
|
|
|
|
# generate 2 tokens, then abandon
|
|
prompt = list(range(1, 6))
|
|
gen = model.generate(list(prompt))
|
|
out1, out2 = next(gen), next(gen)
|
|
|
|
# resume with conversation history + new user tokens appended
|
|
extended = prompt + [out1, out2, 10, 11, 12]
|
|
gen = model.generate(list(extended))
|
|
resumed_out = [next(gen) for _ in range(3)]
|
|
|
|
# compare against fresh generation (no cache) of the same prompt
|
|
model._cached_tokens = []
|
|
gen = model.generate(list(extended))
|
|
fresh_out = [next(gen) for _ in range(3)]
|
|
|
|
self.assertEqual(fresh_out, resumed_out)
|
|
|
|
def test_temperature_zero_is_greedy(self):
|
|
"""Temperature 0 (or near 0) should produce deterministic output."""
|
|
model = Transformer(TEST_CONFIG)
|
|
tokens = list(range(1, 6))
|
|
results = [list(zip(range(5), model.generate(list(tokens)))) for _ in range(3)]
|
|
# all runs should produce the same tokens
|
|
self.assertEqual(results[0], results[1])
|
|
self.assertEqual(results[1], results[2])
|
|
|
|
def test_temperature_high_produces_variety(self):
|
|
"""High temperature should produce different outputs across runs."""
|
|
model = Transformer(TEST_CONFIG)
|
|
tokens = list(range(1, 6))
|
|
runs = set()
|
|
for _ in range(5):
|
|
gen = model.generate(list(tokens), temperature=2.0)
|
|
out = tuple(next(gen) for _ in range(10))
|
|
runs.add(out)
|
|
# with temperature=2.0, we should see at least 2 distinct outputs across 5 runs
|
|
self.assertGreater(len(runs), 1, "high temperature should produce varied outputs")
|
|
|
|
def test_recurrent_temperature_high_produces_variety(self):
|
|
model = Transformer(TEST_CONFIG)
|
|
model.has_recurrent_block = True
|
|
outputs = {model.forward(Tensor([[1]]), 0, Tensor([2.0])).item() for _ in range(5)}
|
|
self.assertGreater(len(outputs), 1)
|
|
|
|
def test_temperature_passed_to_forward(self):
|
|
"""Temperature from generate should be passed through to __call__."""
|
|
model = Transformer(TEST_CONFIG)
|
|
captured_temps = []
|
|
def mock_call(self, tokens, start_pos, temperature, **kwargs):
|
|
captured_temps.append(float(temperature.item()))
|
|
return Tensor([[42]])
|
|
with patch.object(Transformer, '__call__', mock_call):
|
|
gen = model.generate([1, 2, 3], temperature=0.6)
|
|
next(gen)
|
|
self.assertAlmostEqual(captured_temps[-1], 0.6, places=5)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|