Kye Gomez commited on
Commit
0699c00
·
1 Parent(s): c258cdc
Files changed (4) hide show
  1. example.py +3 -1
  2. open_mythos/main.py +0 -1
  3. test_main.py +548 -0
  4. tests/__init__.py +0 -0
example.py CHANGED
@@ -45,4 +45,6 @@ out = model.generate(ids, max_new_tokens=8, n_loops=8)
45
  print(f"[{attn_type.upper()}] Generated shape: {out.shape}")
46
 
47
  A = model.recurrent.injection.get_A()
48
- print(f"[{attn_type.upper()}] Spectral radius ρ(A) max: {A.max().item():.4f} (must be < 1)")
 
 
 
45
  print(f"[{attn_type.upper()}] Generated shape: {out.shape}")
46
 
47
  A = model.recurrent.injection.get_A()
48
+ print(
49
+ f"[{attn_type.upper()}] Spectral radius ρ(A) max: {A.max().item():.4f} (must be < 1)"
50
+ )
open_mythos/main.py CHANGED
@@ -1012,4 +1012,3 @@ class OpenMythos(nn.Module):
1012
  next_tok = torch.multinomial(probs, num_samples=1)
1013
  input_ids = torch.cat([input_ids, next_tok], dim=1)
1014
  return input_ids
1015
-
 
1012
  next_tok = torch.multinomial(probs, num_samples=1)
1013
  input_ids = torch.cat([input_ids, next_tok], dim=1)
1014
  return input_ids
 
test_main.py ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ pytest suite for OpenMythos (second.py).
3
+ Tests every major class and feature: shapes, correctness invariants, and
4
+ architecture-specific properties (LTI stability, ACT halting, depth extrapolation,
5
+ KV cache consistency, GQA vs MLA swap).
6
+ """
7
+
8
+ import torch
9
+ import pytest
10
+ from open_mythos.main import (
11
+ ACTHalting,
12
+ Expert,
13
+ GQAttention,
14
+ LTIInjection,
15
+ LoRAAdapter,
16
+ MLAttention,
17
+ MoEFFN,
18
+ MythosConfig,
19
+ OpenMythos,
20
+ RecurrentBlock,
21
+ RMSNorm,
22
+ TransformerBlock,
23
+ apply_rope,
24
+ loop_index_embedding,
25
+ precompute_rope_freqs,
26
+ )
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Shared small configs (kept tiny so tests run fast on CPU)
30
+ # ---------------------------------------------------------------------------
31
+
32
+ B, T = 2, 8 # batch, sequence length
33
+
34
+
35
+ def gqa_cfg(**overrides) -> MythosConfig:
36
+ defaults = dict(
37
+ vocab_size=200,
38
+ dim=64,
39
+ n_heads=4,
40
+ n_kv_heads=2,
41
+ max_seq_len=32,
42
+ max_loop_iters=3,
43
+ prelude_layers=1,
44
+ coda_layers=1,
45
+ attn_type="gqa",
46
+ n_experts=4,
47
+ n_shared_experts=1,
48
+ n_experts_per_tok=2,
49
+ expert_dim=16,
50
+ act_threshold=0.99,
51
+ lora_rank=4,
52
+ # MLA fields must be valid even when not used
53
+ kv_lora_rank=16,
54
+ q_lora_rank=32,
55
+ qk_rope_head_dim=8,
56
+ qk_nope_head_dim=8,
57
+ v_head_dim=8,
58
+ )
59
+ defaults.update(overrides)
60
+ return MythosConfig(**defaults)
61
+
62
+
63
+ def mla_cfg(**overrides) -> MythosConfig:
64
+ return gqa_cfg(attn_type="mla", **overrides)
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # RMSNorm
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ class TestRMSNorm:
73
+ def test_output_shape(self):
74
+ norm = RMSNorm(64)
75
+ x = torch.randn(2, 8, 64)
76
+ assert norm(x).shape == x.shape
77
+
78
+ def test_unit_rms(self):
79
+ # after norm the RMS of each vector should be ≈ 1 when weight=1
80
+ norm = RMSNorm(64)
81
+ torch.nn.init.ones_(norm.weight)
82
+ x = torch.randn(4, 64)
83
+ out = norm(x)
84
+ rms = out.pow(2).mean(-1).sqrt()
85
+ assert torch.allclose(rms, torch.ones_like(rms), atol=1e-4)
86
+
87
+ def test_learnable_weight(self):
88
+ norm = RMSNorm(8)
89
+ assert norm.weight.requires_grad
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # RoPE utilities
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ class TestRoPE:
98
+ def test_precompute_shape(self):
99
+ freqs = precompute_rope_freqs(dim=16, max_len=32)
100
+ assert freqs.shape == (32, 8) # (max_len, dim//2)
101
+ assert freqs.is_complex()
102
+
103
+ def test_apply_rope_shape(self):
104
+ freqs = precompute_rope_freqs(dim=16, max_len=32)
105
+ x = torch.randn(B, T, 4, 16)
106
+ out = apply_rope(x, freqs)
107
+ assert out.shape == x.shape
108
+
109
+ def test_apply_rope_preserves_norm(self):
110
+ # rotation is an isometry — norms must be unchanged
111
+ freqs = precompute_rope_freqs(dim=16, max_len=32)
112
+ x = torch.randn(B, T, 4, 16)
113
+ out = apply_rope(x, freqs)
114
+ assert torch.allclose(x.norm(dim=-1), out.norm(dim=-1), atol=1e-5)
115
+
116
+ def test_different_positions_differ(self):
117
+ freqs = precompute_rope_freqs(dim=16, max_len=32)
118
+ x = torch.ones(1, 2, 1, 16)
119
+ out = apply_rope(x, freqs)
120
+ # position 0 and position 1 should produce different rotations
121
+ assert not torch.allclose(out[0, 0], out[0, 1])
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # GQAttention
126
+ # ---------------------------------------------------------------------------
127
+
128
+
129
+ class TestGQAttention:
130
+ def setup_method(self):
131
+ self.cfg = gqa_cfg()
132
+ self.freqs = precompute_rope_freqs(
133
+ self.cfg.dim // self.cfg.n_heads, self.cfg.max_seq_len
134
+ )
135
+ self.attn = GQAttention(self.cfg)
136
+
137
+ def test_output_shape(self):
138
+ x = torch.randn(B, T, self.cfg.dim)
139
+ out = self.attn(x, self.freqs)
140
+ assert out.shape == (B, T, self.cfg.dim)
141
+
142
+ def test_kv_cache_accumulates(self):
143
+ cache = {}
144
+ x = torch.randn(B, T, self.cfg.dim)
145
+ self.attn(x, self.freqs, kv_cache=cache, cache_key="layer0")
146
+ assert "layer0" in cache
147
+ k_len = cache["layer0"]["k"].shape[1]
148
+ # second call adds T more tokens
149
+ self.attn(x, self.freqs, kv_cache=cache, cache_key="layer0")
150
+ assert cache["layer0"]["k"].shape[1] == k_len + T
151
+
152
+ def test_with_causal_mask(self):
153
+ x = torch.randn(B, T, self.cfg.dim)
154
+ mask = torch.full((1, 1, T, T), float("-inf"))
155
+ mask = torch.triu(mask, diagonal=1)
156
+ out = self.attn(x, self.freqs, mask=mask)
157
+ assert out.shape == (B, T, self.cfg.dim)
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # MLAttention
162
+ # ---------------------------------------------------------------------------
163
+
164
+
165
+ class TestMLAttention:
166
+ def setup_method(self):
167
+ self.cfg = mla_cfg()
168
+ self.freqs = precompute_rope_freqs(
169
+ self.cfg.qk_rope_head_dim, self.cfg.max_seq_len
170
+ )
171
+ self.attn = MLAttention(self.cfg)
172
+
173
+ def test_output_shape(self):
174
+ x = torch.randn(B, T, self.cfg.dim)
175
+ out = self.attn(x, self.freqs)
176
+ assert out.shape == (B, T, self.cfg.dim)
177
+
178
+ def test_cache_stores_compressed_kv(self):
179
+ cache = {}
180
+ x = torch.randn(B, T, self.cfg.dim)
181
+ self.attn(x, self.freqs, kv_cache=cache, cache_key="mla0")
182
+ assert "c_kv" in cache["mla0"]
183
+ assert "k_rope" in cache["mla0"]
184
+ # c_kv should have kv_lora_rank as last dim, not full K/V
185
+ assert cache["mla0"]["c_kv"].shape[-1] == self.cfg.kv_lora_rank
186
+
187
+ def test_cache_accumulates_across_steps(self):
188
+ cache = {}
189
+ x = torch.randn(B, T, self.cfg.dim)
190
+ self.attn(x, self.freqs, kv_cache=cache, cache_key="mla0")
191
+ first_len = cache["mla0"]["c_kv"].shape[1]
192
+ self.attn(x, self.freqs, kv_cache=cache, cache_key="mla0")
193
+ assert cache["mla0"]["c_kv"].shape[1] == first_len + T
194
+
195
+ def test_with_causal_mask(self):
196
+ x = torch.randn(B, T, self.cfg.dim)
197
+ mask = torch.triu(torch.full((1, 1, T, T), float("-inf")), diagonal=1)
198
+ out = self.attn(x, self.freqs, mask=mask)
199
+ assert out.shape == (B, T, self.cfg.dim)
200
+
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # Expert (dense SwiGLU FFN)
204
+ # ---------------------------------------------------------------------------
205
+
206
+
207
+ class TestExpert:
208
+ def test_output_shape(self):
209
+ expert = Expert(dim=64, expert_dim=32)
210
+ x = torch.randn(B, T, 64)
211
+ assert expert(x).shape == (B, T, 64)
212
+
213
+ def test_flat_input(self):
214
+ expert = Expert(dim=32, expert_dim=16)
215
+ x = torch.randn(5, 32)
216
+ assert expert(x).shape == (5, 32)
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # MoEFFN
221
+ # ---------------------------------------------------------------------------
222
+
223
+
224
+ class TestMoEFFN:
225
+ def setup_method(self):
226
+ self.cfg = gqa_cfg()
227
+ self.moe = MoEFFN(self.cfg)
228
+
229
+ def test_output_shape(self):
230
+ x = torch.randn(B, T, self.cfg.dim)
231
+ assert self.moe(x).shape == (B, T, self.cfg.dim)
232
+
233
+ def test_router_bias_not_grad(self):
234
+ # router_bias is a buffer, not a parameter
235
+ param_names = {n for n, _ in self.moe.named_parameters()}
236
+ assert "router_bias" not in param_names
237
+
238
+ def test_shared_experts_always_fire(self):
239
+ # Zero out all routed experts; output should still be nonzero from shared
240
+ for exp in self.moe.routed_experts:
241
+ for p in exp.parameters():
242
+ p.data.zero_()
243
+ x = torch.randn(B, T, self.cfg.dim)
244
+ out = self.moe(x)
245
+ assert out.abs().sum() > 0
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # loop_index_embedding
250
+ # ---------------------------------------------------------------------------
251
+
252
+
253
+ class TestLoopIndexEmbedding:
254
+ def test_output_shape(self):
255
+ h = torch.randn(B, T, 64)
256
+ out = loop_index_embedding(h, loop_t=0, loop_dim=8)
257
+ assert out.shape == h.shape
258
+
259
+ def test_different_iterations_differ(self):
260
+ h = torch.zeros(1, 1, 64)
261
+ out0 = loop_index_embedding(h, loop_t=0, loop_dim=8)
262
+ out1 = loop_index_embedding(h, loop_t=1, loop_dim=8)
263
+ assert not torch.allclose(out0, out1)
264
+
265
+ def test_only_first_dims_modified(self):
266
+ h = torch.zeros(1, 1, 64)
267
+ loop_dim = 8
268
+ out = loop_index_embedding(h, loop_t=3, loop_dim=loop_dim)
269
+ # channels beyond loop_dim should be unchanged (still 0)
270
+ assert torch.all(out[..., loop_dim:] == 0)
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # LoRAAdapter
275
+ # ---------------------------------------------------------------------------
276
+
277
+
278
+ class TestLoRAAdapter:
279
+ def setup_method(self):
280
+ self.lora = LoRAAdapter(dim=64, rank=8, max_loops=10)
281
+
282
+ def test_output_shape(self):
283
+ x = torch.randn(B, T, 64)
284
+ out = self.lora(x, loop_t=0)
285
+ assert out.shape == (B, T, 64)
286
+
287
+ def test_different_loops_differ(self):
288
+ x = torch.randn(B, T, 64)
289
+ out0 = self.lora(x, loop_t=0)
290
+ out1 = self.lora(x, loop_t=1)
291
+ assert not torch.allclose(out0, out1)
292
+
293
+
294
+ # ---------------------------------------------------------------------------
295
+ # TransformerBlock
296
+ # ---------------------------------------------------------------------------
297
+
298
+
299
+ class TestTransformerBlock:
300
+ def test_gqa_output_shape(self):
301
+ cfg = gqa_cfg()
302
+ block = TransformerBlock(cfg, use_moe=False)
303
+ freqs = precompute_rope_freqs(cfg.dim // cfg.n_heads, cfg.max_seq_len)
304
+ x = torch.randn(B, T, cfg.dim)
305
+ assert block(x, freqs).shape == (B, T, cfg.dim)
306
+
307
+ def test_mla_output_shape(self):
308
+ cfg = mla_cfg()
309
+ block = TransformerBlock(cfg, use_moe=False)
310
+ freqs = precompute_rope_freqs(cfg.qk_rope_head_dim, cfg.max_seq_len)
311
+ x = torch.randn(B, T, cfg.dim)
312
+ assert block(x, freqs).shape == (B, T, cfg.dim)
313
+
314
+ def test_moe_block_output_shape(self):
315
+ cfg = gqa_cfg()
316
+ block = TransformerBlock(cfg, use_moe=True)
317
+ freqs = precompute_rope_freqs(cfg.dim // cfg.n_heads, cfg.max_seq_len)
318
+ x = torch.randn(B, T, cfg.dim)
319
+ assert block(x, freqs).shape == (B, T, cfg.dim)
320
+
321
+ def test_attn_type_selection(self):
322
+ assert isinstance(TransformerBlock(gqa_cfg()).attn, GQAttention)
323
+ assert isinstance(TransformerBlock(mla_cfg()).attn, MLAttention)
324
+
325
+
326
+ # ---------------------------------------------------------------------------
327
+ # LTIInjection
328
+ # ---------------------------------------------------------------------------
329
+
330
+
331
+ class TestLTIInjection:
332
+ def setup_method(self):
333
+ self.inj = LTIInjection(dim=64)
334
+
335
+ def test_output_shape(self):
336
+ h = torch.randn(B, T, 64)
337
+ e = torch.randn(B, T, 64)
338
+ t = torch.randn(B, T, 64)
339
+ assert self.inj(h, e, t).shape == (B, T, 64)
340
+
341
+ def test_spectral_radius_lt_1(self):
342
+ A = self.inj.get_A()
343
+ assert A.max().item() < 1.0
344
+
345
+ def test_spectral_radius_gt_0(self):
346
+ A = self.inj.get_A()
347
+ assert A.min().item() > 0.0
348
+
349
+ def test_spectral_radius_stable_after_large_grad_step(self):
350
+ # Simulate an aggressive gradient update and verify stability holds
351
+ opt = torch.optim.SGD(self.inj.parameters(), lr=1e3)
352
+ h = torch.randn(B, T, 64)
353
+ e = torch.randn(B, T, 64)
354
+ t = torch.randn(B, T, 64)
355
+ loss = self.inj(h, e, t).sum()
356
+ loss.backward()
357
+ opt.step()
358
+ A = self.inj.get_A()
359
+ assert A.max().item() < 1.0
360
+
361
+
362
+ # ---------------------------------------------------------------------------
363
+ # ACTHalting
364
+ # ---------------------------------------------------------------------------
365
+
366
+
367
+ class TestACTHalting:
368
+ def setup_method(self):
369
+ self.act = ACTHalting(dim=64)
370
+
371
+ def test_output_shape(self):
372
+ h = torch.randn(B, T, 64)
373
+ p = self.act(h)
374
+ assert p.shape == (B, T)
375
+
376
+ def test_values_in_01(self):
377
+ h = torch.randn(B, T, 64)
378
+ p = self.act(h)
379
+ assert p.min().item() >= 0.0
380
+ assert p.max().item() <= 1.0
381
+
382
+
383
+ # ---------------------------------------------------------------------------
384
+ # RecurrentBlock
385
+ # ---------------------------------------------------------------------------
386
+
387
+
388
+ class TestRecurrentBlock:
389
+ def setup_method(self):
390
+ self.cfg = gqa_cfg()
391
+ self.block = RecurrentBlock(self.cfg)
392
+ self.freqs = precompute_rope_freqs(
393
+ self.cfg.dim // self.cfg.n_heads, self.cfg.max_seq_len
394
+ )
395
+
396
+ def test_output_shape(self):
397
+ h = torch.randn(B, T, self.cfg.dim)
398
+ e = torch.randn(B, T, self.cfg.dim)
399
+ out = self.block(h, e, self.freqs)
400
+ assert out.shape == (B, T, self.cfg.dim)
401
+
402
+ def test_more_loops_changes_output(self):
403
+ h = torch.randn(B, T, self.cfg.dim)
404
+ e = torch.randn(B, T, self.cfg.dim)
405
+ out1 = self.block(h.clone(), e.clone(), self.freqs, n_loops=1)
406
+ out3 = self.block(h.clone(), e.clone(), self.freqs, n_loops=3)
407
+ assert not torch.allclose(out1, out3)
408
+
409
+ def test_single_loop_runs(self):
410
+ h = torch.randn(B, T, self.cfg.dim)
411
+ e = torch.randn(B, T, self.cfg.dim)
412
+ out = self.block(h, e, self.freqs, n_loops=1)
413
+ assert out.shape == (B, T, self.cfg.dim)
414
+
415
+
416
+ # ---------------------------------------------------------------------------
417
+ # OpenMythos — GQA mode
418
+ # ---------------------------------------------------------------------------
419
+
420
+
421
+ class TestOpenMythosGQA:
422
+ def setup_method(self):
423
+ self.cfg = gqa_cfg()
424
+ self.model = OpenMythos(self.cfg)
425
+ self.ids = torch.randint(0, self.cfg.vocab_size, (B, T))
426
+
427
+ def test_forward_shape(self):
428
+ logits = self.model(self.ids)
429
+ assert logits.shape == (B, T, self.cfg.vocab_size)
430
+
431
+ def test_forward_no_nan(self):
432
+ logits = self.model(self.ids)
433
+ assert not torch.isnan(logits).any()
434
+
435
+ def test_generate_shape(self):
436
+ out = self.model.generate(self.ids, max_new_tokens=4, n_loops=2)
437
+ assert out.shape == (B, T + 4)
438
+
439
+ def test_weight_tying(self):
440
+ assert self.model.head.weight is self.model.embed.weight
441
+
442
+ def test_lti_spectral_radius(self):
443
+ A = self.model.recurrent.injection.get_A()
444
+ assert A.max().item() < 1.0
445
+
446
+ def test_depth_extrapolation_changes_output(self):
447
+ # More loops at inference should produce different (ideally better) output
448
+ logits_shallow = self.model(self.ids, n_loops=1)
449
+ logits_deep = self.model(self.ids, n_loops=3)
450
+ assert not torch.allclose(logits_shallow, logits_deep)
451
+
452
+ def test_kv_cache_generate_matches_no_cache(self):
453
+ # Single-token generation with and without cache should agree
454
+ torch.manual_seed(0)
455
+ prompt = torch.randint(0, self.cfg.vocab_size, (1, T))
456
+ with torch.no_grad():
457
+ logits_no_cache = self.model(prompt, n_loops=2)[:, -1, :]
458
+ cache = {}
459
+ logits_cached = self.model(prompt, n_loops=2, kv_cache=cache)[:, -1, :]
460
+ assert torch.allclose(logits_no_cache, logits_cached, atol=1e-4)
461
+
462
+ def test_single_token_forward(self):
463
+ # Mask is None when T=1; should not crash
464
+ single = torch.randint(0, self.cfg.vocab_size, (B, 1))
465
+ logits = self.model(single)
466
+ assert logits.shape == (B, 1, self.cfg.vocab_size)
467
+
468
+
469
+ # ---------------------------------------------------------------------------
470
+ # OpenMythos — MLA mode
471
+ # ---------------------------------------------------------------------------
472
+
473
+
474
+ class TestOpenMythosMLА:
475
+ def setup_method(self):
476
+ self.cfg = mla_cfg()
477
+ self.model = OpenMythos(self.cfg)
478
+ self.ids = torch.randint(0, self.cfg.vocab_size, (B, T))
479
+
480
+ def test_forward_shape(self):
481
+ logits = self.model(self.ids)
482
+ assert logits.shape == (B, T, self.cfg.vocab_size)
483
+
484
+ def test_forward_no_nan(self):
485
+ assert not torch.isnan(self.model(self.ids)).any()
486
+
487
+ def test_generate_shape(self):
488
+ out = self.model.generate(self.ids, max_new_tokens=4, n_loops=2)
489
+ assert out.shape == (B, T + 4)
490
+
491
+ def test_lti_spectral_radius(self):
492
+ A = self.model.recurrent.injection.get_A()
493
+ assert A.max().item() < 1.0
494
+
495
+ def test_mla_cache_is_compressed(self):
496
+ # MLA cache should store c_kv (lora_rank), not full K/V (n_heads * head_dim)
497
+ cache = {}
498
+ with torch.no_grad():
499
+ self.model(self.ids, kv_cache=cache)
500
+ # find any MLA cache entry and check dimensions
501
+ mla_entries = {k: v for k, v in cache.items() if "c_kv" in v}
502
+ assert len(mla_entries) > 0
503
+ for entry in mla_entries.values():
504
+ assert entry["c_kv"].shape[-1] == self.cfg.kv_lora_rank
505
+
506
+
507
+ # ---------------------------------------------------------------------------
508
+ # GQA vs MLA: same config, different attn_type
509
+ # ---------------------------------------------------------------------------
510
+
511
+
512
+ class TestAttnTypeSwap:
513
+ def test_gqa_and_mla_produce_different_outputs(self):
514
+ cfg_gqa = gqa_cfg()
515
+ cfg_mla = mla_cfg()
516
+ ids = torch.randint(0, cfg_gqa.vocab_size, (B, T))
517
+ logits_gqa = OpenMythos(cfg_gqa)(ids)
518
+ logits_mla = OpenMythos(cfg_mla)(ids)
519
+ # different architectures, different params → outputs must differ
520
+ assert not torch.allclose(logits_gqa, logits_mla)
521
+
522
+ def test_both_modes_produce_valid_shapes(self):
523
+ ids = torch.randint(0, 200, (B, T))
524
+ for attn_type in ("gqa", "mla"):
525
+ cfg = gqa_cfg(attn_type=attn_type)
526
+ logits = OpenMythos(cfg)(ids)
527
+ assert logits.shape == (B, T, cfg.vocab_size)
528
+
529
+ def test_mla_fewer_kv_cache_bytes(self):
530
+ # MLA cache should be smaller than GQA cache for the same sequence
531
+ ids = torch.randint(0, 200, (1, T))
532
+ cache_gqa, cache_mla = {}, {}
533
+ with torch.no_grad():
534
+ OpenMythos(gqa_cfg())(ids, kv_cache=cache_gqa)
535
+ OpenMythos(mla_cfg())(ids, kv_cache=cache_mla)
536
+
537
+ def cache_bytes(cache):
538
+ return sum(
539
+ t.numel() * t.element_size()
540
+ for entry in cache.values()
541
+ for t in entry.values()
542
+ )
543
+
544
+ assert cache_bytes(cache_mla) < cache_bytes(cache_gqa)
545
+
546
+
547
+ if __name__ == "__main__":
548
+ pytest.main([__file__, "--verbose"])
tests/__init__.py ADDED
File without changes