Skip to content

[Caching] Ref 7: CPU per-task artifact cache (ORC object serialize/load) - #901

Open
hughperkins wants to merge 22 commits into
mainfrom
hp/po-7-cpu-pertask
Open

[Caching] Ref 7: CPU per-task artifact cache (ORC object serialize/load)#901
hughperkins wants to merge 22 commits into
mainfrom
hp/po-7-cpu-pertask

Merge remote-tracking branch 'origin/main' into hp/po-7-cpu-pertask

69c8957
Select commit
Loading
Failed to load commit list.
Sign in for the full log view
GitHub Actions / Coverage Report succeeded Sep 3, 2026 in 0s

Diff Coverage Report

See details below for per-line coverage annotations.

Details

Coverage Report (69c895735)

Metric Value
Diff coverage (changed lines only) 10%
Overall project coverage 74%

Total: 52 lines, 47 missing, 10% covered

🔴 tests/python/test_per_offload_cache.py (10%)
   1292  # --- Cross-process per-task artifact cache (CUDA + AMDGPU + CPU) ------------------------------------------------------
   1295  # task's own IR (name-free), so a later process reuses an unchanged task instead of recompiling it. CUDA and AMDGPU fill
   1296  # it with GPU code and CPU with a host object; it is gated on `offline_cache`. Reuse is reported on
   1297  # `PerOffloadCacheObservations.tasks_*` (-1 when offline_cache is disabled).
   1375  
   1376  
🟢 1377  @test_utils.test(arch=qd.cpu, offline_cache=False)
🟢 1378  def test_per_task_artifact_cache_disabled_without_offline_cache_cpu() -> None:
   1379      # With offline_cache off the per-task cache never runs, so its counts stay at the -1 sentinel; the frontend split
   1380      # still fires.
🔴 1381      @qd.kernel
🔴 1382      def kernel_two_loops(x: qd.types.ndarray(qd.f32, ndim=1)) -> None:
🔴 1383          for i in x:
🔴 1384              x[i] = x[i] * 2.0 + 1.0
🔴 1385          for i in x:
🔴 1386              x[i] = x[i] - 3.0
   1387  
🔴 1388      arr = qd.ndarray(qd.f32, shape=(_N,))
🔴 1389      arr.from_numpy(np.arange(_N, dtype=np.float32))
🔴 1390      kernel_two_loops(arr)
   1391  
🔴 1392      obs = kernel_two_loops._primal.per_offload_cache_observations
🔴 1393      assert obs.frontend_constructs_total == 2, obs
🔴 1394      assert obs.tasks_total == -1, obs
🔴 1395      assert obs.tasks_cache_hit == -1, obs
🔴 1396      assert obs.tasks_recompiled == -1, obs
🔴 1397      assert np.allclose(arr.to_numpy(), np.arange(_N) * 2.0 + 1.0 - 3.0), arr.to_numpy()
   1398  
   1399  
🟢 1400  def test_per_task_artifact_cache_reuses_shared_task_cross_process_cpu() -> None:
   1401      # A fresh process with a warm disk cache loads an unchanged task instead of recompiling it. Re-init with the same
   1402      # cache path emulates the second process.
🟢 1403      if qd.cpu not in test_utils.expected_archs():
🟢 1404          pytest.skip("this variant exercises the CPU per-task artifact cache")
   1405  
🔴 1406      cache_dir = tempfile.mkdtemp()
🔴 1407      try:
🔴 1408          qd.init(arch=qd.cpu, offline_cache=True, offline_cache_file_path=cache_dir)
   1409  
🔴 1410          @qd.kernel
🔴 1411          def k_first(x: qd.types.ndarray(qd.f32, ndim=1)) -> None:
🔴 1412              for i in x:
🔴 1413                  x[i] = x[i] * 2.0 + 1.0
🔴 1414              for i in x:
🔴 1415                  x[i] = x[i] - 3.0
   1416  
🔴 1417          a = qd.ndarray(qd.f32, shape=(_N,))
🔴 1418          a.from_numpy(np.arange(_N, dtype=np.float32))
🔴 1419          k_first(a)
🔴 1420          obs1 = k_first._primal.per_offload_cache_observations
🔴 1421          assert obs1.tasks_total >= 2, obs1
🔴 1422          assert obs1.tasks_cache_hit == 0, obs1
   1423  
   1424          # Second "process": fresh runtime, same disk. k_second's first loop matches k_first's, so that task is a hit.
🔴 1425          qd.init(arch=qd.cpu, offline_cache=True, offline_cache_file_path=cache_dir)
   1426  
🔴 1427          @qd.kernel
🔴 1428          def k_second(x: qd.types.ndarray(qd.f32, ndim=1)) -> None:
🔴 1429              for i in x:
🔴 1430                  x[i] = x[i] * 2.0 + 1.0
🔴 1431              for i in x:
🔴 1432                  x[i] = x[i] + 7.0
   1433  
🔴 1434          b = qd.ndarray(qd.f32, shape=(_N,))
🔴 1435          b.from_numpy(np.arange(_N, dtype=np.float32))
🔴 1436          k_second(b)
🔴 1437          obs2 = k_second._primal.per_offload_cache_observations
🔴 1438          assert obs2.tasks_cache_hit > 0, obs2
🔴 1439          assert obs2.tasks_recompiled >= 1, obs2
🔴 1440          assert obs2.tasks_cache_hit + obs2.tasks_recompiled == obs2.tasks_total, obs2
   1441  
🔴 1442          assert np.allclose(b.to_numpy(), np.arange(_N) * 2.0 + 1.0 + 7.0), b.to_numpy()
   1443      finally:
🔴 1444          qd.reset()
🔴 1445          shutil.rmtree(cache_dir, ignore_errors=True)