"""Tests for tensor free: del-based + context manager cleanup. Validates: TF1. PEMemAllocator.free_hbm/free_tcm reclaims space TF2. del tensor triggers cleanup (VA/PA returned, MMU unmapped) TF3. Context manager cleans up all tensors on exit TF4. del after context exit is safe (no crash) TF5. Alloc-del-alloc cycle reuses VA and PA TF6. del already-freed tensor is safe (no crash) """ import gc import pytest from pathlib import Path from kernbench.policy.address.allocator import AddressConfig, PEMemAllocator from kernbench.policy.address.pe_mmu import PageFault from kernbench.policy.placement.dp import DPPolicy from kernbench.sim_engine.engine import GraphEngine from kernbench.runtime_api.context import RuntimeContext from kernbench.runtime_api.types import DeviceSelector from kernbench.topology.builder import load_topology TOPOLOGY_PATH = Path(__file__).parent.parent / "topology.yaml" _MB = 1 << 20 _GB = 1 << 30 _CFG = AddressConfig( sip_count=2, cubes_per_sip=16, pes_per_cube=8, hbm_bytes_per_cube=48 * _GB, hbm_slices_per_cube=8, tcm_bytes_per_pe=16 * _MB, tcm_scheduler_reserved_bytes=4 * _MB, sram_bytes_per_cube=32 * _MB, ) def _make_ctx(): graph = load_topology(TOPOLOGY_PATH) engine = GraphEngine(graph) ctx = RuntimeContext( engine=engine, target_device=DeviceSelector("sip:0"), correlation_id="test_free", spec=graph.spec, ) return ctx, engine # ── TF1. PEMemAllocator free ───────────────────────────────────────── def test_allocator_free_hbm_reclaims_space(): """free_hbm returns HBM space; subsequent alloc can reuse it.""" a = PEMemAllocator(sip_id=0, die_id=0, pe_id=0, cfg=_CFG) pa1 = a.alloc_hbm(4096) used_after_alloc = a.hbm_used a.free_hbm(pa1, 4096) assert a.hbm_used == used_after_alloc - 4096 pa2 = a.alloc_hbm(4096) assert pa2 is not None def test_allocator_free_tcm_reclaims_space(): """free_tcm returns TCM space.""" a = PEMemAllocator(sip_id=0, die_id=0, pe_id=0, cfg=_CFG) pa1 = a.alloc_tcm(256) used_after_alloc = a.tcm_used a.free_tcm(pa1, 256) assert a.tcm_used == used_after_alloc - 256 # ── TF2. del tensor triggers cleanup ───────────────────────────────── def test_del_tensor_unmaps_mmu(): """del tensor removes MMU mappings.""" ctx, engine = _make_ctx() dp = DPPolicy(cube="replicate", pe="replicate") t = ctx.zeros((128, 128), dtype="f16", dp=dp, name="del_test") va_base = t._handle.va_base # Verify mapping exists mmu_comp = engine._components["sip0.cube0.pe0.pe_mmu"] assert mmu_comp.mmu.translate(va_base) is not None # Delete tensor del t gc.collect() # Mapping should be gone with pytest.raises(PageFault): mmu_comp.mmu.translate(va_base) def test_del_tensor_reclaims_va(): """del tensor returns VA space for reuse.""" ctx, engine = _make_ctx() dp = DPPolicy(cube="replicate", pe="replicate") t1 = ctx.zeros((128, 128), dtype="f16", dp=dp, name="va_reuse1") va1 = t1._handle.va_base del t1 gc.collect() t2 = ctx.zeros((128, 128), dtype="f16", dp=dp, name="va_reuse2") va2 = t2._handle.va_base assert va2 == va1 # ── TF3. Context manager cleanup ───────────────────────────────────── def test_context_manager_cleans_all(): """Exiting context manager cleans up all tensors.""" graph = load_topology(TOPOLOGY_PATH) engine = GraphEngine(graph) with RuntimeContext( engine=engine, target_device=DeviceSelector("sip:0"), correlation_id="ctx_mgr", spec=graph.spec, ) as ctx: dp = DPPolicy(cube="replicate", pe="replicate") t = ctx.zeros((128, 128), dtype="f16", dp=dp, name="ctx_mgr_test") va_base = t._handle.va_base # After context exit, MMU mappings should be cleared mmu_comp = engine._components["sip0.cube0.pe0.pe_mmu"] with pytest.raises(PageFault): mmu_comp.mmu.translate(va_base) # ── TF4. del after context exit is safe ─────────────────────────────── def test_del_after_context_exit_no_crash(): """del tensor after context manager exit does not crash.""" graph = load_topology(TOPOLOGY_PATH) engine = GraphEngine(graph) ctx = RuntimeContext( engine=engine, target_device=DeviceSelector("sip:0"), correlation_id="safe_del", spec=graph.spec, ) dp = DPPolicy(cube="replicate", pe="replicate") t = ctx.zeros((128, 128), dtype="f16", dp=dp, name="safe_del_test") # Simulate context going away ctx.cleanup() # del should not crash even though context already cleaned up del t gc.collect() # No exception = pass # ── TF5. Alloc-del-alloc cycle ──────────────────────────────────────── def test_alloc_del_cycle(): """Multiple alloc-del cycles work correctly.""" ctx, engine = _make_ctx() dp = DPPolicy(cube="replicate", pe="replicate") for i in range(3): t = ctx.zeros((128, 128), dtype="f16", dp=dp, name=f"cycle_{i}") assert t._handle is not None assert t._handle.va_base > 0 del t gc.collect() # ── TF6. del already-cleaned tensor is safe ────────────────────────── def test_del_already_cleaned_tensor_no_crash(): """del on a tensor whose handle is already None does not crash.""" ctx, engine = _make_ctx() dp = DPPolicy(cube="replicate", pe="replicate") t = ctx.zeros((128, 128), dtype="f16", dp=dp, name="double_del") ctx.cleanup() # clears all tensors # t._handle is now None del t # should not crash gc.collect()