fractus-vorax / tests /test_diagnostics.py
thefinalboss's picture
Fractus-Vorax v1.0.0 — the takeover: sealed CTE brain + ingestion organs + mechanical speech (199 tests, honest floors)
1da7ac7 verified
Raw
History Blame Contribute Delete
8.32 kB
# tests/test_diagnostics.py
"""Plan 6, tâche 2 — diagnostics.py : l'observabilité à ciel ouvert.
expert_routing_snapshot : structure valide sur un tiny CteCore (une clé
`layer_{i}` par couche, 2 experts retenus par layer, poids renormalisés
sommant à 1, ids dans [0, n_experts), triés par gate décroissante) ;
EXACTITUDE contre la formule von Mises réécrite à la main sur les phases
captées par un hook frais (κ_eff = 4.0/2.5 = 1.6 — le PIÈGE température du
doc §MoE batché — top-2 renormalisé) ; reproductibilité malgré la pollution
des états porteurs (reset au départ) ; hooks retirés après usage.
head_readout : floats Python bornés [0, 1) sur CTE (les deux têtes), None
sur FractusCore (pas de têtes confiance/salience — duck-typing honnête).
"""
import pytest
torch = pytest.importorskip(
"torch", reason="diagnostics nécessite torch (substrat sans torch)"
)
from fractus_vorax.model.cte_core import CteCore, CteCoreConfig
from fractus_vorax.model.diagnostics import expert_routing_snapshot, head_readout
def _tiny(**overrides) -> CteCoreConfig:
return CteCoreConfig(vocab_size=128, **overrides)
def _tiny_fractus():
from fractus_vorax.model.fractus_core import FractusCore, FractusCoreConfig
cfg = FractusCoreConfig(
vocab_size=128, d_model=64, n_layers=2, n_experts=4, top_k=2,
rank=16, max_seq_len=64,
)
return FractusCore(cfg)
# ---------------------------------------------------------------------------
# expert_routing_snapshot — structure
# ---------------------------------------------------------------------------
def test_snapshot_structure_tiny():
torch.manual_seed(0)
cfg = _tiny()
core = CteCore(cfg)
snap = expert_routing_snapshot(core, [10, 20, 30])
assert set(snap.keys()) == {f"layer_{i}" for i in range(cfg.n_layers)}
for entries in snap.values():
assert len(entries) == cfg.top_k # 2 experts retenus par layer
ids = [e for e, _ in entries]
weights = [w for _, w in entries]
assert all(isinstance(e, int) and 0 <= e < cfg.n_experts for e in ids)
assert all(isinstance(w, float) and w > 0.0 for w in weights)
assert abs(sum(weights) - 1.0) < 1e-5 # top-k RENORMALISÉ
# triées par gate décroissante (convention torch.topk)
assert weights == sorted(weights, reverse=True)
def test_snapshot_layer_count_follows_config():
"""Le snapshot suit le nombre de couches du noyau (tiny 2, ici 3 ; le 1B
réel en aurait 16 — une clé layer_{i} par bloc)."""
torch.manual_seed(0)
cfg = _tiny(n_layers=3)
core = CteCore(cfg)
snap = expert_routing_snapshot(core, [1, 2, 3, 4, 5])
assert len(snap) == 3
assert set(snap) == {"layer_0", "layer_1", "layer_2"}
# ---------------------------------------------------------------------------
# expert_routing_snapshot — exactitude vs la formule von Mises à la main
# ---------------------------------------------------------------------------
def test_snapshot_matches_von_mises_formula():
"""EXACTITUDE : ids EXACTS et poids ≈ identiques à la formule von Mises
réécrite à la main dans CE test (doc §MoE batché) sur les phases captées
au hook — κ_eff = 4.0/2.5 = 1.6 hardcodé ici : le test attraperait un
noyau qui aurait oublié le piège température."""
torch.manual_seed(0)
cfg = _tiny()
core = CteCore(cfg)
ids = torch.tensor([[10, 20, 30]])
snap = expert_routing_snapshot(core, ids)
captured = [] # (moe, phases réellement consommées par la passe MoE)
def hook(module, args, output):
captured.append((module, args[1].detach().clone()))
handles = [blk.moe.register_forward_hook(hook) for blk in core.blocks]
try:
core.reset_states(1)
core(ids) # même forward, mêmes états initiaux => mêmes phases
finally:
for h in handles:
h.remove()
assert len(captured) == cfg.n_layers
kappa_eff = 4.0 / 2.5 # doc §MoE batché : κ=4.0, temperature=2.5
for i, (moe, phases) in enumerate(captured):
theta_bar = torch.atan2(
torch.sin(phases).sum(dim=-1), torch.cos(phases).sum(dim=-1)
)[0, -1]
gates = torch.exp(kappa_eff * torch.cos(theta_bar - moe.expert_phases))
gates = gates / gates.sum()
vals, idx = gates.topk(cfg.top_k)
expected = [
(int(e), float(w))
for e, w in zip(idx.tolist(), (vals / vals.sum()).tolist())
]
got = snap[f"layer_{i}"]
assert [e for e, _ in got] == [e for e, _ in expected]
assert all(abs(a - b) < 1e-5 for (_, a), (_, b) in zip(got, expected))
# ---------------------------------------------------------------------------
# expert_routing_snapshot — robustesse, hygiène, duck-typing
# ---------------------------------------------------------------------------
def test_snapshot_deterministic_across_state_pollution():
"""Le reset au départ rend la lecture reproductible même après que le
noyau ait avalé d'autres chunks (états porteurs mutés par le forward)."""
torch.manual_seed(0)
core = CteCore(_tiny())
ids = [7, 8, 9]
first = expert_routing_snapshot(core, ids)
core(torch.tensor([[40, 50, 60, 61]])) # pollue attn_S/attn_z/thought_state
second = expert_routing_snapshot(core, ids)
assert first == second
def test_snapshot_list_and_tensor_equivalent():
"""Ergonomie : list[int] et tensor (accepté (1, L) et (L,)) disent la
même chose sur le même noyau."""
torch.manual_seed(0)
core = CteCore(_tiny())
a = expert_routing_snapshot(core, [10, 20, 30])
b = expert_routing_snapshot(core, torch.tensor([10, 20, 30]))
c = expert_routing_snapshot(core, torch.tensor([[10, 20, 30]]))
assert a == b == c
def test_snapshot_rejects_empty_ids():
torch.manual_seed(0)
core = CteCore(_tiny())
with pytest.raises(ValueError):
expert_routing_snapshot(core, [])
def test_hooks_removed_and_core_still_usable():
"""Hygiène : les hooks partent après l'appel, le noyau fonctionne
exactement comme avant (aucune trace du passage du diagnosticien)."""
torch.manual_seed(0)
core = CteCore(_tiny())
expert_routing_snapshot(core, [1, 2])
head_readout(core, [1, 2])
for blk in core.blocks:
assert not blk.moe._forward_hooks
assert not core.output_head._forward_hooks
logits = core(torch.tensor([[3, 4]]))
assert tuple(logits.shape) == (1, 2, 128)
def test_snapshot_duck_types_fractus_core():
"""Le routage est lisible sur les DEUX noyaux (mêmes conventions moe :
forward(h, phases) + _compute_gates) — FractusCore κ=4.0 direct, phases
par position (on lit la dernière)."""
torch.manual_seed(0)
core = _tiny_fractus()
snap = expert_routing_snapshot(core, [10, 20, 30])
assert set(snap) == {"layer_0", "layer_1"}
for entries in snap.values():
assert len(entries) == 2
weights = [w for _, w in entries]
assert abs(sum(weights) - 1.0) < 1e-5
# ---------------------------------------------------------------------------
# head_readout — têtes confiance/salience du CTE
# ---------------------------------------------------------------------------
def test_head_readout_cte_floats_bounded():
"""CTE : les deux têtes se lisent — floats Python dans [0, 1) (sigmoid),
reproductibles malgré la pollution des états porteurs."""
torch.manual_seed(0)
core = CteCore(_tiny())
out = head_readout(core, [10, 20, 30])
assert set(out) == {"confidence", "salience"}
assert isinstance(out["confidence"], float)
assert isinstance(out["salience"], float)
assert 0.0 <= out["confidence"] < 1.0 # borné (le plan : « head_readout borné »)
assert 0.0 <= out["salience"] < 1.0
core(torch.tensor([[90, 91]])) # pollue les états porteurs
again = head_readout(core, [10, 20, 30])
assert out == again
def test_head_readout_fractus_none():
"""FractusCore n'a PAS de têtes confiance/salience (lm_head seule) :
lecture honnête → les deux à None, sans lancer le moindre forward."""
torch.manual_seed(0)
core = _tiny_fractus()
assert head_readout(core, [1, 2, 3]) == {"confidence": None, "salience": None}