From 5d571018de5b63a9a90e5c11e6d6e86bbff4a957 Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sat, 29 Aug 2026 21:59:58 -0500 Subject: [PATCH 1/4] Route reconstructed pages to native page placement Use the direct-file native restore function only for immutable flat snapshot objects and route reconstructed page-delta bytes to the in-memory C++/CUDA page-placement function. The two interfaces have distinct verified signatures: direct restore accepts manifest lookup and cache-root inputs; page placement accepts authenticated encoded pages and a decoded page plan. A rejected page-delta restore continues to recompute rather than serving unverified state. SparkCache wire values, digest salts, chunk geometry, and cache namespace are unchanged; deployment source receipts are rebound to the resulting source tree. Validation: 739 SparkCache tests passed with 7 skipped; 108 deployment tests passed with 1 skipped; Ruff and diff checks passed. --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- sparkcache/spark_context_cache_connector.py | 32 +++++++++++++------ .../test_spark_context_cache_connector.py | 16 +++++++++- 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index e73064d..16e4a2e 100644 --- a/deploy/deepseek_v4/tp4_profile.json +++ b/deploy/deepseek_v4/tp4_profile.json @@ -4,7 +4,7 @@ "cache_model_profile": "deepseek-v4-fp8-hma", "published_runtime_base": "ghcr.io/fujitsupolycom/gb10-vllm-serving@sha256:6fc26fdad81a18f0fff67ce0a05f6d90165625ea2e1cac8a6f39bfb462017028", "sparkcache": { - "source_sha256": "4998b24f4f504aeeb9bf92769ec720e282f546e6726d89fdfd06c4efa8d17c10" + "source_sha256": "f7c0565521fddeff7085e4cc08043cb8d1e2bde33abc67f83b8608a162d05b88" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 68803e1..4ef0b23 100644 --- a/deploy/glm52_35bpw/profile.json +++ b/deploy/glm52_35bpw/profile.json @@ -5,7 +5,7 @@ "published_runtime_base": "ghcr.io/fujitsupolycom/gb10-vllm-serving@sha256:6fc26fdad81a18f0fff67ce0a05f6d90165625ea2e1cac8a6f39bfb462017028", "base_image_requirement": "exact GLM-5.2 3.5-bpw R7 image recorded by the source container inspection", "sparkcache": { - "source_sha256": "4998b24f4f504aeeb9bf92769ec720e282f546e6726d89fdfd06c4efa8d17c10" + "source_sha256": "f7c0565521fddeff7085e4cc08043cb8d1e2bde33abc67f83b8608a162d05b88" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/sparkcache/spark_context_cache_connector.py b/sparkcache/spark_context_cache_connector.py index e310d79..75483f7 100644 --- a/sparkcache/spark_context_cache_connector.py +++ b/sparkcache/spark_context_cache_connector.py @@ -168,7 +168,10 @@ def _load_native_components() -> SimpleNamespace: ArenaMode=placement.ArenaMode, RecordKind=placement.RecordKind, execute_native_restore=restore.execute_native_restore, - execute_native_hybrid_placement=(hybrid_restore.execute_native_hybrid_restore), + execute_native_hybrid_restore=hybrid_restore.execute_native_hybrid_restore, + execute_native_hybrid_placement=( + hybrid_restore.execute_native_hybrid_placement + ), bind_page_reference=binding.bind_page_reference, hybrid_page_cuda_capability=binding.CAP_HYBRID_PAGE_CUDA, ) @@ -683,7 +686,8 @@ def __init__( self._native_adapter: Any = None self._native_adapters: list[Any] = [] self._native_execute_restore: Any = None - self._native_execute_hybrid: Any = None + self._native_execute_hybrid_restore: Any = None + self._native_execute_hybrid_placement: Any = None self._native_required_record_mask = 0 # Scheduler state. self._need_load: dict[str, tuple[str, int]] = {} @@ -2067,9 +2071,12 @@ def _configure_native_hybrid_restore(self) -> None: ) adapters.append(adapter) adapter.configure_pages(layout, self._layer_tensors) - execute = components.execute_native_hybrid_placement - if not callable(execute): - raise TypeError("native hybrid restore orchestrator is not callable") + execute_restore = components.execute_native_hybrid_restore + execute_placement = components.execute_native_hybrid_placement + if not callable(execute_restore): + raise TypeError("native hybrid direct-restore orchestrator is not callable") + if not callable(execute_placement): + raise TypeError("native hybrid page-placement orchestrator is not callable") except Exception as error: for adapter in adapters: with contextlib.suppress(Exception): @@ -2079,7 +2086,8 @@ def _configure_native_hybrid_restore(self) -> None: ) from error self._native_adapters = adapters self._native_adapter = adapters[0] - self._native_execute_hybrid = execute + self._native_execute_hybrid_restore = execute_restore + self._native_execute_hybrid_placement = execute_placement self.counters["native_hybrid_configured"] = 1 logger.info( "spark-context-cache: native hybrid restore configured" @@ -3220,14 +3228,16 @@ def _load_hybrid_pages( if len(groups) != len(layout.groups): raise HybridCodecError("request block tables disagree with page groups") if self._native_restore_enabled and lookup.root_kind != "page_delta": - if not self._native_adapters or not callable(self._native_execute_hybrid): + if not self._native_adapters or not callable( + self._native_execute_hybrid_restore + ): raise RuntimeError( "native hybrid restore selected without a configured adapter" ) if not 0 <= native_lane < len(self._native_adapters): raise RuntimeError("native hybrid restore lane is unavailable") try: - result = self._native_execute_hybrid( + result = self._native_execute_hybrid_restore( adapter=self._native_adapters[native_lane], request_id=plan.request_id, lookup=lookup, @@ -3316,14 +3326,16 @@ def _load_hybrid_pages( time.perf_counter_ns() - reassembly_started, ) if self._native_restore_enabled: - if not self._native_adapters or not callable(self._native_execute_hybrid): + if not self._native_adapters or not callable( + self._native_execute_hybrid_placement + ): raise RuntimeError( "native hybrid restore selected without a configured adapter" ) if not 0 <= native_lane < len(self._native_adapters): raise RuntimeError("native hybrid restore lane is unavailable") try: - result = self._native_execute_hybrid( + result = self._native_execute_hybrid_placement( adapter=self._native_adapters[native_lane], request_id=plan.request_id, encoded_pages=encoded_pages, diff --git a/sparkcache/test_spark_context_cache_connector.py b/sparkcache/test_spark_context_cache_connector.py index e5a4d35..ce05137 100644 --- a/sparkcache/test_spark_context_cache_connector.py +++ b/sparkcache/test_spark_context_cache_connector.py @@ -9,6 +9,7 @@ import dataclasses import hashlib +import inspect import json import os import struct @@ -153,6 +154,19 @@ def _get_connector_metadata(self): ) +class NativeHybridDispatchTests(unittest.TestCase): + def test_loader_exposes_distinct_direct_and_materialized_page_paths(self) -> None: + components = connector_module._load_native_components() + direct = components.execute_native_hybrid_restore + materialized = components.execute_native_hybrid_placement + + self.assertIsNot(direct, materialized) + self.assertIn("lookup", inspect.signature(direct).parameters) + self.assertNotIn("encoded_pages", inspect.signature(direct).parameters) + self.assertIn("encoded_pages", inspect.signature(materialized).parameters) + self.assertNotIn("lookup", inspect.signature(materialized).parameters) + + class CodecTests(unittest.TestCase): def test_owned_positions_interleave_one(self) -> None: self.assertEqual(codec.owned_positions(8, 4, 0), (0, 4)) @@ -558,7 +572,7 @@ def native_placement(**kwargs): connector._native_restore_enabled = True connector._native_adapters = [object()] - connector._native_execute_hybrid = native_placement + connector._native_execute_hybrid_placement = native_placement self.assertTrue( connector._load_one( _ReqPlan( From b95aa8ab0068dc66a6892a5c311d7e9dd4a9c55a Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:41:58 -0500 Subject: [PATCH 2/4] Name SparkCache CUDA restore interfaces Expose SparkCache CUDA restore and CUDA placement names in connector configuration, environment variables, deployment profiles, CLI options, logs, and documentation. Preserve legacy native-restore keys and Python symbols as compatibility aliases; conflicting canonical and legacy values reject startup and legacy-only input warns once per process. Cache identity, digest salts, chunk geometry, and stored bytes are unchanged. Validation: 746 SparkCache tests passed with 7 skipped; 108 deployment tests passed with 1 skipped; Ruff passed. --- DEEPSEEK_V4_LIVE_VALIDATION.md | 2 +- GLM52_A2_LIVE_VALIDATION.md | 2 +- GLM53_FLASH_DFLASH7_LIVE_VALIDATION.md | 2 +- ...3_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md | 16 +- MULTI_MODEL_LIVE_VALIDATION.md | 2 +- README.md | 10 +- ROADMAP.md | 8 +- deploy/deepseek_v4/README.md | 2 +- deploy/deepseek_v4/entrypoint.sh | 2 +- deploy/deepseek_v4/test_tp4_profile.py | 17 +- deploy/deepseek_v4/tp4_profile.json | 4 +- deploy/deepseek_v4/tp4_profile.py | 26 +- deploy/glm52_35bpw/README.md | 16 +- deploy/glm52_35bpw/launch.py | 41 ++- deploy/glm52_35bpw/profile.json | 4 +- deploy/glm52_35bpw/profile.py | 165 ++++++++---- deploy/glm53_flash/IMAGE_ANNOUNCEMENT.md | 4 +- deploy/glm53_flash/README.md | 19 +- deploy/glm53_flash/concurrency_benchmark.py | 10 +- deploy/glm53_flash/profile.py | 2 +- .../glm53_flash/test_concurrency_benchmark.py | 2 +- docs/sparkcache-prefix-explainer.html | 6 +- pyproject.toml | 2 +- sparkcache/README.md | 30 ++- sparkcache/native/README.md | 38 +-- sparkcache/native/SNAPSHOT_BYTE_COMPARISON.md | 2 +- sparkcache/native/python/__init__.py | 4 +- sparkcache/spark_cache_cuda.py | 4 + sparkcache/spark_cache_native.py | 6 +- sparkcache/spark_context_cache_config.py | 235 ++++++++++++++---- sparkcache/spark_context_cache_connector.py | 87 ++++--- .../spark_context_cache_cuda_page_restore.py | 4 + .../spark_context_cache_cuda_placement.py | 4 + .../spark_context_cache_cuda_restore.py | 4 + sparkcache/spark_context_cache_hybrid.py | 2 +- ...ark_context_cache_native_hybrid_restore.py | 60 ++++- .../spark_context_cache_native_placement.py | 54 ++-- .../spark_context_cache_native_restore.py | 41 ++- sparkcache/spark_context_cache_profiles.py | 63 +++-- sparkcache/test_cuda_restore_terminology.py | 30 +++ sparkcache/test_defect_regressions.py | 49 ++-- sparkcache/test_generalization.py | 10 +- sparkcache/test_glm52_35bpw_deploy.py | 130 +++++----- sparkcache/test_glm53_flash_deploy.py | 17 +- sparkcache/test_spark_context_cache_config.py | 118 +++++++-- .../test_spark_context_cache_connector.py | 70 +++--- 46 files changed, 955 insertions(+), 471 deletions(-) create mode 100644 sparkcache/spark_cache_cuda.py create mode 100644 sparkcache/spark_context_cache_cuda_page_restore.py create mode 100644 sparkcache/spark_context_cache_cuda_placement.py create mode 100644 sparkcache/spark_context_cache_cuda_restore.py create mode 100644 sparkcache/test_cuda_restore_terminology.py diff --git a/DEEPSEEK_V4_LIVE_VALIDATION.md b/DEEPSEEK_V4_LIVE_VALIDATION.md index 118ff19..c4d0b47 100644 --- a/DEEPSEEK_V4_LIVE_VALIDATION.md +++ b/DEEPSEEK_V4_LIVE_VALIDATION.md @@ -124,7 +124,7 @@ filling a 200 GiB root or qualify model-serving load behavior. This evidence qualifies one TP2/DCP1 development appliance. Model-serving load behavior and arbitrary DeepSeek-V4 deployments are unsupported. Block-page storage has bounded NVMe maintenance for end-of-prefill asynchronous -snapshots. Native restore, streaming snapshots, and DCP-sharded block pages +snapshots. SparkCache CUDA restore, streaming snapshots, and DCP-sharded block pages are unsupported by this profile. The qualified service used the Python asynchronous restore path; every chunk was checksum-verified before its pages were installed. diff --git a/GLM52_A2_LIVE_VALIDATION.md b/GLM52_A2_LIVE_VALIDATION.md index 7718bd3..d15e79d 100644 --- a/GLM52_A2_LIVE_VALIDATION.md +++ b/GLM52_A2_LIVE_VALIDATION.md @@ -97,4 +97,4 @@ diagnostic. This record qualifies only the exact `0.1.0a2` wheel and GLM TP4/DCP4 lane. It does not qualify the `0.1.0a2` DeepSeek TP2/DCP1 or TP4/DCP1 profiles, a different runtime image, a different checkpoint, another scheduler budget, -streaming snapshots, or native restore. +streaming snapshots, or SparkCache CUDA restore. diff --git a/GLM53_FLASH_DFLASH7_LIVE_VALIDATION.md b/GLM53_FLASH_DFLASH7_LIVE_VALIDATION.md index d81f6d0..10b7525 100644 --- a/GLM53_FLASH_DFLASH7_LIVE_VALIDATION.md +++ b/GLM53_FLASH_DFLASH7_LIVE_VALIDATION.md @@ -170,7 +170,7 @@ visible `message.content` must equal `SPARKCACHE_GLM53_OK` byte for byte. This evidence does not qualify another SparkCache source tree, vLLM source contract, target or draft checkpoint, parallel topology, scheduler budget, -cache geometry, native direct restore, streaming snapshots, or MTP profile. +cache geometry, SparkCache direct CUDA restore, streaming snapshots, or MTP profile. It does not establish throughput neutrality or restore performance for spans larger than 8,192 tokens. Full reasoning-trace equality is not a semantic oracle for this GLM runtime. The historical canary establishes successful diff --git a/GLM53_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md b/GLM53_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md index b99eeea..c55cab1 100644 --- a/GLM53_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md +++ b/GLM53_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md @@ -1,10 +1,14 @@ -# GLM-5.3 native restore and shared-prefix validation +# GLM-5.3 SparkCache CUDA restore and shared-prefix validation + +The repository path retains its compatibility filename so existing evidence +links remain valid. In this record, SparkCache CUDA restore and SparkCache CUDA +placement are the canonical capability and data-movement terms. Date: 2026-08-29 ## Status -Native direct restore, verified multi-group recovery, bounded shared GPU-prefix +SparkCache direct CUDA restore, verified multi-group recovery, bounded shared GPU-prefix reuse, C2/C8/C16 completion, shared-trunk C16 completion, and continued generation are **qualified** for the exact GLM-5.3 Flash TP4/DCP1 runtime identified below. @@ -31,7 +35,7 @@ qualification requires a receipt produced by the equality validator in | vLLM source revision | `local-inference-lab/vllm@da4d7be6c97434f6942292ed8abbf4b32dc44355` | | Serving topology | GLM-5.3 Flash, TP4/DCP1, one rank on each of `spark-r0` through `spark-r3` | | Scheduler capacity | `--max-num-seqs 32` | -| Restore concurrency | Two host restore workers and two native placement lanes per rank | +| Restore concurrency | Two host restore workers and two SparkCache CUDA placement lanes per rank | | Native staging | Two 256 MiB mapped-host arenas per rank | | Persistent prefix | 131,072 tokens and 813,068,464 encoded bytes per rank | | Runtime receipt | `evidence/glm53-flash-dflash7-bf16/hotlease-2b86fb9-runtime.json` | @@ -48,7 +52,7 @@ The direct page-placement implementation is identified by these commits: | Responsibility | Revision | |---|---| | Restore phase timing | `175f9401984a03744d7fe1a985d7c2ef6035f949` | -| Native hybrid-page placement | `71f367be07788d611698a251fe866d678b0034ae` | +| SparkCache CUDA page placement | `71f367be07788d611698a251fe866d678b0034ae` | | Multi-slab restore and exact-prefix discovery | `8e7f5fc62fd4fffdd661aca9ea634cf130c45d1a` | | Direct pipelined slab restore | `94c44930a13df5c668d777e0270e7d8203069d7c` | | Authenticated span-table bound | `9dbf73c0caab89b24346567e2769752ac746e114` | @@ -62,7 +66,7 @@ source-tree SHA-256 `b3e84d...` identified in the table above. ## Implemented restore path -Native restore reads immutable `.spcc` objects directly into alternating +SparkCache CUDA restore reads immutable `.spcc` objects directly into alternating mapped-host arenas with `pread`, hashes every complete file in place, validates its authenticated extent table, and submits only validated spans to the CUDA page-placement kernel. Read work and CUDA submission overlap across slabs. @@ -96,7 +100,7 @@ historical canary found the expected marker suffix, and HTTP health remained ### Eight concurrent 16K prefixes The Python/Torch placement path produced 1.54--1.57 second submission spikes; -eight clients completed in 9.45--10.64 seconds. Native placement submitted in +eight clients completed in 9.45--10.64 seconds. SparkCache CUDA placement submitted in 6--15 ms, and two restore lanes completed eight clients in approximately 1.2--2.1 seconds. This diagnostic isolates page placement as the dominant serialized cost in that workload; it is not a separate deployment diff --git a/MULTI_MODEL_LIVE_VALIDATION.md b/MULTI_MODEL_LIVE_VALIDATION.md index 17f9d10..e07d74d 100644 --- a/MULTI_MODEL_LIVE_VALIDATION.md +++ b/MULTI_MODEL_LIVE_VALIDATION.md @@ -141,6 +141,6 @@ three recorded profiles. This record does not qualify another wheel, source tree, runtime image, checkpoint, scheduler budget, topology, cache geometry, or vLLM source -contract. DeepSeek DCP2 and DCP4, streaming snapshots, native restore in these +contract. DeepSeek DCP2 and DCP4, streaming snapshots, SparkCache CUDA restore in these profiles, buddy replication, and longest-stored-prefix reuse for a growing conversation remain unsupported or research-only as stated in `README.md`. diff --git a/README.md b/README.md index 504bc56..b947429 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Python-placement restore. The source deployment separately qualifies native 131,072-token restore and bounded shared-prefix reuse. See [the public image record](deploy/glm53_flash/IMAGE_ANNOUNCEMENT.md), [the GLM-5.3 validation](GLM53_FLASH_DFLASH7_LIVE_VALIDATION.md), and -[the native restore record](GLM53_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md). +[the SparkCache CUDA restore record](GLM53_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md). -The public image does not contain the model checkpoints. Native restore and +The public image does not contain the model checkpoints. SparkCache CUDA restore and shared GPU-prefix qualification belong to a later source-bound runtime that has no published OCI digest. @@ -147,7 +147,7 @@ timing. | Prefix and concurrency | Comparison | Recorded result | |---|---|---| | 8,192 tokens, C1 | qualified Python page restore | 147.2–194.0 ms cache service per rank | -| 16,384 tokens, C8 | Python/Torch placement vs native placement | 9.45–10.64 s vs 1.2–2.1 s client latency | +| 16,384 tokens, C8 | Python/Torch placement vs SparkCache CUDA placement | 9.45–10.64 s vs 1.2–2.1 s client latency | | 131,072 tokens, C1 | reconstruction pipeline vs cold direct mapped-arena restore | 1.29–1.46 s vs 131–250 ms cache service per rank; a host-warm restore reached 104–165 ms | | 131,072-token shared prefix, C16 | independent restores vs shared verified GPU blocks | rank-local work fell from 16 × 813 MB to 1 × 813 MB; standard-chat client p50 fell from 3.363 s to 2.980 s | | 131,072-token shared prefix, pretokenized C16 | standalone measurement | 2.698 s client p50 and 2.701 s maximum | @@ -237,7 +237,7 @@ and recovery behavior are derived and tested. |---|---|---| | `vllm-project/vllm@fcc614141e5e9ab18cb304c476f7feed2a9552e3` with `patches/vllm/` | **implemented** | Exact patch inputs are published; no standalone public runtime builder is provided | | vLLM build `e2666d9a6` with `patches/vllm-e2666d9a6/` | **qualified** | DeepSeek-V4 and GLM-5.2 builders verify source, patch, and postimage hashes | -| `local-inference-lab/vllm@da4d7be6c97434f6942292ed8abbf4b32dc44355` with `patches/vllm-da4d7be/` | **qualified** | GLM-5.3 HMA recovery, native restore, and bounded shared-prefix attachment | +| `local-inference-lab/vllm@da4d7be6c97434f6942292ed8abbf4b32dc44355` with `patches/vllm-da4d7be/` | **qualified** | GLM-5.3 HMA recovery, SparkCache CUDA restore, and bounded shared-prefix attachment | The GLM-5.3 contract at [`vllm-kv-block-lease-contract-da4d7be.json`](sparkcache/runtime_patches/vllm-kv-block-lease-contract-da4d7be.json) @@ -251,7 +251,7 @@ Native loading requires an explicit library path and SHA-256. CUDA 13 builds run a GPU-free byte-exact reference test and a CUDA hybrid-page probe before model-serving qualification. -Native direct restore reads `.spcc` objects into alternating mapped arenas, +SparkCache direct CUDA restore reads `.spcc` objects into alternating mapped arenas, hashes complete files in place, validates authenticated extents, and overlaps read work with CUDA submission. diff --git a/ROADMAP.md b/ROADMAP.md index f62a647..58bb2e2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -27,7 +27,7 @@ GPU-free coverage proves copy-on-write extension, bounded page-delta compaction, recurrent/sliding boundary geometry, corruption removal, reference-aware maintenance, and verified reconstruction. Live GLM-5.3 qualification must still measure publication bytes, cold restore latency, -native-placement latency after page reconstruction, SSD writes, and continued +SparkCache CUDA-placement latency after page reconstruction, SSD writes, and continued generation across repeated conversation extensions. ### Per-entry retention controls @@ -54,14 +54,14 @@ verification before vLLM schedules the request. It must reuse the bounded asynchronous-load machinery without claiming an external hit until all ranks confirm completion. -### Native restore expansion +### SparkCache CUDA restore expansion **Status: research-only qualification work.** Native multi-group page restore is implemented and source-runtime-qualified for the recorded GLM-5.3 TP4/DCP1 profile. Tail page deltas reconstruct a fully verified snapshot before Python -or native placement, but that path has no live performance qualification. +or SparkCache CUDA placement, but that path has no live performance qualification. -DeepSeek-V4 opaque HMA pages retain their verified Python restore path. Native +DeepSeek-V4 opaque HMA pages retain their verified Python restore path. CUDA support for that profile must describe all five page groups, preserve each group's semantic reuse window, and prove byte identity before changing its qualified deployment contract. diff --git a/deploy/deepseek_v4/README.md b/deploy/deepseek_v4/README.md index ddf804c..2886897 100644 --- a/deploy/deepseek_v4/README.md +++ b/deploy/deepseek_v4/README.md @@ -15,7 +15,7 @@ DeepSeek-V4 SparkCache profiles. size 256; - 524,288-token request limit and 32 sequences; - 200 GiB high / 180 GiB low rank-local NVMe watermarks; and -- Python verified restore with streaming and native placement disabled. +- Python verified restore with streaming and SparkCache CUDA placement disabled. The launcher rejects DCP2/DCP4 because neither DSpark nor the opaque five-group HMA page format defines safe DCP ownership. See `DCP_SUPPORT.md`. diff --git a/deploy/deepseek_v4/entrypoint.sh b/deploy/deepseek_v4/entrypoint.sh index a3e60aa..e1a0366 100644 --- a/deploy/deepseek_v4/entrypoint.sh +++ b/deploy/deepseek_v4/entrypoint.sh @@ -52,7 +52,7 @@ print(json.dumps({ "SPARKCACHE_MAX_SPAN_TOKENS", "524288" )), "spark_cache_streaming_snapshots": False, - "spark_cache_native_restore": False, + "spark_cache_cuda_restore": False, }, }, separators=(",", ":"))) PY diff --git a/deploy/deepseek_v4/test_tp4_profile.py b/deploy/deepseek_v4/test_tp4_profile.py index fd34db6..0e2e1aa 100644 --- a/deploy/deepseek_v4/test_tp4_profile.py +++ b/deploy/deepseek_v4/test_tp4_profile.py @@ -145,7 +145,7 @@ def test_transfer_config_is_bounded_python_hma_restore() -> None: assert extra["spark_cache_model_profile"] == "deepseek-v4-fp8-hma" assert extra["spark_cache_draft_policy"] == "colocated_target" assert extra["spark_cache_streaming_snapshots"] is False - assert extra["spark_cache_native_restore"] is False + assert extra["spark_cache_cuda_restore"] is False assert extra["spark_cache_max_bytes"] == MAX_BYTES == 200 * 1024**3 assert extra["spark_cache_low_watermark_bytes"] == LOW_WATERMARK_BYTES assert LOW_WATERMARK_BYTES == 180 * 1024**3 @@ -174,14 +174,13 @@ def test_transform_accepts_all_four_physical_ranks() -> None: environment = _environment(transformed) assert environment["MASTER_PORT"] == "29600" assert "SPARK_CONTEXT_CACHE_ENABLE" not in environment - assert "SPARK_CONTEXT_CACHE_ENABLE" in environment[ - "SPARKRING_EXPLICITLY_UNSET" - ] + assert "SPARK_CONTEXT_CACHE_ENABLE" in environment["SPARKRING_EXPLICITLY_UNSET"] assert environment["PYTHONPATH"].startswith("/opt/sparkcache-src:") assert "/opt/sparkcache-src/sparkcache" not in environment["PYTHONPATH"] - assert transformed["Config"]["Labels"][ - "org.sparkcache.deployment-profile" - ] == "deepseek-v4-flash-0731-tp4-dcp1" + assert ( + transformed["Config"]["Labels"]["org.sparkcache.deployment-profile"] + == "deepseek-v4-flash-0731-tp4-dcp1" + ) def test_cluster_preflight_accepts_one_homogeneous_four_rank_ring() -> None: @@ -243,9 +242,7 @@ def test_cluster_preflight_rejects_collective_port_drift() -> None: @pytest.mark.parametrize("degree", (2, 4)) def test_transform_rejects_hma_dcp_greater_than_one(degree: int) -> None: inspection = _source() - inspection["Config"]["Cmd"].extend( - ("--decode-context-parallel-size", str(degree)) - ) + inspection["Config"]["Cmd"].extend(("--decode-context-parallel-size", str(degree))) with pytest.raises(ProfileTransformError, match="DCP1"): transform_inspection(inspection, checkpoint_sha256=CHECKPOINT) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index 16e4a2e..e1f1dbe 100644 --- a/deploy/deepseek_v4/tp4_profile.json +++ b/deploy/deepseek_v4/tp4_profile.json @@ -4,7 +4,7 @@ "cache_model_profile": "deepseek-v4-fp8-hma", "published_runtime_base": "ghcr.io/fujitsupolycom/gb10-vllm-serving@sha256:6fc26fdad81a18f0fff67ce0a05f6d90165625ea2e1cac8a6f39bfb462017028", "sparkcache": { - "source_sha256": "f7c0565521fddeff7085e4cc08043cb8d1e2bde33abc67f83b8608a162d05b88" + "source_sha256": "48e008ba0cbd12f1ffae1c28388ea83310f41c6219c955e13d63ab171290d8de" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", @@ -39,7 +39,7 @@ "load_failure_policy": "recompute", "draft_policy": "colocated_target", "streaming_snapshots": false, - "native_restore": false + "cuda_restore": false }, "vllm": { "commit": "e2666d9a6", diff --git a/deploy/deepseek_v4/tp4_profile.py b/deploy/deepseek_v4/tp4_profile.py index 7905d70..10aae2b 100644 --- a/deploy/deepseek_v4/tp4_profile.py +++ b/deploy/deepseek_v4/tp4_profile.py @@ -43,9 +43,7 @@ class ProfileTransformError(DeploymentContractError): def _require_sha256(value: str, role: str) -> str: if _SHA256_RE.fullmatch(value) is None: - raise ProfileTransformError( - f"{role} must be 64 lowercase hexadecimal digits" - ) + raise ProfileTransformError(f"{role} must be 64 lowercase hexadecimal digits") return value @@ -229,7 +227,9 @@ def _validate_arguments(arguments: list[str]) -> int: "--disable-prefix-caching", ): if forbidden in arguments: - raise ProfileTransformError(f"source command contains unsupported {forbidden}") + raise ProfileTransformError( + f"source command contains unsupported {forbidden}" + ) for required_flag in ("--enable-auto-tool-choice",): if arguments.count(required_flag) != 1: raise ProfileTransformError(f"source command requires {required_flag}") @@ -241,7 +241,9 @@ def _validate_arguments(arguments: list[str]) -> int: kernel = json.loads(_one(arguments, "--kernel-config")) speculative = json.loads(_one(arguments, "--speculative-config")) except json.JSONDecodeError as error: - raise ProfileTransformError("source DeepSeek JSON argument is invalid") from error + raise ProfileTransformError( + "source DeepSeek JSON argument is invalid" + ) from error if kernel.get("enable_cutedsl_warmup") is not False: raise ProfileTransformError("source DeepSeek kernel config must disable warmup") expected_speculative = { @@ -249,7 +251,9 @@ def _validate_arguments(arguments: list[str]) -> int: "num_speculative_tokens": serving["speculation_tokens"], "moe_backend": serving["speculation_moe_backend"], } - if any(speculative.get(key) != value for key, value in expected_speculative.items()): + if any( + speculative.get(key) != value for key, value in expected_speculative.items() + ): raise ProfileTransformError("source DeepSeek DSpark configuration differs") if speculative.get("draft_sample_method", "greedy") != "greedy": raise ProfileTransformError("source DeepSeek DSpark sampling must be greedy") @@ -293,7 +297,7 @@ def build_kv_transfer_config(checkpoint_sha256: str) -> dict[str, Any]: "spark_cache_store": True, "spark_cache_restore": True, "spark_cache_streaming_snapshots": False, - "spark_cache_native_restore": False, + "spark_cache_cuda_restore": False, "spark_cache_max_bytes": cache["max_bytes"], "spark_cache_low_watermark_bytes": cache["low_watermark_bytes"], "spark_cache_ttl_seconds": cache["ttl_seconds"], @@ -311,7 +315,9 @@ def _reserved_ports(environment: Iterable[str]) -> frozenset[int]: try: value = int(raw) except ValueError as error: - raise ProfileTransformError(f"environment port {name} is invalid") from error + raise ProfileTransformError( + f"environment port {name} is invalid" + ) from error validated = _port(value, name) assert validated is not None reserved.add(validated) @@ -342,9 +348,7 @@ def transform_inspection( reserved = _reserved_ports(source_environment) api_port = _port(api_port, "api_port") master_port = _port(master_port, "master_port") - effective_api = api_port or ( - int(_one(arguments, "--port")) if rank == 0 else None - ) + effective_api = api_port or (int(_one(arguments, "--port")) if rank == 0 else None) effective_master = master_port or int(_one(arguments, "--master-port")) if effective_api is not None and effective_api == effective_master: raise ProfileTransformError("api_port and master_port must differ") diff --git a/deploy/glm52_35bpw/README.md b/deploy/glm52_35bpw/README.md index 1d42bb0..4f1c671 100644 --- a/deploy/glm52_35bpw/README.md +++ b/deploy/glm52_35bpw/README.md @@ -133,7 +133,7 @@ The default rank-local policy is: - fail-closed load policy `recompute`; - colocated-target MTP state, with no separate draft digest; - scheduler probe `none`; and -- streaming snapshots and native direct restore disabled. +- streaming snapshots and SparkCache direct CUDA restore disabled. The complete `--kv-transfer-config` is the enable switch. The obsolete `SPARK_CONTEXT_CACHE_ENABLE` image variable is removed and explicitly unset @@ -144,9 +144,9 @@ The accepted inspection has neither `--enable-prefix-caching` nor transformer preserves an absent flag or one explicit enable and rejects an explicit disable. -## Native feature switches +## CUDA restore and streaming switches -Streaming publication and native direct restore use different libraries and +Streaming publication and SparkCache direct CUDA restore use different libraries and can be enabled independently. Each enable requires its library's container path and SHA-256: @@ -157,11 +157,11 @@ path and SHA-256: /opt/sparkcache-src/sparkcache/native/build/libspark_cache_snapshot.so \ --streaming-native-library-sha256 <64-lowercase-hex> -# Add independently for native direct restore. ---native-restore \ ---native-restore-library \ +# Add independently for SparkCache direct CUDA restore. +--cuda-restore \ +--cuda-placement-library \ /opt/sparkcache-src/sparkcache/native/build/libspark_cache_placement.so \ ---native-restore-library-sha256 <64-lowercase-hex> +--cuda-placement-library-sha256 <64-lowercase-hex> ``` Keep both switches off for the qualification baseline. Before enabling @@ -170,7 +170,7 @@ in the R7 toolchain. The read-only SparkCache source bind then carries the resulting library into the container at the paths above. The 200/180 GiB capacity policy remains active when streaming is enabled. -Use `--no-streaming-snapshots` and `--no-native-restore` for explicit +Use `--no-streaming-snapshots` and `--no-cuda-restore` for explicit disabled settings. ## Concurrent deployment instances diff --git a/deploy/glm52_35bpw/launch.py b/deploy/glm52_35bpw/launch.py index 50f51a9..73e3d58 100644 --- a/deploy/glm52_35bpw/launch.py +++ b/deploy/glm52_35bpw/launch.py @@ -27,9 +27,7 @@ VLLM_SCHEDULER_PATH = ( "/opt/venv/lib/python3.12/site-packages/vllm/v1/core/sched/scheduler.py" ) -VLLM_CONFIG_PATH = ( - "/opt/venv/lib/python3.12/site-packages/vllm/config/vllm.py" -) +VLLM_CONFIG_PATH = "/opt/venv/lib/python3.12/site-packages/vllm/config/vllm.py" EXPECTED_SCHEDULER_SHA256 = ( "d4ebec211b027b6c7f64574f79374237de0f5fde0c5c03f20f1cb1596ffadc3a" ) @@ -128,19 +126,37 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--streaming-native-library") parser.add_argument("--streaming-native-library-sha256") parser.add_argument("--streaming-timing", action="store_true") + parser.add_argument( + "--cuda-restore", + action=argparse.BooleanOptionalAction, + default=None, + ) + parser.add_argument("--cuda-placement-library") + parser.add_argument("--cuda-placement-library-sha256") + parser.add_argument( + "--cuda-placement-arena-bytes", + type=int, + default=None, + ) + parser.add_argument("--cuda-restore-io-workers", type=int) parser.add_argument( "--native-restore", action=argparse.BooleanOptionalAction, - default=False, + default=None, + help=argparse.SUPPRESS, ) - parser.add_argument("--native-restore-library") - parser.add_argument("--native-restore-library-sha256") + parser.add_argument("--native-restore-library", help=argparse.SUPPRESS) + parser.add_argument("--native-restore-library-sha256", help=argparse.SUPPRESS) parser.add_argument( "--native-restore-arena-bytes", type=int, - default=128 * 1024 * 1024, + help=argparse.SUPPRESS, + ) + parser.add_argument( + "--native-restore-io-workers", + type=int, + help=argparse.SUPPRESS, ) - parser.add_argument("--native-restore-io-workers", type=int, default=8) parser.add_argument("--create-only", action="store_true") args = parser.parse_args(argv) @@ -166,10 +182,13 @@ def main(argv: list[str] | None = None) -> int: cache_root=args.cache_root, streaming_snapshots=args.streaming_snapshots, streaming_native_library=args.streaming_native_library, - streaming_native_library_sha256=( - args.streaming_native_library_sha256 - ), + streaming_native_library_sha256=(args.streaming_native_library_sha256), streaming_timing=args.streaming_timing, + cuda_restore=args.cuda_restore, + cuda_placement_library=args.cuda_placement_library, + cuda_placement_library_sha256=args.cuda_placement_library_sha256, + cuda_placement_arena_bytes=args.cuda_placement_arena_bytes, + cuda_restore_io_workers=args.cuda_restore_io_workers, native_restore=args.native_restore, native_restore_library=args.native_restore_library, native_restore_library_sha256=args.native_restore_library_sha256, diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 4ef0b23..91dcfe7 100644 --- a/deploy/glm52_35bpw/profile.json +++ b/deploy/glm52_35bpw/profile.json @@ -5,7 +5,7 @@ "published_runtime_base": "ghcr.io/fujitsupolycom/gb10-vllm-serving@sha256:6fc26fdad81a18f0fff67ce0a05f6d90165625ea2e1cac8a6f39bfb462017028", "base_image_requirement": "exact GLM-5.2 3.5-bpw R7 image recorded by the source container inspection", "sparkcache": { - "source_sha256": "f7c0565521fddeff7085e4cc08043cb8d1e2bde33abc67f83b8608a162d05b88" + "source_sha256": "48e008ba0cbd12f1ffae1c28388ea83310f41c6219c955e13d63ab171290d8de" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", @@ -41,6 +41,6 @@ "load_failure_policy": "recompute", "draft_policy": "colocated_target", "streaming_snapshots": false, - "native_restore": false + "cuda_restore": false } } diff --git a/deploy/glm52_35bpw/profile.py b/deploy/glm52_35bpw/profile.py index 59b15f9..e22fe33 100644 --- a/deploy/glm52_35bpw/profile.py +++ b/deploy/glm52_35bpw/profile.py @@ -12,6 +12,7 @@ import copy import json import re +import warnings from pathlib import Path, PurePosixPath from typing import Any, Iterable @@ -49,6 +50,35 @@ class ProfileTransformError(DeploymentContractError): _SHA256_RE = re.compile(r"[0-9a-f]{64}\Z") _PORT_ENVIRONMENT_RE = re.compile(r"(?:^|_)PORT\d*\Z") +_LEGACY_CUDA_RESTORE_WARNING_EMITTED = False + + +def _resolve_compat_option( + canonical: Any, + legacy: Any, + *, + canonical_name: str, + legacy_name: str, + default: Any, +) -> Any: + global _LEGACY_CUDA_RESTORE_WARNING_EMITTED + if canonical is not None and legacy is not None and canonical != legacy: + raise ProfileTransformError( + f"conflicting {canonical_name} and legacy alias {legacy_name}" + ) + if canonical is not None: + return canonical + if legacy is not None: + if not _LEGACY_CUDA_RESTORE_WARNING_EMITTED: + _LEGACY_CUDA_RESTORE_WARNING_EMITTED = True + warnings.warn( + "SparkCache native-restore profile names are deprecated; use" + " SparkCache CUDA restore names", + FutureWarning, + stacklevel=3, + ) + return legacy + return default def _require_sha256(value: str, role: str) -> str: @@ -294,14 +324,55 @@ def build_kv_transfer_config( streaming_native_library: str | None = None, streaming_native_library_sha256: str | None = None, streaming_timing: bool = False, - native_restore: bool = False, + cuda_restore: bool | None = None, + cuda_placement_library: str | None = None, + cuda_placement_library_sha256: str | None = None, + cuda_placement_arena_bytes: int | None = None, + cuda_restore_io_workers: int | None = None, + native_restore: bool | None = None, native_restore_library: str | None = None, native_restore_library_sha256: str | None = None, - native_restore_arena_bytes: int = 128 * 1024 * 1024, - native_restore_io_workers: int = 8, + native_restore_arena_bytes: int | None = None, + native_restore_io_workers: int | None = None, ) -> dict[str, Any]: """Build the complete connector argument for one deployment instance.""" + cuda_restore = _resolve_compat_option( + cuda_restore, + native_restore, + canonical_name="cuda_restore", + legacy_name="native_restore", + default=False, + ) + cuda_placement_library = _resolve_compat_option( + cuda_placement_library, + native_restore_library, + canonical_name="cuda_placement_library", + legacy_name="native_restore_library", + default=None, + ) + cuda_placement_library_sha256 = _resolve_compat_option( + cuda_placement_library_sha256, + native_restore_library_sha256, + canonical_name="cuda_placement_library_sha256", + legacy_name="native_restore_library_sha256", + default=None, + ) + cuda_placement_arena_bytes = _resolve_compat_option( + cuda_placement_arena_bytes, + native_restore_arena_bytes, + canonical_name="cuda_placement_arena_bytes", + legacy_name="native_restore_arena_bytes", + default=128 * 1024 * 1024, + ) + cuda_restore_io_workers = _resolve_compat_option( + cuda_restore_io_workers, + native_restore_io_workers, + canonical_name="cuda_restore_io_workers", + legacy_name="native_restore_io_workers", + default=8, + ) + checkpoint_sha256 = _require_sha256(checkpoint_sha256, "checkpoint_sha256") cache_root = _cache_root(cache_root) extra: dict[str, Any] = { @@ -313,7 +384,7 @@ def build_kv_transfer_config( "spark_cache_restore": True, "spark_cache_scheduler_probe": "none", "spark_cache_streaming_snapshots": bool(streaming_snapshots), - "spark_cache_native_restore": bool(native_restore), + "spark_cache_cuda_restore": bool(cuda_restore), "spark_cache_max_bytes": MAX_BYTES, "spark_cache_low_watermark_bytes": LOW_WATERMARK_BYTES, "spark_cache_ttl_seconds": TTL_SECONDS, @@ -341,48 +412,55 @@ def build_kv_transfer_config( "spark_cache_streaming_timing": int(bool(streaming_timing)), } ) - elif any( - value is not None - for value in (streaming_native_library, streaming_native_library_sha256) - ) or streaming_timing: + elif ( + any( + value is not None + for value in (streaming_native_library, streaming_native_library_sha256) + ) + or streaming_timing + ): raise ProfileTransformError( "streaming native options require streaming_snapshots=True" ) - if native_restore: - if native_restore_library is None or native_restore_library_sha256 is None: + if cuda_restore: + if cuda_placement_library is None or cuda_placement_library_sha256 is None: raise ProfileTransformError( - "native restore requires its placement library path and SHA-256" + "SparkCache CUDA restore requires its placement library" + " path and SHA-256" ) - if native_restore_arena_bytes not in { + if cuda_placement_arena_bytes not in { 64 * 1024 * 1024, 128 * 1024 * 1024, 256 * 1024 * 1024, }: raise ProfileTransformError( - "native_restore_arena_bytes must be 64, 128, or 256 MiB" + "cuda_placement_arena_bytes must be 64, 128, or 256 MiB" ) - if not 1 <= native_restore_io_workers <= 32: - raise ProfileTransformError("native_restore_io_workers must be in [1, 32]") + if not 1 <= cuda_restore_io_workers <= 32: + raise ProfileTransformError("cuda_restore_io_workers must be in [1, 32]") extra.update( { - "spark_cache_native_library": _deployment_path( - native_restore_library, "native_restore_library" + "spark_cache_cuda_placement_library": _deployment_path( + cuda_placement_library, "cuda_placement_library" ), - "spark_cache_native_library_sha256": _require_sha256( - native_restore_library_sha256, - "native_restore_library_sha256", + "spark_cache_cuda_placement_library_sha256": _require_sha256( + cuda_placement_library_sha256, + "cuda_placement_library_sha256", ), - "spark_cache_native_arena_bytes": native_restore_arena_bytes, - "spark_cache_native_io_workers": native_restore_io_workers, + "spark_cache_cuda_placement_arena_bytes": (cuda_placement_arena_bytes), + "spark_cache_cuda_restore_io_workers": cuda_restore_io_workers, } ) elif any( value is not None - for value in (native_restore_library, native_restore_library_sha256) + for value in ( + cuda_placement_library, + cuda_placement_library_sha256, + ) ): raise ProfileTransformError( - "native restore library options require native_restore=True" + "SparkCache CUDA placement library options require cuda_restore=True" ) return { @@ -406,9 +484,7 @@ def transform_vllm_args( arguments = _vllm_args(list(source_command)) if len(arguments) < 2 or arguments[0] != "serve" or arguments[1].startswith("-"): - raise ProfileTransformError( - "source command must begin with 'serve MODEL_PATH'" - ) + raise ProfileTransformError("source command must begin with 'serve MODEL_PATH'") api_port = _port(api_port, "api_port") master_port = _port(master_port, "master_port") effective_api_port = api_port or _single_existing_port(arguments, "--port") @@ -473,12 +549,8 @@ def _transform_environment( required = { "SPARKRING_ATTEST_MODEL_REPOSITORY": str(PROFILE["model"]["repository"]), "SPARKRING_ATTEST_MODEL_REVISION": str(PROFILE["model"]["revision"]), - "SPARKRING_ATTEST_MODEL_CONFIG_SHA256": str( - PROFILE["model"]["config_sha256"] - ), - "SPARKRING_ATTEST_MODEL_INDEX_SHA256": str( - PROFILE["model"]["index_sha256"] - ), + "SPARKRING_ATTEST_MODEL_CONFIG_SHA256": str(PROFILE["model"]["config_sha256"]), + "SPARKRING_ATTEST_MODEL_INDEX_SHA256": str(PROFILE["model"]["index_sha256"]), "KV_FP8_ROPE": "1", "VLLM_NVFP4_MLA_DYNAMIC_SCALE": "1", "VLLM_EXL3_PREFILL_CAPACITY": str(serving["max_num_batched_tokens"]), @@ -489,9 +561,7 @@ def _transform_environment( "VLLM_SPARK_MTP_ADAPTIVE_WINDOW": "0", "VLLM_SPARK_TRUE_ADAPTIVE_DRAFT": "0", "VLLM_B12X_MLA_CKV_GATHER": "1", - "VLLM_B12X_MLA_CKV_GATHER_MAX_TOKENS": str( - serving["max_model_len"] - ), + "VLLM_B12X_MLA_CKV_GATHER_MAX_TOKENS": str(serving["max_model_len"]), "VLLM_SPARK_TP4_MODE": "custom", "SPARK_TP4_LIBRARY": "/opt/sparkring/spark_transport/" "libspark_transport_capi.so", @@ -509,9 +579,10 @@ def _transform_environment( raise ProfileTransformError( f"source GLM-5.2 serving recipe R7 entrypoint would unset required variable {name}" ) - public_q40 = environment.get( - "SPARK_Q40_EXACT_STATE_EXPECTED_EXL3_SHA256" - ) == "8fad5330c88f55dc57e4d8e298f2af23e16390b97153b569a2e572e0fb5065c2" + public_q40 = ( + environment.get("SPARK_Q40_EXACT_STATE_EXPECTED_EXL3_SHA256") + == "8fad5330c88f55dc57e4d8e298f2af23e16390b97153b569a2e572e0fb5065c2" + ) if not public_q40 or "SPARK_Q35_EXACT_STATE_EXPECTED_EXL3_SHA256" in environment: raise ProfileTransformError( "source GLM-5.2 serving recipe R7 environment must select the public target-only" @@ -558,11 +629,16 @@ def transform_inspection( streaming_native_library: str | None = None, streaming_native_library_sha256: str | None = None, streaming_timing: bool = False, - native_restore: bool = False, + cuda_restore: bool | None = None, + cuda_placement_library: str | None = None, + cuda_placement_library_sha256: str | None = None, + cuda_placement_arena_bytes: int | None = None, + cuda_restore_io_workers: int | None = None, + native_restore: bool | None = None, native_restore_library: str | None = None, native_restore_library_sha256: str | None = None, - native_restore_arena_bytes: int = 128 * 1024 * 1024, - native_restore_io_workers: int = 8, + native_restore_arena_bytes: int | None = None, + native_restore_io_workers: int | None = None, api_port: int | None = None, master_port: int | None = None, ) -> dict[str, Any]: @@ -605,6 +681,11 @@ def transform_inspection( streaming_native_library=streaming_native_library, streaming_native_library_sha256=streaming_native_library_sha256, streaming_timing=streaming_timing, + cuda_restore=cuda_restore, + cuda_placement_library=cuda_placement_library, + cuda_placement_library_sha256=cuda_placement_library_sha256, + cuda_placement_arena_bytes=cuda_placement_arena_bytes, + cuda_restore_io_workers=cuda_restore_io_workers, native_restore=native_restore, native_restore_library=native_restore_library, native_restore_library_sha256=native_restore_library_sha256, diff --git a/deploy/glm53_flash/IMAGE_ANNOUNCEMENT.md b/deploy/glm53_flash/IMAGE_ANNOUNCEMENT.md index ee2051e..f34a981 100644 --- a/deploy/glm53_flash/IMAGE_ANNOUNCEMENT.md +++ b/deploy/glm53_flash/IMAGE_ANNOUNCEMENT.md @@ -182,7 +182,7 @@ ENTRYPOINT=["vllm"] BF16, seven speculative tokens, draft TP4, CC BY-NC-ND 4.0. - KV mode: 12 GiB FP8 GPU KV per rank; measured capacity 549,950 tokens. - External cache: SparkCache maximum 48 GiB and low watermark 40 GiB per - rank; rank-local NVMe; native direct restore and streaming disabled. + rank; rank-local NVMe; SparkCache direct CUDA restore and streaming disabled. - Graphs: target `FULL_AND_PIECEWISE`, DFlash FULL, capture sizes 8, 16, 32, 64, 128, and 256. - Scheduler: asynchronous scheduling, chunked prefill, native prefix caching, @@ -238,7 +238,7 @@ performance baseline is implied. ## Known limitations - MTP drafting, other checkpoints, other topologies, spans over 8,192 tokens, - native direct restore, streaming snapshots, throughput, and soak behavior: + SparkCache direct CUDA restore, streaming snapshots, throughput, and soak behavior: `Not tested`. - Exact semantic output was not established by the historical suffix-only receipt. Requalification must use the exact-content qualifier. diff --git a/deploy/glm53_flash/README.md b/deploy/glm53_flash/README.md index 24b341d..bb3dede 100644 --- a/deploy/glm53_flash/README.md +++ b/deploy/glm53_flash/README.md @@ -10,7 +10,7 @@ records: records an 8,192-token persistent restore through the Python page-placement path at 147.2--194.0 ms per rank. - [`GLM53_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md`](../../GLM53_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md) - records native direct restore of a 131,072-token prefix, multi-group + records SparkCache direct CUDA restore of a 131,072-token prefix, multi-group recovery, and bounded shared GPU-prefix reuse through C16. Qualification applies only to the checkpoint revisions, source contracts, @@ -195,14 +195,15 @@ token incomplete for retry; it does not delay model serving indefinitely. See [`sparkcache/README.md`](../../sparkcache/README.md#one-shot-cache-clear) for token, root-path, and deletion-scope rules. -Native page restore is disabled unless the launch supplies all of: +SparkCache CUDA restore is disabled unless the launch supplies all of: -- `SPARK_CONTEXT_CACHE_NATIVE_RESTORE=1`; -- an attested `SPARK_CONTEXT_CACHE_NATIVE_LIBRARY` path; -- its `SPARK_CONTEXT_CACHE_NATIVE_LIBRARY_SHA256`; -- a 64, 128, or 256 MiB `SPARK_CONTEXT_CACHE_NATIVE_ARENA_BYTES` value. +- `SPARK_CONTEXT_CACHE_CUDA_RESTORE=1`; +- an attested `SPARK_CONTEXT_CACHE_CUDA_PLACEMENT_LIBRARY` path; +- its `SPARK_CONTEXT_CACHE_CUDA_PLACEMENT_LIBRARY_SHA256`; +- a 64, 128, or 256 MiB + `SPARK_CONTEXT_CACHE_CUDA_PLACEMENT_ARENA_BYTES` value. -The qualified 128K runtime used two host restore workers, two native placement +The qualified 128K runtime used two host restore workers, two SparkCache CUDA placement lanes, and two 256 MiB mapped-host arenas per rank. Streaming snapshots remain unsupported for opaque page storage. @@ -233,12 +234,12 @@ python -m deploy.glm53_flash.concurrency_benchmark \ ``` The default fixture reproduces the recorded 131,072-token persistent prefix. -See the native restore validation record for exact runtime identities, results, +See the SparkCache CUDA restore validation record for exact runtime identities, results, and committed receipts. ## Compatibility -The native placement path, longest exact-boundary search, and shared GPU lease +The SparkCache CUDA placement path, longest exact-boundary search, and shared GPU lease do not change `CacheIdentity`, digest values, 256-token logical geometry, exact manifest format, or existing chunk bytes. Missing or incompatible state remains a cache miss followed by ordinary computation. diff --git a/deploy/glm53_flash/concurrency_benchmark.py b/deploy/glm53_flash/concurrency_benchmark.py index 859f475..aa170a4 100644 --- a/deploy/glm53_flash/concurrency_benchmark.py +++ b/deploy/glm53_flash/concurrency_benchmark.py @@ -40,7 +40,7 @@ class BenchmarkConfig: scenario: str cache_state: str pretokenize: bool = False - prefix_header: str = "Native 128K restore test.\n" + prefix_header: str = "SparkCache CUDA 128K restore test.\n" prefix_repetitions: int = 131_072 tail_repetitions: int = 32 max_tokens: int = 1 @@ -271,7 +271,9 @@ def _parser() -> argparse.ArgumentParser: ) parser.add_argument("--endpoint", required=True) parser.add_argument("--model", required=True) - parser.add_argument("--concurrency", type=int, choices=CONCURRENCY_LEVELS, required=True) + parser.add_argument( + "--concurrency", type=int, choices=CONCURRENCY_LEVELS, required=True + ) parser.add_argument("--scenario", choices=SCENARIOS, required=True) parser.add_argument("--cache-state", choices=CACHE_STATES, required=True) parser.add_argument( @@ -280,7 +282,9 @@ def _parser() -> argparse.ArgumentParser: help="tokenize each unique chat prompt before starting timed completions", ) parser.add_argument("--output", type=Path, required=True) - parser.add_argument("--prefix-header", default="Native 128K restore test.\n") + parser.add_argument( + "--prefix-header", default="SparkCache CUDA 128K restore test.\n" + ) parser.add_argument("--prefix-repetitions", type=int, default=131_072) parser.add_argument("--tail-repetitions", type=int, default=32) parser.add_argument("--max-tokens", type=int, default=1) diff --git a/deploy/glm53_flash/profile.py b/deploy/glm53_flash/profile.py index 9facc8d..f09fd06 100644 --- a/deploy/glm53_flash/profile.py +++ b/deploy/glm53_flash/profile.py @@ -113,7 +113,7 @@ def build_kv_transfer_config( "spark_cache_restore": True, "spark_cache_scheduler_probe": "none", "spark_cache_streaming_snapshots": False, - "spark_cache_native_restore": False, + "spark_cache_cuda_restore": False, "spark_cache_max_bytes": max_bytes, "spark_cache_low_watermark_bytes": low_watermark_bytes, "spark_cache_ttl_seconds": 0, diff --git a/deploy/glm53_flash/test_concurrency_benchmark.py b/deploy/glm53_flash/test_concurrency_benchmark.py index bfa775d..9ccc810 100644 --- a/deploy/glm53_flash/test_concurrency_benchmark.py +++ b/deploy/glm53_flash/test_concurrency_benchmark.py @@ -36,7 +36,7 @@ def test_prompt_shapes_are_stable_and_distinguish_scenarios() -> None: assert identical[0] == identical[1] assert shared[0] != shared[1] assert all( - prompt.startswith("Native 128K restore test.\n" + "benchmark " * 4) + prompt.startswith("SparkCache CUDA 128K restore test.\n" + "benchmark " * 4) for prompt in shared ) assert "tail-00 tail-00" in shared[0] diff --git a/docs/sparkcache-prefix-explainer.html b/docs/sparkcache-prefix-explainer.html index 06a4a58..165a18a 100644 --- a/docs/sparkcache-prefix-explainer.html +++ b/docs/sparkcache-prefix-explainer.html @@ -532,7 +532,7 @@
The 16-request qualification used vLLM --max-num-seqs 32. Every request completed, each rank performed one external restore, and the semantic canary passed.
Unrelated-cold C16 and decode-interference remain unqualified. See the native restore and concurrency validation for exact runtime identities and receipts.
+Unrelated-cold C16 and decode-interference remain unqualified. See the SparkCache CUDA restore and concurrency validation for exact runtime identities and receipts.
diff --git a/pyproject.toml b/pyproject.toml index b848fb6..7dda18a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ license-files = ["LICENSE"] requires-python = ">=3.11" # The storage engine, codec, replication protocol, and streaming state # machines are dependency-free. torch is required only by the connector and -# native-restore modules; vLLM is required only at serve time and is +# SparkCache CUDA restore modules; vLLM is required only at serve time and is # deliberately not a package dependency (see README: runtime pinning). dependencies = [] classifiers = [ diff --git a/sparkcache/README.md b/sparkcache/README.md index 36d9534..22576e7 100644 --- a/sparkcache/README.md +++ b/sparkcache/README.md @@ -36,8 +36,8 @@ SparkCache reads and writes only the rank's local filesystem. | `spark_context_cache_codec.py` | DCP row ownership, record packing, and digest helpers for per-token storage | | `spark_context_cache_hybrid.py` | opaque HMA page encoding and topology validation | | `persistent_context_cache/cache_manifest.py` | `ManifestStore`; exact manifests, row-prefix aliases, durable publication, lookup, restore, invalidation, and maintenance | -| `spark_context_cache_native_placement.py` | `NativePlacementAdapter`; attested CUDA placement transaction | -| `spark_context_cache_native_restore.py` | bounded read/hash/slab orchestration for native placement | +| `spark_context_cache_cuda_placement.py` | `CudaPlacementAdapter`; attested SparkCache CUDA placement transaction | +| `spark_context_cache_cuda_restore.py` | bounded read/hash/slab orchestration for SparkCache CUDA placement | | `spark_context_cache_native_hybrid_restore.py` | authenticated direct reads and multi-slab mapped-arena placement for opaque HMA pages | | `spark_context_cache_restore_timing.py` | `sparkcache-restore-timing/v1` asynchronous restore records | | `streaming/factory.py` | scheduler and worker adapters for write-behind publication | @@ -92,6 +92,21 @@ mode `block_pages_v1` maps the same operator value to the page-delta namespace. Both values are part of cache identity and therefore cleanly miss `snapshot-v1` entries. Streaming-snapshot deployments reject the option. +SparkCache CUDA restore uses these optional connector settings: + +- `spark_cache_cuda_restore`; +- `spark_cache_cuda_placement_library` and + `spark_cache_cuda_placement_library_sha256`; +- `spark_cache_cuda_placement_arena_bytes`; and +- `spark_cache_cuda_restore_io_workers`. + +The equivalent environment variables begin with +`SPARK_CONTEXT_CACHE_CUDA_`. Legacy `native` configuration, environment, CLI, +and profile names remain accepted as compatibility aliases. A legacy-only +configuration warns once per process. Supplying canonical and legacy values +that disagree rejects startup. Generated configurations use only the CUDA +names. The terminology change does not alter cache identity or stored bytes. + ## Storage and integrity `ManifestStore` publishes files in this order: @@ -156,7 +171,7 @@ completion marker. Process-local and file-lock acquisition share one 30-second budget. A lock timeout or filesystem error leaves the requested token incomplete and disables -persistent store, restore, streaming publication, and native restore for that +persistent store, restore, streaming publication, and SparkCache CUDA restore for that connector process. Model serving can continue without persistent cache use, and a later startup retries the same token. @@ -225,7 +240,7 @@ when placement completes and intentionally excludes that bookkeeping. ## Optional paths -- **Native direct restore — implemented.** Requires the checksum-attested +- **SparkCache direct CUDA restore — implemented.** Requires the checksum-attested `libspark_cache_placement` artifact and remains disabled unless the launch supplies its path, SHA-256, and a supported mapped-host arena size. The `glm53-flash-hybrid` profile supports authenticated multi-slab page restore; @@ -250,7 +265,7 @@ and chunk geometry. GLM-5.3 Flash opaque pages are qualified at TP4/DCP1 with BF16 DFlash2 using seven draft tokens. Source revision -`2b86fb9d02fa3595cca5caa864b81aedce44b8bb` qualifies native direct restore, +`2b86fb9d02fa3595cca5caa864b81aedce44b8bb` qualifies SparkCache direct CUDA restore, multi-group recovery, and shared GPU-prefix reuse through C16 under a 32-sequence scheduler ceiling. Sparse row-prefix aliases have GPU-free coverage but no live model-serving qualification. @@ -263,8 +278,9 @@ See: TP4/DCP4 evidence; - `../GLM53_FLASH_DFLASH7_LIVE_VALIDATION.md` for the GLM-5.3 Python page-placement record; -- `../GLM53_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md` for GLM-5.3 native - restore, recovery, and C2/C8/C16 shared-prefix evidence; +- `../GLM53_NATIVE_RESTORE_PERFORMANCE_VALIDATION.md` for GLM-5.3 SparkCache + CUDA restore, recovery, and C2/C8/C16 shared-prefix evidence. The historical + filename remains stable for existing links; - `../deploy/deepseek_v4/DCP_SUPPORT.md` for the HMA DCP limitation; and - `../ROADMAP.md` for research-only and unsupported work. diff --git a/sparkcache/native/README.md b/sparkcache/native/README.md index 307f7df..afb4b25 100644 --- a/sparkcache/native/README.md +++ b/sparkcache/native/README.md @@ -1,4 +1,4 @@ -# Native placement and snapshot libraries +# SparkCache CUDA placement and snapshot libraries `libspark_cache_placement.so` implements checksum-gated direct restore into registered cache tensors. It is **implemented** and opt-in; qualified @@ -8,10 +8,10 @@ checksum-bound four-rank integration gate cover this library. `libspark_cache_snapshot.so` implements the store-side gather ring documented in [`SNAPSHOT_RING_STATE_MODEL.md`](SNAPSHOT_RING_STATE_MODEL.md). It is -**research-only** and uses a separate fail-open ABI so optional snapshot -publication cannot weaken fail-closed restore placement. +**research-only** and uses a separate best-effort ABI. Optional snapshot +publication cannot make unverified state eligible for restoration. -The native restore path removes Python object construction and host-side +The SparkCache CUDA restore path removes Python object construction and host-side record transposition from large restores. ## Why this path exists @@ -36,7 +36,7 @@ scatter operations. The vectorized Python fallback beside this directory reduces assembly to about 0.4 seconds on the development CPU, but it still transposes all 3.14 GB in host memory before placement. -The native direct path removes that entire middle representation: +The SparkCache direct CUDA path removes that entire middle representation: ```text pread encoded .spcc files directly into cudaHostAllocMapped arena @@ -51,7 +51,7 @@ layer-wise scatter exists on this path. ## Canonical Python/ctypes boundary -[`../spark_cache_native.py`](../spark_cache_native.py) is the single Python ABI +[`../spark_cache_cuda.py`](../spark_cache_cuda.py) is the canonical Python ABI declaration. The `sparkcache.native.python` package re-exports that module; it does not maintain a second copy. Do not independently declare these structures in probes or connector modules. `load_library()` first calls @@ -60,17 +60,17 @@ version, arena/record constants, structure sizes, or required capabilities do not match. Model-serving restore orchestration uses the attested adapter in -[`../spark_context_cache_native_placement.py`](../spark_context_cache_native_placement.py): +[`../spark_context_cache_cuda_placement.py`](../spark_context_cache_cuda_placement.py): ```python -from sparkcache.spark_context_cache_native_placement import ( +from sparkcache.spark_context_cache_cuda_placement import ( ArenaMode, - NativePlacementAdapter, - NativePlacementLibrary, + CudaPlacementAdapter, + CudaPlacementLibrary, ) -library = NativePlacementLibrary.load(path, expected_sha256=digest) -adapter = NativePlacementAdapter.create( +library = CudaPlacementLibrary.load(path, expected_sha256=digest) +adapter = CudaPlacementAdapter.create( library, arena_mode=ArenaMode.MAPPED_HOST, arena_bytes=arena_bytes, @@ -81,7 +81,7 @@ adapter = NativePlacementAdapter.create( ) ``` -`execute_native_restore()` owns multi-file slab packing, complete-file digest +`execute_cuda_restore()` owns multi-file slab packing, complete-file digest verification, arena lifetime, direct-slab submission, and parked-request completion. `ParkedRestore.finish()` is the only success edge that permits the requester to resume; any exception aborts the transaction and recomputes the @@ -92,6 +92,12 @@ for failures before `create()` returns a handle, and an in-flight statistics snapshot. Python therefore gets complete diagnostics without retaining a borrowed C string. +The modules and symbols containing `native` remain compatibility interfaces. +Canonical imports use `spark_cache_cuda`, +`spark_context_cache_cuda_placement`, `spark_context_cache_cuda_restore`, and +`spark_context_cache_cuda_page_restore`. The compatibility names alias the +same classes and functions; they do not introduce a second implementation. + ## Implemented interfaces [`include/spark_cache_placement.h`](include/spark_cache_placement.h) defines a @@ -102,7 +108,7 @@ fixed C ABI: - one uploaded `uint32` physical-slot vector per restore; - two reusable 64, 128, or 256 MiB arenas; - direct encoded-chunk and transposed-slab submission modes; -- fail-closed begin/acquire/submit/finish transaction states. +- verified-or-recompute begin/acquire/submit/finish transaction states. The destination descriptor stores a current runtime pointer, capacity, stride, record kind, byte width, and source layer ordinal. No physical slot @@ -305,7 +311,7 @@ Admission gates: - no memory-growth trend across ten restores; - concurrent serving never stalls behind the parked requester; - warm read+verify remains at or below 300 ms p95; -- native placement is at or below 500 ms p95 on the slowest rank; +- SparkCache CUDA placement is at or below 500 ms p95 on the slowest rank; - complete warm restore is at or below 1.2 seconds p95, with a 1.5-second initial acceptance ceiling. @@ -327,6 +333,6 @@ environment flag and an attested library hash. The validation order is: eight-request aggregate decode, cancellation, and restart gates; 6. only then make direct mapped placement the default. -The native direct-placement path shortens the restored requester's own wait. +The SparkCache direct CUDA placement path shortens the restored requester's own wait. The asynchronous scheduler contract keeps that requester-specific wait from blocking unrelated requests. diff --git a/sparkcache/native/SNAPSHOT_BYTE_COMPARISON.md b/sparkcache/native/SNAPSHOT_BYTE_COMPARISON.md index 54e5cdc..7ad94b1 100644 --- a/sparkcache/native/SNAPSHOT_BYTE_COMPARISON.md +++ b/sparkcache/native/SNAPSHOT_BYTE_COMPARISON.md @@ -199,7 +199,7 @@ record mask is `0b011`. A separate `mtp_draft_kv` source would contradict the qualified registration contract and double-count drafter state. The 368- and 132-byte source strides are the tightly packed registered-tensor -contract used by native placement. The 512/256 strides in the small fixture +contract used by SparkCache CUDA placement. The 512/256 strides in the small fixture above are intentional padding stress, not claimed model-serving strides. When the source-table exporter is wired in, it must derive and attest `tensor.stride(1) * tensor.element_size()` and fail closed unless it agrees diff --git a/sparkcache/native/python/__init__.py b/sparkcache/native/python/__init__.py index cc6a564..53b9f80 100644 --- a/sparkcache/native/python/__init__.py +++ b/sparkcache/native/python/__init__.py @@ -1,3 +1,3 @@ -"""Python helpers for native SparkCache placement and snapshot libraries.""" +"""Python helpers for SparkCache CUDA placement and snapshot libraries.""" -from sparkcache.spark_cache_native import * # noqa: F401,F403 +from sparkcache.spark_cache_cuda import * # noqa: F401,F403 diff --git a/sparkcache/spark_cache_cuda.py b/sparkcache/spark_cache_cuda.py new file mode 100644 index 0000000..8993f80 --- /dev/null +++ b/sparkcache/spark_cache_cuda.py @@ -0,0 +1,4 @@ +"""Canonical Python binding for the SparkCache CUDA placement ABI.""" + +from sparkcache.spark_cache_native import * # noqa: F403 +from sparkcache.spark_cache_native import __all__ # noqa: F401 diff --git a/sparkcache/spark_cache_native.py b/sparkcache/spark_cache_native.py index 742f063..f00dbe9 100644 --- a/sparkcache/spark_cache_native.py +++ b/sparkcache/spark_cache_native.py @@ -1,4 +1,4 @@ -"""Strict ctypes binding for the native SparkCache placement ABI. +"""Strict ctypes binding for the SparkCache CUDA placement ABI. This module is intentionally dependency-free. The full-artifact gate may use ``arena_memoryview`` with ``os.preadv`` to fill cudaHostAllocMapped memory @@ -436,6 +436,9 @@ def pread_exact(fd: int, target: memoryview, offset: int = 0) -> None: consumed += count +CudaPlacementError = NativePlacementError + + __all__ = [ "ABI_VERSION", "ARENA_COUNT", @@ -446,6 +449,7 @@ def pread_exact(fd: int, target: memoryview, offset: int = 0) -> None: "AbiInfo", "ArenaView", "ChunkDescriptor", + "CudaPlacementError", "DestinationDescriptor", "NativePlacementError", "PlacementConfig", diff --git a/sparkcache/spark_context_cache_config.py b/sparkcache/spark_context_cache_config.py index 6feb86f..8a83a16 100644 --- a/sparkcache/spark_context_cache_config.py +++ b/sparkcache/spark_context_cache_config.py @@ -20,10 +20,12 @@ import json import os import re +import threading +import warnings from dataclasses import dataclass from pathlib import Path from types import MappingProxyType -from typing import Any, Mapping, Sequence +from typing import Any, Callable, Mapping, Sequence from sparkcache.spark_context_cache_profiles import ( ProfileError, @@ -41,9 +43,80 @@ ) SHA256_RE = re.compile(r"[0-9a-f]{64}\Z") -_NATIVE_ARENA_BYTES = frozenset( +_CUDA_PLACEMENT_ARENA_BYTES = frozenset( {64 * 1024 * 1024, 128 * 1024 * 1024, 256 * 1024 * 1024} ) +_MISSING = object() +_LEGACY_CUDA_RESTORE_WARNING_LOCK = threading.Lock() +_LEGACY_CUDA_RESTORE_WARNING_EMITTED = False + + +def _warn_legacy_cuda_restore_config() -> None: + """Warn once when a process relies only on legacy configuration names.""" + + global _LEGACY_CUDA_RESTORE_WARNING_EMITTED + with _LEGACY_CUDA_RESTORE_WARNING_LOCK: + if _LEGACY_CUDA_RESTORE_WARNING_EMITTED: + return + _LEGACY_CUDA_RESTORE_WARNING_EMITTED = True + warnings.warn( + "SparkCache native-restore configuration names are deprecated; use the" + " SparkCache CUDA restore configuration names", + FutureWarning, + stacklevel=3, + ) + + +def _compat_config_value( + extra: Callable[[str, Any], Any], + *, + canonical_key: str, + legacy_key: str, + canonical_env: str, + legacy_env: str, + default: Any, + normalize: Callable[[Any], Any] = str, +) -> Any: + """Resolve one canonical setting and its compatibility alias.""" + + canonical_extra = extra(canonical_key, _MISSING) + legacy_extra = extra(legacy_key, _MISSING) + canonical_value = ( + canonical_extra + if canonical_extra is not _MISSING + else os.environ.get(canonical_env, _MISSING) + ) + legacy_value = ( + legacy_extra + if legacy_extra is not _MISSING + else os.environ.get(legacy_env, _MISSING) + ) + if canonical_value is not _MISSING and legacy_value is not _MISSING: + try: + disagree = normalize(canonical_value) != normalize(legacy_value) + except (TypeError, ValueError) as error: + raise RuntimeError( + "spark-context-cache: conflicting SparkCache CUDA restore" + f" settings {canonical_key} and legacy alias {legacy_key}" + ) from error + if disagree: + raise RuntimeError( + "spark-context-cache: conflicting SparkCache CUDA restore" + f" settings {canonical_key} and legacy alias {legacy_key}" + ) + return canonical_value + if canonical_value is not _MISSING: + return canonical_value + if legacy_value is not _MISSING: + _warn_legacy_cuda_restore_config() + return legacy_value + return default + + +def _config_bool(value: Any) -> bool: + if value in (1, "1", True, "true"): + return True + return False def _nonnegative_config_int(value: Any, label: str) -> int: @@ -226,16 +299,46 @@ class ConnectorConfig: store_enabled: bool restore_enabled: bool streaming_snapshots_enabled: bool - native_restore_enabled: bool - native_library_path: str - native_library_sha256: str - native_arena_bytes: int - native_io_workers: int + cuda_restore_enabled: bool + cuda_placement_library_path: str + cuda_placement_library_sha256: str + cuda_placement_arena_bytes: int + cuda_restore_io_workers: int scheduler_probe: str identity_base: Mapping[str, Any] load_thread_limit: int max_pending_restores: int + @property + def native_restore_enabled(self) -> bool: + """Compatibility alias for :attr:`cuda_restore_enabled`.""" + + return self.cuda_restore_enabled + + @property + def native_library_path(self) -> str: + """Compatibility alias for the CUDA placement library path.""" + + return self.cuda_placement_library_path + + @property + def native_library_sha256(self) -> str: + """Compatibility alias for the CUDA placement library digest.""" + + return self.cuda_placement_library_sha256 + + @property + def native_arena_bytes(self) -> int: + """Compatibility alias for the CUDA placement arena size.""" + + return self.cuda_placement_arena_bytes + + @property + def native_io_workers(self) -> int: + """Compatibility alias for the CUDA restore I/O worker count.""" + + return self.cuda_restore_io_workers + def build_identity(self, shard_rank: int, tp_shard_rank: int) -> CacheIdentity: """Construct a :class:`CacheIdentity` for a DCP shard rank. @@ -422,68 +525,98 @@ def parse_connector_config( "spark-context-cache: tail-cow-v1 publication does not support" " streaming snapshots" ) - native_restore_enabled = extra( - "spark_cache_native_restore", - os.environ.get("SPARK_CONTEXT_CACHE_NATIVE_RESTORE", "0"), - ) in (1, "1", True, "true") - native_library_path = str( - extra( - "spark_cache_native_library", - os.environ.get("SPARK_CONTEXT_CACHE_NATIVE_LIBRARY", ""), + cuda_restore_enabled = _config_bool( + _compat_config_value( + extra, + canonical_key="spark_cache_cuda_restore", + legacy_key="spark_cache_native_restore", + canonical_env="SPARK_CONTEXT_CACHE_CUDA_RESTORE", + legacy_env="SPARK_CONTEXT_CACHE_NATIVE_RESTORE", + default="0", + normalize=_config_bool, + ) + ) + cuda_placement_library_path = str( + _compat_config_value( + extra, + canonical_key="spark_cache_cuda_placement_library", + legacy_key="spark_cache_native_library", + canonical_env="SPARK_CONTEXT_CACHE_CUDA_PLACEMENT_LIBRARY", + legacy_env="SPARK_CONTEXT_CACHE_NATIVE_LIBRARY", + default="", ) or "" ) - native_library_sha256 = str( - extra( - "spark_cache_native_library_sha256", - os.environ.get("SPARK_CONTEXT_CACHE_NATIVE_LIBRARY_SHA256", ""), + cuda_placement_library_sha256 = str( + _compat_config_value( + extra, + canonical_key="spark_cache_cuda_placement_library_sha256", + legacy_key="spark_cache_native_library_sha256", + canonical_env="SPARK_CONTEXT_CACHE_CUDA_PLACEMENT_LIBRARY_SHA256", + legacy_env="SPARK_CONTEXT_CACHE_NATIVE_LIBRARY_SHA256", + default="", ) or "" ) - native_arena_raw = str( - extra( - "spark_cache_native_arena_bytes", - os.environ.get("SPARK_CONTEXT_CACHE_NATIVE_ARENA_BYTES", ""), + cuda_placement_arena_raw = str( + _compat_config_value( + extra, + canonical_key="spark_cache_cuda_placement_arena_bytes", + legacy_key="spark_cache_native_arena_bytes", + canonical_env="SPARK_CONTEXT_CACHE_CUDA_PLACEMENT_ARENA_BYTES", + legacy_env="SPARK_CONTEXT_CACHE_NATIVE_ARENA_BYTES", + default="", + normalize=int, ) or "" ) try: - native_arena_bytes = int(native_arena_raw) if native_arena_raw else 0 + cuda_placement_arena_bytes = ( + int(cuda_placement_arena_raw) if cuda_placement_arena_raw else 0 + ) except ValueError as error: - if native_restore_enabled: + if cuda_restore_enabled: raise RuntimeError( - "spark-context-cache: native restore requires an integer arena size" + "spark-context-cache: SparkCache CUDA restore requires an integer" + " placement arena size" ) from error - native_arena_bytes = 0 - native_workers_raw = extra( - "spark_cache_native_io_workers", - os.environ.get("SPARK_CONTEXT_CACHE_NATIVE_IO_WORKERS", "8"), + cuda_placement_arena_bytes = 0 + cuda_restore_workers_raw = _compat_config_value( + extra, + canonical_key="spark_cache_cuda_restore_io_workers", + legacy_key="spark_cache_native_io_workers", + canonical_env="SPARK_CONTEXT_CACHE_CUDA_RESTORE_IO_WORKERS", + legacy_env="SPARK_CONTEXT_CACHE_NATIVE_IO_WORKERS", + default="8", + normalize=int, ) try: - native_io_workers = int(native_workers_raw) + cuda_restore_io_workers = int(cuda_restore_workers_raw) except (TypeError, ValueError) as error: - if native_restore_enabled: + if cuda_restore_enabled: raise RuntimeError( - "spark-context-cache: native restore requires an integer" + "spark-context-cache: SparkCache CUDA restore requires an integer" " IO worker count" ) from error - native_io_workers = 8 - if native_restore_enabled: - library_path = Path(native_library_path) + cuda_restore_io_workers = 8 + if cuda_restore_enabled: + library_path = Path(cuda_placement_library_path) if ( - not native_library_path + not cuda_placement_library_path or not library_path.is_absolute() - or SHA256_RE.fullmatch(native_library_sha256) is None - or native_arena_bytes not in _NATIVE_ARENA_BYTES + or SHA256_RE.fullmatch(cuda_placement_library_sha256) is None + or cuda_placement_arena_bytes not in _CUDA_PLACEMENT_ARENA_BYTES ): raise RuntimeError( - "spark-context-cache: native restore requires an" - " absolute library path, a 64-character lowercase" - " SHA-256, and arena bytes equal to 64, 128, or 256 MiB" + "spark-context-cache: SparkCache CUDA restore requires an" + " absolute CUDA placement library path, a 64-character" + " lowercase SHA-256, and placement arena bytes equal to" + " 64, 128, or 256 MiB" ) - if not 1 <= native_io_workers <= 32: + if not 1 <= cuda_restore_io_workers <= 32: raise RuntimeError( - "spark-context-cache: native restore IO workers must be in [1, 32]" + "spark-context-cache: SparkCache CUDA restore IO workers" + " must be in [1, 32]" ) draft_policy = extra( "spark_cache_draft_policy", @@ -558,7 +691,7 @@ def parse_connector_config( dcp_degree=dcp_degree, block_size=block_size, min_span_tokens=min_span, - native_restore=native_restore_enabled, + cuda_restore=cuda_restore_enabled, ) except ProfileError as error: raise RuntimeError(f"spark-context-cache: {error}") from error @@ -588,7 +721,7 @@ def parse_connector_config( ), ), ) - if native_restore_enabled and storage_mode != "block_pages_v1": + if cuda_restore_enabled and storage_mode != "block_pages_v1": load_thread_limit = 1 max_pending_restores_raw = extra( "spark_cache_max_pending_restores", @@ -623,11 +756,11 @@ def parse_connector_config( store_enabled=store_enabled, restore_enabled=restore_enabled, streaming_snapshots_enabled=streaming_snapshots_enabled, - native_restore_enabled=native_restore_enabled, - native_library_path=native_library_path, - native_library_sha256=native_library_sha256, - native_arena_bytes=native_arena_bytes, - native_io_workers=native_io_workers, + cuda_restore_enabled=cuda_restore_enabled, + cuda_placement_library_path=cuda_placement_library_path, + cuda_placement_library_sha256=cuda_placement_library_sha256, + cuda_placement_arena_bytes=cuda_placement_arena_bytes, + cuda_restore_io_workers=cuda_restore_io_workers, scheduler_probe=scheduler_probe, identity_base=_freeze_config_value(identity_base), load_thread_limit=load_thread_limit, diff --git a/sparkcache/spark_context_cache_connector.py b/sparkcache/spark_context_cache_connector.py index 75483f7..09375bf 100644 --- a/sparkcache/spark_context_cache_connector.py +++ b/sparkcache/spark_context_cache_connector.py @@ -798,7 +798,7 @@ def __init__( } logger.info( "spark-context-cache: role=%s root=%s dcp=%d store=%s restore=%s" - " native_restore=%s max_span=%d load_threads=%d max_bytes=%d" + " cuda_restore=%s max_span=%d load_threads=%d max_bytes=%d" " low_bytes=%d ttl_seconds=%d", role.name, self._root, @@ -826,11 +826,11 @@ def _install_streaming_runtime(self, role: KVConnectorRole) -> None: runtime = factory(self) except Exception as error: raise RuntimeError( - "spark-context-cache: streaming runtime installation failed closed" + "spark-context-cache: streaming runtime installation was rejected" ) from error if runtime is None: raise RuntimeError( - "spark-context-cache: streaming runtime installation failed closed" + "spark-context-cache: streaming runtime installation was rejected" ) if role is KVConnectorRole.SCHEDULER: required = ( @@ -1202,9 +1202,7 @@ def _lease_publication_candidates( _SHARED_PREFIX_LEASE_TTL_SECONDS, ), ) - return ( - (flight.digest, flight.span_tokens, _SHARED_PREFIX_LEASE_TTL_SECONDS), - ) + return ((flight.digest, flight.span_tokens, _SHARED_PREFIX_LEASE_TTL_SECONDS),) def get_shared_prefix_lease_to_publish( self, request: "Request" @@ -1231,8 +1229,7 @@ def shared_prefix_lease_published(self, request_id: str, lease_key: str) -> bool if flight is None or not flight.workers_finished: return False candidates = { - key: span - for key, span, _ in self._lease_publication_candidates(flight) + key: span for key, span, _ in self._lease_publication_candidates(flight) } span = candidates.get(lease_key) if span is None: @@ -1905,23 +1902,26 @@ def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]) -> None: self.discover_manifests() def _configure_native_restore(self) -> None: - """Attest and bind native placement only after final CUDA inventory.""" + """Attest SparkCache CUDA placement after final CUDA inventory.""" if self._native_adapter is not None: raise RuntimeError( - "spark-context-cache: native placement is already configured" + "spark-context-cache: SparkCache CUDA placement is already configured" ) if self._storage_mode == "block_pages_v1": self._configure_native_hybrid_restore() return assert self._plans is not None if not self._plans: - raise RuntimeError("spark-context-cache: native restore has no layer plans") + raise RuntimeError( + "spark-context-cache: SparkCache CUDA restore has no layer plans" + ) first_tensor = self._layer_tensors[self._plans[0].name] device = first_tensor.device if device.type != "cuda": raise RuntimeError( - "spark-context-cache: native restore requires CUDA cache tensors" + "spark-context-cache: SparkCache CUDA restore requires CUDA" + " cache tensors" ) device_ordinal = ( int(device.index) @@ -1950,7 +1950,7 @@ def _configure_native_restore(self) -> None: # stores the full chunk on every rank); an arena that cannot # hold one encoded chunk would fail every restore plan later. raise RuntimeError( - "spark-context-cache: native arena" + "spark-context-cache: SparkCache CUDA placement arena" f" ({self._native_arena_bytes} bytes) cannot hold one" f" encoded chunk (floor {payload_floor} bytes) at" f" dcp_degree={self._dcp_degree}" @@ -1990,26 +1990,26 @@ def _configure_native_restore(self) -> None: if ordinal is None: raise RuntimeError( f"spark-context-cache: record kind {kind} has no" - " native placement ordinal" + " SparkCache CUDA placement ordinal" ) required |= 1 << int(ordinal) native_execute = components.execute_native_restore if not callable(native_execute): - raise TypeError("native restore orchestrator is not callable") + raise TypeError("SparkCache CUDA restore orchestrator is not callable") except Exception as error: if adapter is not None: with contextlib.suppress(Exception): adapter.close() raise RuntimeError( - f"spark-context-cache: native restore configuration" - f" failed closed: {error}" + f"spark-context-cache: SparkCache CUDA restore configuration" + f" was rejected: {error}" ) from error self._native_adapter = adapter self._native_execute_restore = native_execute self._native_required_record_mask = required self.counters["native_configured"] = 1 logger.info( - "spark-context-cache: native restore configured library=%s" + "spark-context-cache: SparkCache CUDA restore configured library=%s" " sha256=%s arena_mib=%d destinations=%d slots=%d" " max_chunks_per_slab=%d", self._native_library_path, @@ -2023,12 +2023,12 @@ def _configure_native_restore(self) -> None: def _configure_native_hybrid_restore(self) -> None: layout = self._page_layout if layout is None: - raise RuntimeError("native hybrid restore has no page layout") + raise RuntimeError("SparkCache CUDA restore has no page layout") first_layer = layout.groups[0].layers[0] first_tensor = self._layer_tensors[first_layer.name] device = first_tensor.device if device.type != "cuda": - raise RuntimeError("native hybrid restore requires CUDA page tensors") + raise RuntimeError("SparkCache CUDA restore requires CUDA page tensors") device_ordinal = ( int(device.index) if device.index is not None @@ -2042,7 +2042,7 @@ def _configure_native_hybrid_restore(self) -> None: } if len(capacities) != 1: raise RuntimeError( - "native hybrid page group layers disagree on capacity" + "SparkCache CUDA page group layers disagree on capacity" ) max_slots += capacities.pop() adapters = [] @@ -2058,7 +2058,9 @@ def _configure_native_hybrid_restore(self) -> None: & int(components.hybrid_page_cuda_capability) == 0 ): - raise RuntimeError("native library lacks hybrid page CUDA scatter") + raise RuntimeError( + "SparkCache CUDA placement library lacks page scatter" + ) for _lane in range(self._load_thread_limit): adapter = components.NativePlacementAdapter.create( library, @@ -2074,15 +2076,20 @@ def _configure_native_hybrid_restore(self) -> None: execute_restore = components.execute_native_hybrid_restore execute_placement = components.execute_native_hybrid_placement if not callable(execute_restore): - raise TypeError("native hybrid direct-restore orchestrator is not callable") + raise TypeError( + "SparkCache direct CUDA restore orchestrator is not callable" + ) if not callable(execute_placement): - raise TypeError("native hybrid page-placement orchestrator is not callable") + raise TypeError( + "SparkCache CUDA page-placement orchestrator is not callable" + ) except Exception as error: for adapter in adapters: with contextlib.suppress(Exception): adapter.close() raise RuntimeError( - f"spark-context-cache: native hybrid restore configuration failed: {error}" + "spark-context-cache: SparkCache CUDA restore configuration" + f" failed: {error}" ) from error self._native_adapters = adapters self._native_adapter = adapters[0] @@ -2090,7 +2097,7 @@ def _configure_native_hybrid_restore(self) -> None: self._native_execute_hybrid_placement = execute_placement self.counters["native_hybrid_configured"] = 1 logger.info( - "spark-context-cache: native hybrid restore configured" + "spark-context-cache: SparkCache CUDA restore configured" " destinations=%d max_slots=%d arena_mib=%d lanes=%d", destination_count, max_slots, @@ -2981,8 +2988,7 @@ def _verify_shared_segment_roots( with self._load_lock: self._held.discard(digest) logger.warning( - "spark-context-cache: shared trunk rejected digest=%s" - " leader=%s", + "spark-context-cache: shared trunk rejected digest=%s leader=%s", digest[:12], plan.digest[:12], ) @@ -3062,7 +3068,7 @@ def _load_one( or self._native_required_record_mask == 0 ): raise RuntimeError( - "native restore selected without a configured adapter" + "SparkCache CUDA restore selected without a configured adapter" ) positions = owned_positions(plan.span_tokens, self._dcp_degree, rank) slots = local_slots_for_positions( @@ -3091,7 +3097,8 @@ def _load_one( # retry those blocks: retire the entry and publish all of # this request's blocks as invalid for clean recompute. logger.warning( - "spark-context-cache: native load failed closed: %s", + "spark-context-cache: SparkCache CUDA restore rejected;" + " recomputing: %s", error, ) self._invalidate_after_failure( @@ -3119,7 +3126,7 @@ def _load_one( "native_chunks_verified", 0 ) + int(result.verified_chunks) logger.info( - "spark-context-cache: native restored %d chunks" + "spark-context-cache: SparkCache CUDA restore verified %d chunks" " (%d encoded bytes, %d slabs) read_hash=%.1f ms" " parse_submit=%.1f ms finish=%.1f ms", result.verified_chunks, @@ -3232,10 +3239,10 @@ def _load_hybrid_pages( self._native_execute_hybrid_restore ): raise RuntimeError( - "native hybrid restore selected without a configured adapter" + "SparkCache CUDA restore selected without a configured adapter" ) if not 0 <= native_lane < len(self._native_adapters): - raise RuntimeError("native hybrid restore lane is unavailable") + raise RuntimeError("SparkCache CUDA placement lane is unavailable") try: result = self._native_execute_hybrid_restore( adapter=self._native_adapters[native_lane], @@ -3250,7 +3257,7 @@ def _load_hybrid_pages( ) except Exception as error: # noqa: BLE001 logger.warning( - "spark-context-cache: native hybrid placement failed: %s", + "spark-context-cache: SparkCache CUDA placement rejected: %s", error, ) self._invalidate_after_failure(plan.digest) @@ -3274,7 +3281,7 @@ def _load_hybrid_pages( self.counters.get("native_hybrid_load_verified", 0) + 1 ) logger.info( - "spark-context-cache: native hybrid direct restored %d bytes" + "spark-context-cache: SparkCache direct CUDA restore verified %d bytes" " slabs=%d read_hash=%.1f ms submit=%.1f ms finish=%.1f ms", result.source_bytes, result.slabs, @@ -3330,10 +3337,10 @@ def _load_hybrid_pages( self._native_execute_hybrid_placement ): raise RuntimeError( - "native hybrid restore selected without a configured adapter" + "SparkCache CUDA restore selected without a configured adapter" ) if not 0 <= native_lane < len(self._native_adapters): - raise RuntimeError("native hybrid restore lane is unavailable") + raise RuntimeError("SparkCache CUDA placement lane is unavailable") try: result = self._native_execute_hybrid_placement( adapter=self._native_adapters[native_lane], @@ -3344,7 +3351,7 @@ def _load_hybrid_pages( ) except Exception as error: # noqa: BLE001 logger.warning( - "spark-context-cache: native hybrid placement failed: %s", + "spark-context-cache: SparkCache CUDA placement rejected: %s", error, ) self._invalidate_after_failure(plan.digest) @@ -3362,7 +3369,7 @@ def _load_hybrid_pages( self.counters.get("native_hybrid_load_verified", 0) + 1 ) logger.info( - "spark-context-cache: native hybrid restored %d bytes" + "spark-context-cache: SparkCache CUDA placement verified %d bytes" " submit=%.1f ms finish=%.1f ms", result.source_bytes, result.copy_and_submit_ms, @@ -3589,7 +3596,7 @@ def shutdown(self): ) logger.warning( "spark-context-cache: shutdown left %d loader(s) alive;" - " retaining native placement handle until process exit", + " retaining SparkCache CUDA placement handle until process exit", live_threads, ) return None diff --git a/sparkcache/spark_context_cache_cuda_page_restore.py b/sparkcache/spark_context_cache_cuda_page_restore.py new file mode 100644 index 0000000..4068ed4 --- /dev/null +++ b/sparkcache/spark_context_cache_cuda_page_restore.py @@ -0,0 +1,4 @@ +"""Canonical SparkCache direct CUDA restore and page-placement interface.""" + +from sparkcache.spark_context_cache_native_hybrid_restore import * # noqa: F403 +from sparkcache.spark_context_cache_native_hybrid_restore import __all__ # noqa: F401 diff --git a/sparkcache/spark_context_cache_cuda_placement.py b/sparkcache/spark_context_cache_cuda_placement.py new file mode 100644 index 0000000..e35a606 --- /dev/null +++ b/sparkcache/spark_context_cache_cuda_placement.py @@ -0,0 +1,4 @@ +"""Canonical SparkCache CUDA placement adapter interface.""" + +from sparkcache.spark_context_cache_native_placement import * # noqa: F403 +from sparkcache.spark_context_cache_native_placement import __all__ # noqa: F401 diff --git a/sparkcache/spark_context_cache_cuda_restore.py b/sparkcache/spark_context_cache_cuda_restore.py new file mode 100644 index 0000000..c9c7970 --- /dev/null +++ b/sparkcache/spark_context_cache_cuda_restore.py @@ -0,0 +1,4 @@ +"""Canonical SparkCache CUDA restore planning and execution interface.""" + +from sparkcache.spark_context_cache_native_restore import * # noqa: F403 +from sparkcache.spark_context_cache_native_restore import __all__ # noqa: F401 diff --git a/sparkcache/spark_context_cache_hybrid.py b/sparkcache/spark_context_cache_hybrid.py index c0e938e..87cf6ad 100644 --- a/sparkcache/spark_context_cache_hybrid.py +++ b/sparkcache/spark_context_cache_hybrid.py @@ -112,7 +112,7 @@ def plan_page_snapshot( """Validate the small SPHP1 header and describe payload byte extents. ``encoded_prefix`` may be the complete snapshot or only the header bytes. - Native restore uses the latter after authenticating the containing .spcc + SparkCache CUDA restore uses the latter after authenticating the containing .spcc object, avoiding a Python slice for every layer payload. """ diff --git a/sparkcache/spark_context_cache_native_hybrid_restore.py b/sparkcache/spark_context_cache_native_hybrid_restore.py index f5b6fbc..b89bafa 100644 --- a/sparkcache/spark_context_cache_native_hybrid_restore.py +++ b/sparkcache/spark_context_cache_native_hybrid_restore.py @@ -1,4 +1,4 @@ -"""Native mapped-host placement for an authenticated hybrid page snapshot.""" +"""SparkCache CUDA placement for an authenticated hybrid page snapshot.""" from __future__ import annotations @@ -28,7 +28,7 @@ class NativeHybridRestoreError(RuntimeError): - """Hybrid native placement cannot safely complete.""" + """SparkCache CUDA page placement cannot safely complete.""" @dataclass(frozen=True) @@ -70,7 +70,9 @@ def plan_page_slabs( arena_bytes: int, ) -> tuple[NativePageSlab, ...]: if arena_bytes <= 0: - raise NativeHybridRestoreError("native page arena bytes must be positive") + raise NativeHybridRestoreError( + "SparkCache CUDA placement arena bytes must be positive" + ) payload_bytes = plan.total_bytes - plan.header_bytes slabs = [] for slab_start in range(0, payload_bytes, arena_bytes): @@ -94,7 +96,9 @@ def plan_page_slabs( ) ) if not spans: - raise NativeHybridRestoreError("native page slab has no copy spans") + raise NativeHybridRestoreError( + "SparkCache CUDA page slab has no copy spans" + ) slabs.append(NativePageSlab(slab_start, slab_end, tuple(spans))) return tuple(slabs) @@ -123,7 +127,7 @@ def execute_native_hybrid_placement( first_arena = transaction.acquire_arena(0) if first_arena.arena_mode != native.ARENA_MAPPED_HOST: raise NativeHybridRestoreError( - "hybrid native restore requires a mapped-host arena" + "SparkCache CUDA restore requires a mapped-host arena" ) slabs = plan_page_slabs(plan, arena_bytes=first_arena.capacity_bytes) for slab_index, slab in enumerate(slabs): @@ -148,15 +152,18 @@ def execute_native_hybrid_placement( started = time.perf_counter() stats = transaction.finish() finish_ms = 1e3 * (time.perf_counter() - started) - if transaction.state is not RestoreState.FINISHED or not transaction.can_resume: + if ( + transaction.state is not RestoreState.FINISHED + or not transaction.can_resume + ): raise NativeHybridRestoreError( - "native page finish did not release the parked request" + "SparkCache CUDA placement did not release the parked request" ) except NativeHybridRestoreError: raise except (NativePlacementContractError, RuntimeError, TypeError, ValueError) as error: raise NativeHybridRestoreError( - f"native hybrid placement failed: {error}" + f"SparkCache CUDA page placement was rejected: {error}" ) from error if ( int(stats.slot_uploads) != 1 @@ -167,7 +174,7 @@ def execute_native_hybrid_placement( or int(stats.staged_h2d_bytes) != 0 ): raise NativeHybridRestoreError( - "native hybrid placement statistics violate the mapped transaction" + "SparkCache CUDA placement statistics violate the mapped transaction" ) return NativeHybridRestoreResult( placement_stats=stats, @@ -197,7 +204,11 @@ def _target_record_plan(path: Any, encoded_bytes: int) -> tuple[int, int, int]: if record.get("kind") == "target_ckv": offset = int(record["offset"]) length = int(record["length"]) - if offset < 0 or length <= 0 or payload_offset + offset + length > encoded_bytes: + if ( + offset < 0 + or length <= 0 + or payload_offset + offset + length > encoded_bytes + ): break return payload_offset, offset, length except (OSError, TypeError, ValueError, json.JSONDecodeError) as error: @@ -272,7 +283,12 @@ def execute_native_hybrid_restore( with concurrent.futures.ThreadPoolExecutor( max_workers=min(io_workers, len(slab.chunks)) ) as pool: - tuple(pool.map(lambda item: _read_and_authenticate(*item), zip(slab.chunks, views, strict=True))) + tuple( + pool.map( + lambda item: _read_and_authenticate(*item), + zip(slab.chunks, views, strict=True), + ) + ) read_ms += 1e3 * (time.perf_counter() - started) del views del buffer @@ -296,7 +312,9 @@ def execute_native_hybrid_restore( or int(parsed.record_offset_bytes[_TARGET_KIND]) != target_offset or int(parsed.record_length_bytes[_TARGET_KIND]) != target_length ): - raise NativeHybridRestoreError("authenticated target extent changed") + raise NativeHybridRestoreError( + "authenticated target extent changed" + ) extent_start = stream_offset extent_end = stream_offset + target_length for destination_index, layer in enumerate(page_plan.spans): @@ -306,7 +324,11 @@ def execute_native_hybrid_restore( continue spans.append( native.PageCopySpan( - chunk.arena_offset_bytes + payload + target_offset + start - extent_start, + chunk.arena_offset_bytes + + payload + + target_offset + + start + - extent_start, start - page_plan.header_bytes, start - layer.source_start, end - start, @@ -334,7 +356,17 @@ def execute_native_hybrid_restore( ) +CudaPageRestoreError = NativeHybridRestoreError +CudaPageRestoreResult = NativeHybridRestoreResult +CudaPageSlab = NativePageSlab +execute_cuda_direct_restore = execute_native_hybrid_restore +execute_cuda_page_placement = execute_native_hybrid_placement + + __all__ = [ + "CudaPageRestoreError", + "CudaPageRestoreResult", + "CudaPageSlab", "NativeHybridRestoreError", "NativeHybridRestoreResult", "NativePageSlab", @@ -342,4 +374,6 @@ def execute_native_hybrid_restore( "plan_page_slabs", "execute_native_hybrid_placement", "execute_native_hybrid_restore", + "execute_cuda_direct_restore", + "execute_cuda_page_placement", ] diff --git a/sparkcache/spark_context_cache_native_placement.py b/sparkcache/spark_context_cache_native_placement.py index 3aa8b22..e5bd71a 100644 --- a/sparkcache/spark_context_cache_native_placement.py +++ b/sparkcache/spark_context_cache_native_placement.py @@ -1,4 +1,4 @@ -"""Opt-in model-serving seam for the implemented native context-cache placer. +"""Opt-in model-serving seam for SparkCache CUDA placement. This module does not install itself into the vLLM connector. It layers artifact attestation, registered-tensor validation, and parked-request @@ -33,14 +33,14 @@ class NativePlacementContractError(RuntimeError): - """The Python/native integration contract is not safe to execute.""" + """The Python/CUDA integration contract is not safe to execute.""" class NativePlacementCallError(RuntimeError): - """An attested native function returned a non-success status.""" + """An attested SparkCache CUDA function returned a non-success status.""" def __init__(self, action: str, status: int, detail: str = "") -> None: - message = f"native cache placement failed to {action}: status={status}" + message = f"SparkCache CUDA placement failed to {action}: status={status}" if detail: message += f": {detail}" super().__init__(message) @@ -77,7 +77,7 @@ class RestoreState(Enum): # Public aliases keep the adapter surface explicit without defining a second -# ctypes ABI that could drift from the native placement contract. +# ctypes ABI that could drift from the SparkCache CUDA placement contract. SparkCacheDestinationDescriptor = native.DestinationDescriptor SparkCacheChunkDescriptor = native.ChunkDescriptor SparkCachePlacementStats = native.PlacementStats @@ -353,23 +353,23 @@ def load( resolved = Path(artifact_path).resolve(strict=True) except (OSError, RuntimeError) as error: raise NativePlacementContractError( - f"native placement artifact is unavailable: {artifact_path}" + f"SparkCache CUDA placement artifact is unavailable: {artifact_path}" ) from error if not resolved.is_file(): raise NativePlacementContractError( - f"native placement artifact is not a regular file: {resolved}" + f"SparkCache CUDA placement artifact is not a regular file: {resolved}" ) actual_sha256 = hashlib.sha256(resolved.read_bytes()).hexdigest() if actual_sha256 != expected_sha256: raise NativePlacementContractError( - "native placement artifact SHA-256 mismatch:" + "SparkCache CUDA placement artifact SHA-256 mismatch:" f" expected={expected_sha256}, actual={actual_sha256}" ) try: cdll, abi_info = binding_loader(str(resolved)) except (OSError, native.NativePlacementError) as error: raise NativePlacementContractError( - f"canonical native binding rejected attested artifact: {error}" + f"SparkCache CUDA binding rejected attested artifact: {error}" ) from error if abi_info.abi_version != native.ABI_VERSION: raise NativePlacementContractError( @@ -399,7 +399,7 @@ def check( class NativePlacementAdapter: - """One configured native placer with one serialized restore transaction.""" + """One CUDA placer with one serialized restore transaction.""" def __init__( self, @@ -442,7 +442,7 @@ def create( mode = ArenaMode(arena_mode) except (TypeError, ValueError) as error: raise NativePlacementContractError( - "unsupported native arena mode" + "unsupported SparkCache CUDA placement arena mode" ) from error arena_bytes = _u64(arena_bytes, "arena_bytes", positive=True) max_destinations = _u32(max_destinations, "max_destinations", positive=True) @@ -479,13 +479,13 @@ def create( library.check(result, "create placement") if not handle.value: raise NativePlacementContractError( - "native placement create succeeded with a null handle" + "SparkCache CUDA placement creation returned a null handle" ) return cls(library, handle, max_destinations, max_slots, mode) def _ensure_open(self) -> None: if self._closed or not self._handle.value: - raise NativePlacementContractError("native placement is closed") + raise NativePlacementContractError("SparkCache CUDA placement is closed") def _call(self, action: str, function: Any, *arguments: Any) -> None: self._ensure_open() @@ -563,7 +563,8 @@ def begin_parked_restore( ) if self._active is not None: raise NativePlacementContractError( - f"native restore already active for {self._active.request_id}" + "SparkCache CUDA restore is already active for" + f" {self._active.request_id}" ) if not isinstance(request_id, str) or not request_id: raise NativePlacementContractError("request_id must be a nonempty string") @@ -603,7 +604,8 @@ def begin_parked_page_restore( ) if self._active is not None: raise NativePlacementContractError( - f"native restore already active for {self._active.request_id}" + "SparkCache CUDA restore is already active for" + f" {self._active.request_id}" ) if not isinstance(request_id, str) or not request_id: raise NativePlacementContractError("request_id must be a nonempty string") @@ -618,7 +620,9 @@ def begin_parked_page_restore( groups.append(native.PageGroupDescriptor(len(flattened), len(slots), 0, 0)) flattened.extend(slots) if not groups or len(flattened) > self._max_slots: - raise NativePlacementContractError("page slot vector exceeds configured maximum") + raise NativePlacementContractError( + "page slot vector exceeds configured maximum" + ) snapshot_bytes = _u64(snapshot_bytes, "snapshot_bytes", positive=True) native_groups = (native.PageGroupDescriptor * len(groups))(*groups) native_slots = (ctypes.c_uint32 * len(flattened))(*flattened) @@ -672,7 +676,7 @@ def __del__(self) -> None: # pragma: no cover class ParkedRestore: - """Native transaction whose request is resumable only after ``finish``.""" + """CUDA transaction whose request is resumable only after ``finish``.""" def __init__( self, @@ -700,7 +704,7 @@ def _require_parked(self) -> None: ) if self._adapter._active is not self: raise NativePlacementContractError( - f"restore {self.request_id} no longer owns native placement" + f"restore {self.request_id} no longer owns SparkCache CUDA placement" ) def _abort_after_failure(self) -> None: @@ -744,7 +748,7 @@ def acquire_arena(self, arena_index: int) -> native.ArenaView: ): self._abort_after_failure() raise NativePlacementContractError( - "native arena view disagrees with the requested arena contract" + "SparkCache CUDA arena view disagrees with the requested contract" ) return view @@ -810,7 +814,7 @@ def submit_direct_slab( isinstance(item, native.ChunkDescriptor) for item in chunk_tuple ): raise NativePlacementContractError( - "direct slab requires native chunk descriptors" + "direct CUDA slab requires SparkCache chunk descriptors" ) native_chunks = (native.ChunkDescriptor * len(chunk_tuple))(*chunk_tuple) self._call( @@ -883,8 +887,18 @@ def __exit__(self, _exc_type, _exc, _traceback) -> None: self.abort() +CudaPlacementAdapter = NativePlacementAdapter +CudaPlacementCallError = NativePlacementCallError +CudaPlacementContractError = NativePlacementContractError +CudaPlacementLibrary = NativePlacementLibrary + + __all__ = [ "ArenaMode", + "CudaPlacementAdapter", + "CudaPlacementCallError", + "CudaPlacementContractError", + "CudaPlacementLibrary", "NativePlacementAdapter", "NativePlacementCallError", "NativePlacementContractError", diff --git a/sparkcache/spark_context_cache_native_restore.py b/sparkcache/spark_context_cache_native_restore.py index 6442064..635ec98 100644 --- a/sparkcache/spark_context_cache_native_restore.py +++ b/sparkcache/spark_context_cache_native_restore.py @@ -1,10 +1,10 @@ -"""Fail-closed orchestration for native SparkCache restore placement. +"""Verified-or-recompute orchestration for SparkCache CUDA restore. This is the model-serving path between a validated ``LookupResult`` and the attested placement adapter. It never imports the model-down experiment gate. Each content-addressed chunk is read directly into a mapped host arena, its manifest length and one whole-file SHA-256 are checked, and only then is the -native parser allowed to describe bytes to the scatter kernel. +CUDA parser allowed to describe bytes to the scatter kernel. """ from __future__ import annotations @@ -39,7 +39,7 @@ class NativeRestoreError(RuntimeError): - """A native restore cannot safely complete and must be recomputed.""" + """A SparkCache CUDA restore cannot complete and must be recomputed.""" @dataclass(frozen=True) @@ -166,7 +166,7 @@ def plan_native_restore( payload_alignment, "payload_alignment", maximum=1 << 20 ) if not getattr(lookup, "is_hit", False): - raise NativeRestoreError("native restore requires a cache hit") + raise NativeRestoreError("SparkCache CUDA restore requires a cache hit") manifest = getattr(lookup, "_manifest", None) if not isinstance(manifest, Mapping): raise NativeRestoreError("cache hit has no validated manifest") @@ -226,7 +226,7 @@ def plan_native_restore( ) if arena_offset + encoded_bytes > arena_bytes: raise NativeRestoreError( - f"chunk {index} does not fit the configured native arena" + f"chunk {index} does not fit the SparkCache CUDA arena" ) current.append( NativeChunkPlan( @@ -252,7 +252,7 @@ def plan_native_restore( if current: slabs.append(NativeSlabPlan(tuple(current), cursor)) if not slabs: - raise NativeRestoreError("native restore produced no slabs") + raise NativeRestoreError("SparkCache CUDA restore produced no slabs") return tuple(slabs) @@ -315,7 +315,8 @@ def _check_stats( } if mismatches: raise NativeRestoreError( - f"native placement statistics violate restore contract: {mismatches}" + "SparkCache CUDA placement statistics violate the restore contract:" + f" {mismatches}" ) @@ -334,7 +335,7 @@ def execute_native_restore( io_workers: int = 8, payload_alignment: int = 256, ) -> NativeRestoreResult: - """Install one lookup through a single fail-closed native transaction.""" + """Install one lookup through one verified-or-recompute CUDA transaction.""" dcp_degree = _strict_positive_int(dcp_degree, "dcp_degree", maximum=_U32_MAX) dcp_rank = _strict_nonnegative_int(dcp_rank, "dcp_rank", maximum=_U32_MAX) @@ -380,7 +381,7 @@ def execute_native_restore( or slab.arena_used_bytes > arena.capacity_bytes ): raise NativeRestoreError( - "native mapped arena disagrees with configured capacity" + "SparkCache CUDA mapped arena disagrees with configured capacity" ) arena_buffer = native.arena_memoryview( arena, length=slab.arena_used_bytes @@ -433,7 +434,7 @@ def execute_native_restore( or int(output.row_count) != chunk.row_count ): raise NativeRestoreError( - "native parser disagrees with the authenticated" + "SparkCache CUDA parser disagrees with the authenticated" " manifest/slab plan" ) parsed.append(output) @@ -458,7 +459,7 @@ def execute_native_restore( or not transaction.can_resume ): raise NativeRestoreError( - "native finish did not release the parked request" + "SparkCache CUDA finish did not release the parked request" ) except NativeRestoreError: raise @@ -469,7 +470,9 @@ def execute_native_restore( TypeError, ValueError, ) as error: - raise NativeRestoreError(f"native restore failed closed: {error}") from error + raise NativeRestoreError( + f"SparkCache CUDA restore was rejected; recomputing: {error}" + ) from error return NativeRestoreResult( placement_stats=stats, @@ -482,11 +485,25 @@ def execute_native_restore( ) +CudaRestoreChunkPlan = NativeChunkPlan +CudaRestoreError = NativeRestoreError +CudaRestoreResult = NativeRestoreResult +CudaRestoreSlabPlan = NativeSlabPlan +execute_cuda_restore = execute_native_restore +plan_cuda_restore = plan_native_restore + + __all__ = [ + "CudaRestoreChunkPlan", + "CudaRestoreError", + "CudaRestoreResult", + "CudaRestoreSlabPlan", "NativeChunkPlan", "NativeRestoreError", "NativeRestoreResult", "NativeSlabPlan", "execute_native_restore", + "execute_cuda_restore", "plan_native_restore", + "plan_cuda_restore", ] diff --git a/sparkcache/spark_context_cache_profiles.py b/sparkcache/spark_context_cache_profiles.py index 4136e86..61fb231 100644 --- a/sparkcache/spark_context_cache_profiles.py +++ b/sparkcache/spark_context_cache_profiles.py @@ -4,7 +4,7 @@ onto the cache's persistent record vocabulary, plus the identity strings that pin cached bytes to a layout. Profiles carry no runtime state and no vllm/torch imports; the connector resolves one profile at construction and -threads its values through the codec and the native placement layer. +threads its values through the codec and the SparkCache CUDA placement layer. The profile name itself is never serialized. Cache identity remains the tuple of layout strings, checkpoint digests, parallel degrees, geometry, @@ -16,12 +16,13 @@ ``mtp_draft_kv``, plus the non-data ``logical_positions`` and the policy-gated ``boundary_hidden``. Profiles map model layers onto these kinds; unsupported kinds are rejected because the on-disk chunk ABI and the -native placement ABI (at most ``MAX_RECORD_KINDS`` data records per chunk) +SparkCache CUDA placement ABI (at most ``MAX_RECORD_KINDS`` data records per chunk) are frozen. """ from __future__ import annotations +import warnings from dataclasses import dataclass from typing import Mapping @@ -31,7 +32,8 @@ {"target_ckv", "sparse_indexer", "mtp_draft_kv", "boundary_hidden"} ) -# Native placement ABI ordinals (spark_cache_native RECORD_*). Frozen; a +# SparkCache CUDA placement ABI ordinals (legacy spark_cache_native RECORD_*). +# Frozen; a # profile maps families onto them and must never renumber. NATIVE_RECORD_ORDINALS = { "target_ckv": 0, @@ -81,7 +83,7 @@ class ModelProfile: # and capacity accounting deliberately remain unchanged; no implemented # shared-storage interface consumes this field. kv_replicated_across_tp: bool = False - native_page_restore: bool = False + cuda_page_restore: bool = False def __post_init__(self) -> None: if self.boundary_hidden_policy not in _BOUNDARY_POLICIES: @@ -100,9 +102,9 @@ def __post_init__(self) -> None: raise ProfileError( f"profile {self.name}: unknown storage mode {self.storage_mode!r}" ) - if self.native_page_restore and self.storage_mode != "block_pages_v1": + if self.cuda_page_restore and self.storage_mode != "block_pages_v1": raise ProfileError( - f"profile {self.name}: native_page_restore requires block pages" + f"profile {self.name}: cuda_page_restore requires block pages" ) families = {family for _, family in self.classification_rules} families |= self.required_families | self.optional_families @@ -115,13 +117,15 @@ def __post_init__(self) -> None: ) for rule in self.classification_rules: if not rule[0]: - raise ProfileError( - f"profile {self.name}: empty classification pattern" - ) + raise ProfileError(f"profile {self.name}: empty classification pattern") if not self.required_families: - raise ProfileError( - f"profile {self.name}: at least one required family" - ) + raise ProfileError(f"profile {self.name}: at least one required family") + + @property + def native_page_restore(self) -> bool: + """Compatibility alias for :attr:`cuda_page_restore`.""" + + return self.cuda_page_restore def persisted_families(self, draft_kv_policy: str) -> frozenset[str]: """Data families a store must cover under the active draft policy. @@ -151,8 +155,7 @@ def expects_draft_named_layers(self, draft_kv_policy: str) -> bool: state registers unmarked inside the target pool. """ return ( - draft_kv_policy == "separate" - and "mtp_draft_kv" in self.required_families + draft_kv_policy == "separate" and "mtp_draft_kv" in self.required_families ) def validate_for_deployment( @@ -161,7 +164,8 @@ def validate_for_deployment( dcp_degree: int, block_size: int, min_span_tokens: int, - native_restore: bool, + cuda_restore: bool | None = None, + native_restore: bool | None = None, ) -> None: """Fail startup on geometry a store or restore would corrupt or silently truncate later. @@ -170,8 +174,7 @@ def validate_for_deployment( raise ProfileError("dcp_degree must be positive") if self.storage_mode == "block_pages_v1" and dcp_degree != 1: raise ProfileError( - f"profile {self.name}: block-page storage requires" - " dcp_degree 1" + f"profile {self.name}: block-page storage requires dcp_degree 1" ) if self.chunk_tokens % dcp_degree: raise ProfileError( @@ -192,12 +195,24 @@ def validate_for_deployment( f"profile {self.name}: min_span_tokens {min_span_tokens} is" f" below one chunk ({self.chunk_tokens} tokens)" ) - if native_restore: + if cuda_restore is not None and native_restore is not None: + if bool(cuda_restore) != bool(native_restore): + raise ProfileError( + "conflicting cuda_restore and legacy native_restore values" + ) + if cuda_restore is None and native_restore is not None: + warnings.warn( + "native_restore is deprecated; use cuda_restore", + FutureWarning, + stacklevel=2, + ) + cuda_restore = native_restore + if cuda_restore: if self.storage_mode == "block_pages_v1": - if not self.native_page_restore: + if not self.cuda_page_restore: raise ProfileError( - f"profile {self.name}: native restore does not support" - " this block-page layout" + f"profile {self.name}: SparkCache CUDA restore does not" + " support this block-page layout" ) return persisted = self.persisted_families(self.default_draft_kv_policy) @@ -238,9 +253,7 @@ def validate_for_deployment( ("mtp", "mtp_draft_kv"), ("spec", "mtp_draft_kv"), ), - required_families=frozenset( - {"target_ckv", "sparse_indexer", "mtp_draft_kv"} - ), + required_families=frozenset({"target_ckv", "sparse_indexer", "mtp_draft_kv"}), chunk_tokens=256, kv_replicated_across_tp=True, ), @@ -259,7 +272,7 @@ def validate_for_deployment( required_families=frozenset({"target_ckv"}), chunk_tokens=256, storage_mode="block_pages_v1", - native_page_restore=True, + cuda_page_restore=True, kv_replicated_across_tp=True, ), "deepseek-v4-fp8-hma": ModelProfile( diff --git a/sparkcache/test_cuda_restore_terminology.py b/sparkcache/test_cuda_restore_terminology.py new file mode 100644 index 0000000..7ba76b4 --- /dev/null +++ b/sparkcache/test_cuda_restore_terminology.py @@ -0,0 +1,30 @@ +"""GPU-free compatibility tests for SparkCache CUDA restore terminology.""" + +from sparkcache import spark_cache_cuda, spark_cache_native +from sparkcache import spark_context_cache_cuda_page_restore as cuda_page +from sparkcache import spark_context_cache_cuda_placement as cuda_placement +from sparkcache import spark_context_cache_cuda_restore as cuda_restore +from sparkcache import spark_context_cache_native_hybrid_restore as legacy_page +from sparkcache import spark_context_cache_native_placement as legacy_placement +from sparkcache import spark_context_cache_native_restore as legacy_restore + + +def test_canonical_cuda_symbols_alias_legacy_python_api() -> None: + assert ( + spark_cache_cuda.CudaPlacementError is spark_cache_native.NativePlacementError + ) + assert ( + cuda_placement.CudaPlacementAdapter is legacy_placement.NativePlacementAdapter + ) + assert cuda_restore.CudaRestoreError is legacy_restore.NativeRestoreError + assert cuda_restore.execute_cuda_restore is legacy_restore.execute_native_restore + assert cuda_restore.plan_cuda_restore is legacy_restore.plan_native_restore + assert cuda_page.CudaPageRestoreError is legacy_page.NativeHybridRestoreError + assert ( + cuda_page.execute_cuda_page_placement + is legacy_page.execute_native_hybrid_placement + ) + assert ( + cuda_page.execute_cuda_direct_restore + is legacy_page.execute_native_hybrid_restore + ) diff --git a/sparkcache/test_defect_regressions.py b/sparkcache/test_defect_regressions.py index 9336817..4cb4461 100644 --- a/sparkcache/test_defect_regressions.py +++ b/sparkcache/test_defect_regressions.py @@ -16,7 +16,8 @@ import torch import sparkcache.native.python as native_package -import sparkcache.spark_cache_native as canonical_native +import sparkcache.spark_cache_cuda as canonical_cuda +import sparkcache.spark_cache_native as legacy_native import sparkcache.spark_context_cache_store as package_store import sparkcache.streaming as streaming import sparkcache.spark_context_cache_store as flat_store @@ -107,8 +108,7 @@ def test_missed_duplicate_and_reordered_checkpoint_chunks_converge( } worker._held = set(initial) first_cycle = [ - worker.get_kv_connector_stats().data["reports"][0] - for _ in range(3) + worker.get_kv_connector_stats().data["reports"][0] for _ in range(3) ] for report in ( first_cycle[2], @@ -134,8 +134,7 @@ def test_missed_duplicate_and_reordered_checkpoint_chunks_converge( worker._held = set(replacement) worker.get_kv_connector_stats() # dropped checkpoint chunk zero delivered = [ - worker.get_kv_connector_stats().data["reports"][0] - for _ in range(5) + worker.get_kv_connector_stats().data["reports"][0] for _ in range(5) ] complete_cycle = delivered[2:] for report in ( @@ -293,17 +292,11 @@ def test_same_rank_aggregation_loss_recovers_from_replayed_delta(self) -> None: merged = connector_module.SparkCacheStats( data={"reports": [sequence_one]} ).aggregate( - connector_module.SparkCacheStats( - data={"reports": [sequence_two]} - ) - ) - self.assertEqual( - merged.data["reports"][0]["delta"]["sequence"], 2 + connector_module.SparkCacheStats(data={"reports": [sequence_two]}) ) + self.assertEqual(merged.data["reports"][0]["delta"]["sequence"], 2) - scheduler._absorb_quorum( - types.SimpleNamespace(kv_connector_stats=merged) - ) + scheduler._absorb_quorum(types.SimpleNamespace(kv_connector_stats=merged)) self.assertNotIn(replacement, scheduler._quorum) scheduler._absorb_quorum(self._output(sequence_one)) self.assertEqual(scheduler._quorum[replacement], {0}) @@ -313,9 +306,13 @@ class DeadInterfaceRemovalTests(unittest.TestCase): """D-10: native and streaming code expose one model-serving interface each.""" def test_native_binding_and_runtime_interfaces_have_one_owner(self) -> None: - self.assertIs(native_package.PlacementConfig, canonical_native.PlacementConfig) + self.assertIs(native_package.PlacementConfig, canonical_cuda.PlacementConfig) + self.assertIs(canonical_cuda.PlacementConfig, legacy_native.PlacementConfig) + self.assertIs( + canonical_cuda.CudaPlacementError, legacy_native.NativePlacementError + ) self.assertIs(flat_store.ManifestStore, package_store.ManifestStore) - self.assertFalse(hasattr(canonical_native, "PlacementHandle")) + self.assertFalse(hasattr(canonical_cuda, "PlacementHandle")) self.assertFalse(hasattr(ParkedRestore, "submit_transposed_slab")) self.assertFalse(hasattr(streaming, "PreemptionDrainAdapter")) @@ -492,9 +489,7 @@ class SlidingWindowMLASpec(SlidingWindowSpec): block_size=64, storage_block_size=64, page_size_bytes=64, - kv_cache_specs={ - "swa": SlidingWindowMLASpec(sliding_window) - }, + kv_cache_specs={"swa": SlidingWindowMLASpec(sliding_window)}, ), is_eagle_group=False, layer_names=("swa",), @@ -772,9 +767,9 @@ def test_reuse_window_geometry_forks_identity_and_old_entry_misses(self) -> None "state": torch.arange(80 * 1 * 8, dtype=torch.float32).reshape( 80, 1, 8 ), - "state128": torch.arange( - 40 * 1 * 8, dtype=torch.float32 - ).reshape(40, 1, 8), + "state128": torch.arange(40 * 1 * 8, dtype=torch.float32).reshape( + 40, 1, 8 + ), } first.register_kv_caches(pools) plan = _ReqPlan( @@ -813,7 +808,7 @@ def test_resolved_scheduler_block_size_accepts_exact_chunk_multiple(self) -> Non dcp_degree=1, block_size=2304, min_span_tokens=4096, - native_restore=False, + cuda_restore=False, ) @@ -908,8 +903,7 @@ def test_restore_invalid_manifest_and_its_unshared_chunk_are_removed(self) -> No logical_start=0, logical_end=256, records={ - record: record.value.encode() - for record in package_store.StateRecord + record: record.value.encode() for record in package_store.StateRecord }, ) context_digest = hashlib.sha256(b"D-13-invalid-manifest").hexdigest() @@ -923,10 +917,7 @@ def test_restore_invalid_manifest_and_its_unshared_chunk_are_removed(self) -> No chunks=[chunk], ) manifest_path = ( - root - / "manifests" - / identity.storage_key - / f"{context_digest}.json" + root / "manifests" / identity.storage_key / f"{context_digest}.json" ) manifest = json.loads(manifest_path.read_bytes()) manifest["committed_tokens"] = 0 diff --git a/sparkcache/test_generalization.py b/sparkcache/test_generalization.py index 973c685..e5b7e11 100644 --- a/sparkcache/test_generalization.py +++ b/sparkcache/test_generalization.py @@ -111,7 +111,7 @@ def test_registry_profiles_validate_their_own_reference_geometry(self) -> None: dcp_degree=1, block_size=64, min_span_tokens=profile.chunk_tokens, - native_restore=profile.storage_mode == "per_token_rows", + cuda_restore=profile.storage_mode == "per_token_rows", ) def test_resolve_profile_error_lists_known_names(self) -> None: @@ -124,12 +124,12 @@ def test_deepseek_v4_profile_uses_identity_forked_block_pages(self) -> None: self.assertEqual(profile.required_families, frozenset({"target_ckv"})) self.assertEqual(profile.classification_rules, ()) self.assertIn("block-pages-v1", profile.quantization_layout) - with self.assertRaisesRegex(ProfileError, "native restore"): + with self.assertRaisesRegex(ProfileError, "SparkCache CUDA restore"): profile.validate_for_deployment( dcp_degree=1, block_size=64, min_span_tokens=256, - native_restore=True, + cuda_restore=True, ) def test_glm53_profile_is_distinct_hybrid_namespace(self) -> None: @@ -148,7 +148,7 @@ def test_glm53_profile_accepts_resolved_hybrid_scheduler_block_size(self) -> Non dcp_degree=1, block_size=2304, min_span_tokens=4096, - native_restore=False, + cuda_restore=False, ) def test_glm53_profile_rejects_incommensurate_scheduler_block_size(self) -> None: @@ -158,7 +158,7 @@ def test_glm53_profile_rejects_incommensurate_scheduler_block_size(self) -> None dcp_degree=1, block_size=384, min_span_tokens=4096, - native_restore=False, + cuda_restore=False, ) diff --git a/sparkcache/test_glm52_35bpw_deploy.py b/sparkcache/test_glm52_35bpw_deploy.py index 7e71ab9..f241f17 100644 --- a/sparkcache/test_glm52_35bpw_deploy.py +++ b/sparkcache/test_glm52_35bpw_deploy.py @@ -18,6 +18,7 @@ ProfileTransformError, transform_inspection, ) +import deploy.glm52_35bpw.profile as glm_profile from deploy.glm52_35bpw import prepare_vllm_overlays from deploy.glm52_35bpw.semantic_gate import run_hit_after_quorum from deploy.glm52_35bpw import launch as glm_launch @@ -122,8 +123,7 @@ def _source_inspection() -> dict: "fabb73eb513ec64f3a365da396b38de8d55b3930edfb11baeecbf34ecafa6126", "SPARKRING_ATTEST_MODEL_INDEX_SHA256=" "9fd852f69ed64442e31dce1cbc5fe7acd0a76bfb848e945d272fe98d00d0c9cd", - "SPARKRING_MODEL_REVISION=" - "46537e0e16fcd156627800139b41b9c497fc7ee2", + "SPARKRING_MODEL_REVISION=46537e0e16fcd156627800139b41b9c497fc7ee2", "SPARKRING_MODEL_CONFIG_SHA256=" "ffd30e72ab8bb7e8ad560f2aaab03cc595f3106f0acf793ef96eedaf90f66d69", "KV_FP8_ROPE=1", @@ -226,9 +226,7 @@ def _environment(inspection: dict) -> dict[str, str]: def test_deployment_alias_reuses_the_frozen_glm_cache_layout() -> None: - assert resolve_profile("glm52-exl3-r7-3.5bpw") is resolve_profile( - "glm52-nvfp4" - ) + assert resolve_profile("glm52-exl3-r7-3.5bpw") is resolve_profile("glm52-nvfp4") assert resolve_profile("glm52-exl3-r7-3.5bpw").name == "glm52-nvfp4" @@ -294,29 +292,32 @@ def test_glm_cli_passes_required_rank_local_cache_bind( lambda receipt, scheduler, config: "a" * 64, ) - assert launch_main( - [ - "--inspect", - str(path), - "--image", - _source_inspection()["Image"], - "--name", - "glm52-r0", - "--checkpoint-sha256", - DEFAULT_CHECKPOINT_SHA256, - "--cache-host-path", - "/host-cache/glm52-r0", - "--sparkcache-source-host-path", - "/host-code/sparkcache", - "--scheduler-overlay-host-path", - "/host-overlays/scheduler.py", - "--vllm-config-overlay-host-path", - "/host-overlays/vllm.py", - "--vllm-overlay-receipt-host-path", - "/host-overlays/receipt.json", - "--create-only", - ] - ) == 0 + assert ( + launch_main( + [ + "--inspect", + str(path), + "--image", + _source_inspection()["Image"], + "--name", + "glm52-r0", + "--checkpoint-sha256", + DEFAULT_CHECKPOINT_SHA256, + "--cache-host-path", + "/host-cache/glm52-r0", + "--sparkcache-source-host-path", + "/host-code/sparkcache", + "--scheduler-overlay-host-path", + "/host-overlays/scheduler.py", + "--vllm-config-overlay-host-path", + "/host-overlays/vllm.py", + "--vllm-overlay-receipt-host-path", + "/host-overlays/receipt.json", + "--create-only", + ] + ) + == 0 + ) assert observed[0][1]["extra_binds"] == ( ("/host-cache/glm52-r0", "/cache/sparkcache-glm52-r7", False), @@ -390,9 +391,7 @@ def test_overlay_receipt_and_files_must_match(monkeypatch, tmp_path: Path) -> No encoding="utf-8", ) - assert glm_launch._validate_overlay_inputs(receipt, scheduler, config) == ( - "c" * 64 - ) + assert glm_launch._validate_overlay_inputs(receipt, scheduler, config) == ("c" * 64) config.write_bytes(b"changed") with pytest.raises(ProfileTransformError, match="hash differs"): glm_launch._validate_overlay_inputs(receipt, scheduler, config) @@ -451,9 +450,7 @@ def test_profile_matches_the_public_r7_contract() -> None: assert PROFILE["sparkcache"]["source_sha256"] == ( prepare_vllm_overlays.source_tree_sha256(repository / "sparkcache") ) - assert PROFILE["model"]["revision"] == ( - "9ab9579774cc432df91567a36f6e9e863e0d4c9f" - ) + assert PROFILE["model"]["revision"] == ("9ab9579774cc432df91567a36f6e9e863e0d4c9f") assert PROFILE["serving"] == { "served_model_name": "glm-5.2-exl3-r7-3.5bpw", "tensor_parallel_size": 4, @@ -514,7 +511,7 @@ def test_transform_replaces_only_cache_wiring_and_removes_lmcache() -> None: "spark_cache_restore": True, "spark_cache_scheduler_probe": "none", "spark_cache_streaming_snapshots": False, - "spark_cache_native_restore": False, + "spark_cache_cuda_restore": False, "spark_cache_max_bytes": 200 * 1024**3, "spark_cache_low_watermark_bytes": 180 * 1024**3, "spark_cache_ttl_seconds": 0, @@ -538,9 +535,7 @@ def test_transform_replaces_only_cache_wiring_and_removes_lmcache() -> None: ) assert "LMCACHE_CONFIG_FILE" in environment["SPARKRING_EXPLICITLY_UNSET"] assert "LEGACY_CONNECTOR_PATH" in environment["SPARKRING_EXPLICITLY_UNSET"] - assert "SPARK_CONTEXT_CACHE_ENABLE" in environment[ - "SPARKRING_EXPLICITLY_UNSET" - ] + assert "SPARK_CONTEXT_CACHE_ENABLE" in environment["SPARKRING_EXPLICITLY_UNSET"] assert transformed["Config"]["Labels"] == { "org.sparkring.r7": "accepted-source", "org.sparkcache.deployment-profile": "glm52-exl3-r7-3.5bpw", @@ -565,35 +560,35 @@ def test_transform_rejects_the_unsupported_q35_q40_state_variant() -> None: @pytest.mark.parametrize( - ("streaming", "native_restore"), + ("streaming", "cuda_restore"), [(False, False), (True, False), (False, True), (True, True)], ) -def test_streaming_and_native_restore_are_independent( - streaming: bool, native_restore: bool +def test_streaming_and_cuda_restore_are_independent( + streaming: bool, cuda_restore: bool ) -> None: kwargs: dict[str, object] = { "streaming_snapshots": streaming, - "native_restore": native_restore, + "cuda_restore": cuda_restore, } if streaming: kwargs.update( streaming_native_library="/opt/sparkcache/lib/libsnapshot.so", streaming_native_library_sha256="a" * 64, ) - if native_restore: + if cuda_restore: kwargs.update( - native_restore_library="/opt/sparkcache/lib/libplacement.so", - native_restore_library_sha256="b" * 64, + cuda_placement_library="/opt/sparkcache/lib/libplacement.so", + cuda_placement_library_sha256="b" * 64, ) transformed = transform_inspection(_source_inspection(), **kwargs) extra = _connector_extra(transformed) assert extra["spark_cache_streaming_snapshots"] is streaming - assert extra["spark_cache_native_restore"] is native_restore + assert extra["spark_cache_cuda_restore"] is cuda_restore assert extra["spark_cache_max_bytes"] == 200 * 1024**3 assert extra["spark_cache_low_watermark_bytes"] == 180 * 1024**3 assert ("spark_cache_streaming_native_library" in extra) is streaming - assert ("spark_cache_native_library" in extra) is native_restore + assert ("spark_cache_cuda_placement_library" in extra) is cuda_restore if streaming: assert extra["spark_cache_streaming_timing"] == 0 @@ -613,18 +608,43 @@ def test_streaming_timing_uses_the_runtime_zero_or_one_contract() -> None: "kwargs", [ {"streaming_snapshots": True}, - {"native_restore": True}, + {"cuda_restore": True}, { "streaming_native_library": "/opt/libsnapshot.so", "streaming_native_library_sha256": "a" * 64, }, ], ) -def test_native_feature_configuration_fails_closed(kwargs: dict) -> None: +def test_cuda_feature_configuration_is_rejected(kwargs: dict) -> None: with pytest.raises(ProfileTransformError): transform_inspection(_source_inspection(), **kwargs) +def test_legacy_profile_names_warn_and_emit_canonical_config( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(glm_profile, "_LEGACY_CUDA_RESTORE_WARNING_EMITTED", False) + with pytest.warns(FutureWarning, match="CUDA restore"): + transformed = transform_inspection( + _source_inspection(), + native_restore=True, + native_restore_library="/opt/sparkcache/lib/libplacement.so", + native_restore_library_sha256="b" * 64, + ) + extra = _connector_extra(transformed) + assert extra["spark_cache_cuda_restore"] is True + assert "spark_cache_native_restore" not in extra + + +def test_conflicting_profile_aliases_are_rejected() -> None: + with pytest.raises(ProfileTransformError, match="conflicting"): + transform_inspection( + _source_inspection(), + cuda_restore=True, + native_restore=False, + ) + + def test_instance_ports_are_overridden_together_and_cannot_collide() -> None: transformed = transform_inspection( _source_inspection(), api_port=8100, master_port=29600 @@ -657,8 +677,9 @@ def test_direct_and_underscore_wrapped_r7_commands_transform_identically() -> No "_", *direct["Config"]["Cmd"], ] - assert transform_inspection(wrapped)["Config"]["Cmd"] == ( - transform_inspection(direct)["Config"]["Cmd"] + assert ( + transform_inspection(wrapped)["Config"]["Cmd"] + == (transform_inspection(direct)["Config"]["Cmd"]) ) @@ -746,8 +767,7 @@ def test_explicit_block_size_is_preserved_and_other_values_are_rejected() -> Non def test_containerfile_inherits_r7_entrypoint_and_applies_only_glm_patches() -> None: containerfile = ( - Path(__file__).resolve().parents[1] - / "deploy/glm52_35bpw/Containerfile" + Path(__file__).resolve().parents[1] / "deploy/glm52_35bpw/Containerfile" ).read_text(encoding="utf-8") instructions = [ line.strip().split(maxsplit=1)[0].upper() @@ -806,9 +826,7 @@ def test_prepare_vllm_overlays_patches_exact_preimages( assert receipt["schema"] == "sparkcache-glm52-r7-vllm-overlays/v1" assert [record["disposition"] for record in receipt["files"]] == ["patched"] - assert { - path.name for path in output.iterdir() - } == {"example.py", "receipt.json"} + assert {path.name for path in output.iterdir()} == {"example.py", "receipt.json"} assert (output / "example.py").read_bytes() == b"new\n" with pytest.raises(RuntimeError, match="refusing to overwrite"): prepare_vllm_overlays.prepare( diff --git a/sparkcache/test_glm53_flash_deploy.py b/sparkcache/test_glm53_flash_deploy.py index 09d1bb3..41a645d 100644 --- a/sparkcache/test_glm53_flash_deploy.py +++ b/sparkcache/test_glm53_flash_deploy.py @@ -20,10 +20,13 @@ def test_target_revision_identity_is_deterministic() -> None: - assert immutable_revision_identity( - "local-inference-lab/GLM-5.3-Flash-NVFP4", - "520de24eabf507659eaef7c70f14fd584527facc", - ) == TARGET_ID + assert ( + immutable_revision_identity( + "local-inference-lab/GLM-5.3-Flash-NVFP4", + "520de24eabf507659eaef7c70f14fd584527facc", + ) + == TARGET_ID + ) def test_embedded_mtp_policy_changes_the_cache_namespace() -> None: @@ -57,7 +60,9 @@ def test_embedded_mtp_identity_rejects_incomplete_or_invalid_policy( ) -def test_connector_config_binds_target_and_draft_without_optional_native_paths() -> None: +def test_connector_config_binds_target_and_draft_without_optional_native_paths() -> ( + None +): config = build_kv_transfer_config( target_checkpoint_sha256=TARGET_ID, draft_checkpoint_sha256=DRAFT_ID, @@ -70,7 +75,7 @@ def test_connector_config_binds_target_and_draft_without_optional_native_paths() assert extra["spark_cache_draft_checkpoint_sha256"] == DRAFT_ID assert extra["spark_cache_draft_policy"] == "separate" assert extra["spark_cache_streaming_snapshots"] is False - assert extra["spark_cache_native_restore"] is False + assert extra["spark_cache_cuda_restore"] is False assert json.loads(compact_json(config)) == config diff --git a/sparkcache/test_spark_context_cache_config.py b/sparkcache/test_spark_context_cache_config.py index e81260b..d3f9e2b 100644 --- a/sparkcache/test_spark_context_cache_config.py +++ b/sparkcache/test_spark_context_cache_config.py @@ -160,15 +160,93 @@ def test_load_thread_limit_native_restore_forces_one(self) -> None: vllm, _ = _make_vllm_config( { "spark_cache_load_threads": "4", + "spark_cache_cuda_restore": "1", + "spark_cache_cuda_placement_library": _ABS_LIB, + "spark_cache_cuda_placement_library_sha256": _SHA, + "spark_cache_cuda_placement_arena_bytes": "67108864", + } + ) + config = cfg.parse_connector_config(vllm, vllm.kv_transfer_config, None) + self.assertTrue(config.cuda_restore_enabled) + self.assertEqual(config.load_thread_limit, 1) + + def test_legacy_cuda_restore_config_is_accepted_with_one_warning(self) -> None: + vllm, _ = _make_vllm_config( + { "spark_cache_native_restore": "1", "spark_cache_native_library": _ABS_LIB, "spark_cache_native_library_sha256": _SHA, "spark_cache_native_arena_bytes": "67108864", + "spark_cache_native_io_workers": "2", } ) - config = cfg.parse_connector_config(vllm, vllm.kv_transfer_config, None) + with ( + mock.patch.object(cfg, "_LEGACY_CUDA_RESTORE_WARNING_EMITTED", False), + self.assertWarnsRegex(FutureWarning, "CUDA restore"), + ): + config = cfg.parse_connector_config(vllm, vllm.kv_transfer_config, None) + self.assertTrue(config.cuda_restore_enabled) self.assertTrue(config.native_restore_enabled) - self.assertEqual(config.load_thread_limit, 1) + self.assertEqual(config.cuda_placement_library_path, _ABS_LIB) + self.assertEqual(config.cuda_restore_io_workers, 2) + + def test_conflicting_cuda_restore_aliases_are_rejected(self) -> None: + cases = ( + ("spark_cache_cuda_restore", "1", "spark_cache_native_restore", "0"), + ( + "spark_cache_cuda_placement_library", + _ABS_LIB, + "spark_cache_native_library", + str((Path.cwd() / "other.so").resolve()), + ), + ( + "spark_cache_cuda_placement_arena_bytes", + "67108864", + "spark_cache_native_arena_bytes", + "134217728", + ), + ) + for canonical, canonical_value, legacy, legacy_value in cases: + with self.subTest(canonical=canonical): + vllm, _ = _make_vllm_config( + {canonical: canonical_value, legacy: legacy_value} + ) + with self.assertRaisesRegex(RuntimeError, "conflicting"): + cfg.parse_connector_config(vllm, vllm.kv_transfer_config, None) + + def test_conflicting_cuda_restore_environment_aliases_are_rejected(self) -> None: + vllm, _ = _make_vllm_config() + with ( + mock.patch.dict( + os.environ, + { + "SPARK_CONTEXT_CACHE_CUDA_RESTORE": "1", + "SPARK_CONTEXT_CACHE_NATIVE_RESTORE": "0", + }, + ), + self.assertRaisesRegex(RuntimeError, "conflicting"), + ): + cfg.parse_connector_config(vllm, vllm.kv_transfer_config, None) + + def test_legacy_cuda_restore_environment_is_accepted(self) -> None: + vllm, _ = _make_vllm_config() + environment = { + "SPARK_CONTEXT_CACHE_NATIVE_RESTORE": "1", + "SPARK_CONTEXT_CACHE_NATIVE_LIBRARY": _ABS_LIB, + "SPARK_CONTEXT_CACHE_NATIVE_LIBRARY_SHA256": _SHA, + "SPARK_CONTEXT_CACHE_NATIVE_ARENA_BYTES": "67108864", + "SPARK_CONTEXT_CACHE_NATIVE_IO_WORKERS": "3", + } + with ( + mock.patch.object(cfg, "_LEGACY_CUDA_RESTORE_WARNING_EMITTED", False), + mock.patch.dict(os.environ, environment), + self.assertWarnsRegex(FutureWarning, "CUDA restore"), + ): + config = cfg.parse_connector_config( + vllm, vllm.kv_transfer_config, None + ) + self.assertTrue(config.cuda_restore_enabled) + self.assertEqual(config.cuda_restore_io_workers, 3) def test_max_pending_restores_default_64(self) -> None: vllm, _ = _make_vllm_config() @@ -485,26 +563,26 @@ def test_nnegative_config_int_rejects_negative(self) -> None: def test_native_restore_requires_absolute_path(self) -> None: vllm, _ = _make_vllm_config( { - "spark_cache_native_restore": "1", - "spark_cache_native_library": "relative/path.so", - "spark_cache_native_library_sha256": _SHA, - "spark_cache_native_arena_bytes": "67108864", + "spark_cache_cuda_restore": "1", + "spark_cache_cuda_placement_library": "relative/path.so", + "spark_cache_cuda_placement_library_sha256": _SHA, + "spark_cache_cuda_placement_arena_bytes": "67108864", } ) with self.assertRaises(RuntimeError) as ctx: cfg.parse_connector_config(vllm, vllm.kv_transfer_config, None) self.assertIn( - "absolute library path, a 64-character lowercase SHA-256", + "absolute CUDA placement library path, a 64-character lowercase SHA-256", str(ctx.exception), ) def test_native_restore_requires_valid_sha256(self) -> None: vllm, _ = _make_vllm_config( { - "spark_cache_native_restore": "1", - "spark_cache_native_library": _ABS_LIB, - "spark_cache_native_library_sha256": "short", - "spark_cache_native_arena_bytes": "67108864", + "spark_cache_cuda_restore": "1", + "spark_cache_cuda_placement_library": _ABS_LIB, + "spark_cache_cuda_placement_library_sha256": "short", + "spark_cache_cuda_placement_arena_bytes": "67108864", } ) with self.assertRaises(RuntimeError) as ctx: @@ -514,10 +592,10 @@ def test_native_restore_requires_valid_sha256(self) -> None: def test_native_restore_requires_valid_arena(self) -> None: vllm, _ = _make_vllm_config( { - "spark_cache_native_restore": "1", - "spark_cache_native_library": _ABS_LIB, - "spark_cache_native_library_sha256": _SHA, - "spark_cache_native_arena_bytes": "12345", + "spark_cache_cuda_restore": "1", + "spark_cache_cuda_placement_library": _ABS_LIB, + "spark_cache_cuda_placement_library_sha256": _SHA, + "spark_cache_cuda_placement_arena_bytes": "12345", } ) with self.assertRaises(RuntimeError) as ctx: @@ -527,11 +605,11 @@ def test_native_restore_requires_valid_arena(self) -> None: def test_native_restore_requires_valid_io_workers(self) -> None: vllm, _ = _make_vllm_config( { - "spark_cache_native_restore": "1", - "spark_cache_native_library": _ABS_LIB, - "spark_cache_native_library_sha256": _SHA, - "spark_cache_native_arena_bytes": "67108864", - "spark_cache_native_io_workers": "50", + "spark_cache_cuda_restore": "1", + "spark_cache_cuda_placement_library": _ABS_LIB, + "spark_cache_cuda_placement_library_sha256": _SHA, + "spark_cache_cuda_placement_arena_bytes": "67108864", + "spark_cache_cuda_restore_io_workers": "50", } ) with self.assertRaises(RuntimeError) as ctx: diff --git a/sparkcache/test_spark_context_cache_connector.py b/sparkcache/test_spark_context_cache_connector.py index ce05137..43b7f03 100644 --- a/sparkcache/test_spark_context_cache_connector.py +++ b/sparkcache/test_spark_context_cache_connector.py @@ -1042,7 +1042,7 @@ def test_streaming_snapshot_feature_is_disabled_by_default(self) -> None: def test_streaming_snapshot_opt_in_fails_before_native_side_effects(self) -> None: with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex( - RuntimeError, "runtime installation failed closed" + RuntimeError, "runtime installation was rejected" ): _make_connector( Path(directory), @@ -1064,11 +1064,11 @@ def test_disabled_native_mode_ignores_stale_native_settings(self) -> None: Path(directory), 0, extra_config={ - "spark_cache_native_restore": "0", - "spark_cache_native_library": "not-absolute", - "spark_cache_native_library_sha256": "UPPERCASE", - "spark_cache_native_arena_bytes": "not-an-integer", - "spark_cache_native_io_workers": "also-invalid", + "spark_cache_cuda_restore": "0", + "spark_cache_cuda_placement_library": "not-absolute", + "spark_cache_cuda_placement_library_sha256": "UPPERCASE", + "spark_cache_cuda_placement_arena_bytes": "not-an-integer", + "spark_cache_cuda_restore_io_workers": "also-invalid", }, ) connector.register_kv_caches(_make_pools(8, 64)) @@ -1079,21 +1079,21 @@ def test_disabled_native_mode_ignores_stale_native_settings(self) -> None: def test_native_restore_requires_all_three_attested_settings(self) -> None: cases = ( {}, - {"spark_cache_native_library": "/tmp/placement.so"}, + {"spark_cache_cuda_placement_library": "/tmp/placement.so"}, { - "spark_cache_native_library": "/tmp/placement.so", - "spark_cache_native_library_sha256": "0" * 64, + "spark_cache_cuda_placement_library": "/tmp/placement.so", + "spark_cache_cuda_placement_library_sha256": "0" * 64, }, ) for missing in cases: with self.subTest(missing=missing): with tempfile.TemporaryDirectory() as directory: settings = { - "spark_cache_native_restore": "1", + "spark_cache_cuda_restore": "1", **missing, } with self.assertRaisesRegex( - RuntimeError, "native restore requires" + RuntimeError, "SparkCache CUDA restore requires" ): _make_connector(Path(directory), 0, extra_config=settings) @@ -1105,10 +1105,10 @@ def test_native_library_hash_failure_stops_registration(self) -> None: Path(directory), 0, extra_config={ - "spark_cache_native_restore": "1", - "spark_cache_native_library": str(artifact), - "spark_cache_native_library_sha256": "0" * 64, - "spark_cache_native_arena_bytes": str(64 * 1024 * 1024), + "spark_cache_cuda_restore": "1", + "spark_cache_cuda_placement_library": str(artifact), + "spark_cache_cuda_placement_library_sha256": "0" * 64, + "spark_cache_cuda_placement_arena_bytes": str(64 * 1024 * 1024), "spark_cache_load_threads": "2", }, ) @@ -1166,10 +1166,10 @@ def create(cls, library, **kwargs): Path(directory), 0, extra_config={ - "spark_cache_native_restore": "true", - "spark_cache_native_library": str(artifact), - "spark_cache_native_library_sha256": "a" * 64, - "spark_cache_native_arena_bytes": str(128 * 1024 * 1024), + "spark_cache_cuda_restore": "true", + "spark_cache_cuda_placement_library": str(artifact), + "spark_cache_cuda_placement_library_sha256": "a" * 64, + "spark_cache_cuda_placement_arena_bytes": str(128 * 1024 * 1024), }, ) with mock.patch.object( @@ -1196,10 +1196,10 @@ def test_scheduler_role_never_creates_a_native_adapter(self) -> None: Path(directory), 0, extra_config={ - "spark_cache_native_restore": "1", - "spark_cache_native_library": str(artifact), - "spark_cache_native_library_sha256": "b" * 64, - "spark_cache_native_arena_bytes": str(64 * 1024 * 1024), + "spark_cache_cuda_restore": "1", + "spark_cache_cuda_placement_library": str(artifact), + "spark_cache_cuda_placement_library_sha256": "b" * 64, + "spark_cache_cuda_placement_arena_bytes": str(64 * 1024 * 1024), }, role=KVConnectorRole.SCHEDULER, ) @@ -1617,9 +1617,7 @@ def test_page_delta_roots_start_independent_exact_restore_flights( connector.get_num_new_matched_tokens(divergent, 0), (1024, True) ) self.assertEqual(len(connector._restore_flights), 2) - self.assertNotIn( - divergent.request_id, connector._restore_flight_followers - ) + self.assertNotIn(divergent.request_id, connector._restore_flight_followers) self.assertTrue( connector._store.lookup(connector._identity(0), digest_a).is_hit ) @@ -4265,14 +4263,10 @@ def test_c16_distinct_roots_share_one_authenticated_trunk_restore(self) -> None: (None, False), ) - connector.update_state_after_alloc( - leader, self._blocks_stub(), self.SPAN - ) + connector.update_state_after_alloc(leader, self._blocks_stub(), self.SPAN) metadata = connector.build_connector_meta(_empty_scheduler_output()) self.assertEqual(len(metadata.plans), 1) - self.assertEqual( - metadata.plans[0].shared_segments, ((trunk_digest, 768),) - ) + self.assertEqual(metadata.plans[0].shared_segments, ((trunk_digest, 768),)) connector.update_connector_output( types.SimpleNamespace( invalid_block_ids=set(), finished_recving={leader.request_id} @@ -4332,9 +4326,7 @@ def test_same_root_follower_after_distinct_root_uses_selected_trunk(self) -> Non (trunk_digest, 768), ) - connector.update_state_after_alloc( - leader, self._blocks_stub(), self.SPAN - ) + connector.update_state_after_alloc(leader, self._blocks_stub(), self.SPAN) connector.build_connector_meta(_empty_scheduler_output()) connector.update_connector_output( types.SimpleNamespace( @@ -4510,7 +4502,9 @@ def test_one_rank_descriptor_disagreement_rejects_shared_trunk(self) -> None: self.assertEqual(results, [True, True, False, True]) - def test_segment_verification_failure_releases_distinct_root_followers(self) -> None: + def test_segment_verification_failure_releases_distinct_root_followers( + self, + ) -> None: with tempfile.TemporaryDirectory() as directory: connector = self._cohort_connector(Path(directory)) common = list(range(768)) @@ -4530,9 +4524,7 @@ def test_segment_verification_failure_releases_distinct_root_followers(self) -> ) connector.get_num_new_matched_tokens(leader, 0) connector.get_num_new_matched_tokens(follower, 0) - connector.update_state_after_alloc( - leader, self._blocks_stub(), self.SPAN - ) + connector.update_state_after_alloc(leader, self._blocks_stub(), self.SPAN) connector.build_connector_meta(_empty_scheduler_output()) connector.update_connector_output( From 19e2ec8b59c84ef359c2a3290f86962e3ff71d96 Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:55:20 -0500 Subject: [PATCH 3/4] Replace partial terminal pages in HMA deltas Page-semantic extensions accept authenticated chunk boundaries that fall inside a larger HMA page. The delta reuses only byte-identical page prefixes, so a changed boundary-intersecting page and every following page are immutable replacement data. This preserves the longest verified publication base instead of falling back to a full snapshot. Restore continues to authenticate the embedded base graph and reconstructed result before placement; invalid geometry or bytes remain a cache miss and recomputation. Canonical SparkCache CUDA restore and placement configuration remains the only generated configuration vocabulary. Deployable SparkCache source SHA-256: bc7cae86732c869ee8b2205d48ac5be6f580ee8b77a3e4ffd4c69dcd4f1bfae5. Cache namespace impact: none. CacheIdentity values, digest salts, chunk geometry, and page-delta wire schemas are unchanged. Validation: python -m pytest sparkcache -q (747 passed, 7 skipped); python -m pytest deploy -q (108 passed, 1 skipped); python -m ruff check .; source-attestation tests (2 passed). --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- sparkcache/README.md | 18 +- sparkcache/spark_context_cache_hybrid.py | 24 +-- sparkcache/test_defect_regressions.py | 179 ++++++++++++++++++ sparkcache/test_spark_context_cache_hybrid.py | 4 +- 6 files changed, 199 insertions(+), 30 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index e1f1dbe..35f820f 100644 --- a/deploy/deepseek_v4/tp4_profile.json +++ b/deploy/deepseek_v4/tp4_profile.json @@ -4,7 +4,7 @@ "cache_model_profile": "deepseek-v4-fp8-hma", "published_runtime_base": "ghcr.io/fujitsupolycom/gb10-vllm-serving@sha256:6fc26fdad81a18f0fff67ce0a05f6d90165625ea2e1cac8a6f39bfb462017028", "sparkcache": { - "source_sha256": "48e008ba0cbd12f1ffae1c28388ea83310f41c6219c955e13d63ab171290d8de" + "source_sha256": "bc7cae86732c869ee8b2205d48ac5be6f580ee8b77a3e4ffd4c69dcd4f1bfae5" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 91dcfe7..312be95 100644 --- a/deploy/glm52_35bpw/profile.json +++ b/deploy/glm52_35bpw/profile.json @@ -5,7 +5,7 @@ "published_runtime_base": "ghcr.io/fujitsupolycom/gb10-vllm-serving@sha256:6fc26fdad81a18f0fff67ce0a05f6d90165625ea2e1cac8a6f39bfb462017028", "base_image_requirement": "exact GLM-5.2 3.5-bpw R7 image recorded by the source container inspection", "sparkcache": { - "source_sha256": "48e008ba0cbd12f1ffae1c28388ea83310f41c6219c955e13d63ab171290d8de" + "source_sha256": "bc7cae86732c869ee8b2205d48ac5be6f580ee8b77a3e4ffd4c69dcd4f1bfae5" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/sparkcache/README.md b/sparkcache/README.md index 22576e7..b48f193 100644 --- a/sparkcache/README.md +++ b/sparkcache/README.md @@ -223,14 +223,16 @@ when placement completes and intentionally excludes that bookkeeping. chunks. - **Immutable block-page tails — implemented.** The `sparkcache-hybrid-page-delta/v1` codec reuses only byte-identical page - prefixes and binds the base snapshot, layout, block counts, and - recurrent/sliding boundary. `sparkcache-page-delta-manifest/v1` embeds its - authenticated base graph, allowing capacity maintenance to retain shared - objects after predecessor roots are removed. Restore reconstructs the verified - full snapshot before Python or native page placement. GPU-free regression - coverage exists; live model-serving qualification does not. A graph contains - at most two deltas. The following extension publishes a fresh flat snapshot, - bounding reconstruction work and metadata ancestry. + prefixes and binds the base snapshot, layout, block counts, and semantic + token boundaries. A boundary inside an HMA page replaces that complete page + while retaining earlier byte-identical pages. The + `sparkcache-page-delta-manifest/v1` schema embeds its authenticated base + graph, allowing capacity maintenance to retain shared objects after + predecessor roots are removed. Restore reconstructs the verified full + snapshot before Python or native page placement. GPU-free regression coverage + exists; live model-serving qualification does not. A graph contains at most + two deltas. The following extension publishes a fresh flat snapshot, bounding + reconstruction work and metadata ancestry. - **Concurrent shared GPU prefix — implemented.** One leader restores a persistent digest. After every rank succeeds, up to sixteen waiting followers attach through vLLM block references. Two leases may remain reusable for diff --git a/sparkcache/spark_context_cache_hybrid.py b/sparkcache/spark_context_cache_hybrid.py index 87cf6ad..7596f37 100644 --- a/sparkcache/spark_context_cache_hybrid.py +++ b/sparkcache/spark_context_cache_hybrid.py @@ -231,7 +231,6 @@ def decode_page_snapshot( def _validate_delta_boundaries( - layout: PageLayout, base_boundary_tokens: int, result_boundary_tokens: int, ) -> None: @@ -244,11 +243,6 @@ def _validate_delta_boundaries( or result_boundary_tokens <= base_boundary_tokens ): raise HybridCodecError("page delta boundaries are invalid") - for group in layout.groups: - if base_boundary_tokens % group.block_size: - raise HybridCodecError( - "page delta base boundary disagrees with group geometry" - ) def encode_page_delta( @@ -265,15 +259,13 @@ def encode_page_delta( Reuse is established page-by-page across every layer in a page group. A group reuses a page only when the result carries byte-identical opaque - state at the same logical page index. The base snapshot digest and both - semantic boundaries are bound into the delta header. + state at the same logical page index. When the base boundary lies inside + a page, changed bytes make that complete terminal page part of the delta; + preceding byte-identical pages remain reusable. The base snapshot digest + and both semantic boundaries are bound into the delta header. """ - _validate_delta_boundaries( - layout, - base_boundary_tokens, - result_boundary_tokens, - ) + _validate_delta_boundaries(base_boundary_tokens, result_boundary_tokens) base_counts = tuple(int(value) for value in base_block_counts) result_counts = tuple(int(value) for value in result_block_counts) if len(base_counts) != len(layout.groups) or len(result_counts) != len( @@ -354,11 +346,7 @@ def apply_page_delta( ) -> bytes: """Verify and apply one page-semantic delta to its exact base snapshot.""" - _validate_delta_boundaries( - layout, - base_boundary_tokens, - result_boundary_tokens, - ) + _validate_delta_boundaries(base_boundary_tokens, result_boundary_tokens) prefix_bytes = len(_DELTA_MAGIC) + _HEADER_LENGTH.size if ( len(encoded_delta) < prefix_bytes diff --git a/sparkcache/test_defect_regressions.py b/sparkcache/test_defect_regressions.py index 4cb4461..e2f68b7 100644 --- a/sparkcache/test_defect_regressions.py +++ b/sparkcache/test_defect_regressions.py @@ -844,6 +844,185 @@ def test_empty_group_delta_preserves_the_complete_request_table(self) -> None: ) +class DefectD16HybridPageBoundaryTests(unittest.TestCase): + """D-16: page deltas replace a partial terminal HMA page.""" + + def test_glm_page_delta_extends_a_chunk_boundary_inside_an_hma_page( + self, + ) -> None: + class FullAttentionSpec: + block_size = 256 + storage_block_size = 256 + page_size_bytes = 1024 + + class MambaSpec: + block_size = 2304 + storage_block_size = 2304 + page_size_bytes = 1024 + mamba_cache_mode = "align" + tokens_per_state = 2304 + num_speculative_blocks = 0 + num_prefill_checkpoint_blocks = 1 + + config = types.SimpleNamespace( + kv_cache_groups=( + types.SimpleNamespace( + kv_cache_spec=FullAttentionSpec(), + is_eagle_group=False, + layer_names=("full",), + ), + types.SimpleNamespace( + kv_cache_spec=MambaSpec(), + is_eagle_group=False, + layer_names=("recurrent",), + ), + ) + ) + with tempfile.TemporaryDirectory() as directory: + connector = _make_connector( + Path(directory), + 0, + block_size=256, + extra_config={ + "spark_cache_model_profile": "glm53-flash-hybrid", + "spark_cache_publication_schema": "tail-cow-v1", + }, + tp=1, + dcp=1, + kv_cache_config=config, + ) + pools = { + name: ( + torch.arange(256 * 1024, dtype=torch.int32) + .add(offset) + .remainder(251) + .to(torch.uint8) + .reshape(256, 1, 1024) + ) + for name, offset in (("full", 0), ("recurrent", 37)) + } + connector.register_kv_caches(pools) + tokens = tuple(range(12032)) + base_span = 7168 + result_span = 12032 + base_digest = connector._digest(list(tokens), base_span) + result_digest = connector._digest(list(tokens), result_span) + base_groups = (tuple(range(1, 29)), (70, 71, 72, 73)) + result_groups = (tuple(range(1, 48)), (70, 71, 72, 73, 74, 75)) + + connector._store_one( + _ReqPlan( + "glm-base-store", + base_digest, + base_span, + base_groups[0], + True, + block_ids_by_group=base_groups, + token_ids=tokens[:base_span], + ) + ) + expected_base = { + "full": pools["full"][list(base_groups[0])].clone(), + "recurrent": pools["recurrent"][[base_groups[1][-1]]].clone(), + } + base_destination = (tuple(range(128, 156)), (160, 161, 162, 163)) + pools["full"][list(base_destination[0])].zero_() + pools["recurrent"][base_destination[1][-1]].zero_() + self.assertTrue( + connector._load_one( + _ReqPlan( + "glm-base-restore", + base_digest, + base_span, + base_destination[0], + False, + block_ids_by_group=base_destination, + ) + ) + ) + self.assertTrue( + torch.equal( + pools["full"][list(base_destination[0])], + expected_base["full"], + ) + ) + self.assertTrue( + torch.equal( + pools["recurrent"][[base_destination[1][-1]]], + expected_base["recurrent"], + ) + ) + + expected_result = { + "full": pools["full"][list(result_groups[0])].clone(), + "recurrent": pools["recurrent"][[result_groups[1][-1]]].clone(), + } + extension_plan = _ReqPlan( + "glm-extension-store", + result_digest, + result_span, + result_groups[0], + True, + block_ids_by_group=result_groups, + token_ids=tokens, + base_context_digest=base_digest, + base_span_tokens=base_span, + ) + result_snapshot = connector._snapshot_hybrid_store(extension_plan) + connector._store_one(extension_plan) + + lookup = connector._store.lookup(connector._identity(0), result_digest) + self.assertTrue(lookup.is_hit, lookup.reason) + self.assertEqual(lookup.root_kind, "page_delta") + manifest = lookup._manifest + self.assertIsNotNone(manifest) + assert manifest is not None + self.assertEqual(manifest["base_block_counts"], [28, 1]) + self.assertEqual(manifest["result_block_counts"], [47, 1]) + delta_chunks = connector._store._read_context_chunks( + manifest["delta_chunks"], + connector._identity(0).required_records, + ) + encoded_delta = b"".join( + chunk.records[package_store.StateRecord.TARGET_CKV] + for chunk in delta_chunks + ) + self.assertLess( + len(encoded_delta), + len(result_snapshot.encoded_pages), + ) + result_destination = ( + tuple(range(176, 223)), + (230, 231, 232, 233, 234, 235), + ) + pools["full"][list(result_destination[0])].zero_() + pools["recurrent"][result_destination[1][-1]].zero_() + self.assertTrue( + connector._load_one( + _ReqPlan( + "glm-extension-restore", + result_digest, + result_span, + result_destination[0], + False, + block_ids_by_group=result_destination, + ) + ) + ) + self.assertTrue( + torch.equal( + pools["full"][list(result_destination[0])], + expected_result["full"], + ) + ) + self.assertTrue( + torch.equal( + pools["recurrent"][[result_destination[1][-1]]], + expected_result["recurrent"], + ) + ) + + class DigestNamespaceTests(unittest.TestCase): """D-4: context digests are identical across roles and physical ranks.""" diff --git a/sparkcache/test_spark_context_cache_hybrid.py b/sparkcache/test_spark_context_cache_hybrid.py index d0d1c15..009c6a1 100644 --- a/sparkcache/test_spark_context_cache_hybrid.py +++ b/sparkcache/test_spark_context_cache_hybrid.py @@ -140,14 +140,14 @@ def test_page_delta_rejects_wrong_base_corruption_and_unproven_boundary( base_boundary_tokens=256, result_boundary_tokens=512, ) - with self.assertRaisesRegex(HybridCodecError, "group geometry"): + with self.assertRaisesRegex(HybridCodecError, "boundaries are invalid"): encode_page_delta( layout, base, result, base_block_counts=(1, 1), result_block_counts=(2, 2), - base_boundary_tokens=257, + base_boundary_tokens=512, result_boundary_tokens=512, ) From 5ec6a9953ad5d39120298bbfc26e95a6fa4b1dc3 Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 00:15:28 -0500 Subject: [PATCH 4/4] Read page-delta chunks concurrently --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- .../cache_manifest.py | 11 +++--- .../test_cache_manifest.py | 35 +++++++++++++++++++ 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index 35f820f..7377823 100644 --- a/deploy/deepseek_v4/tp4_profile.json +++ b/deploy/deepseek_v4/tp4_profile.json @@ -4,7 +4,7 @@ "cache_model_profile": "deepseek-v4-fp8-hma", "published_runtime_base": "ghcr.io/fujitsupolycom/gb10-vllm-serving@sha256:6fc26fdad81a18f0fff67ce0a05f6d90165625ea2e1cac8a6f39bfb462017028", "sparkcache": { - "source_sha256": "bc7cae86732c869ee8b2205d48ac5be6f580ee8b77a3e4ffd4c69dcd4f1bfae5" + "source_sha256": "bc238f96e550c7ec27d4081dd1f2e741d404aaf5c8572d89ccc5e76812be4d63" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 312be95..14dc752 100644 --- a/deploy/glm52_35bpw/profile.json +++ b/deploy/glm52_35bpw/profile.json @@ -5,7 +5,7 @@ "published_runtime_base": "ghcr.io/fujitsupolycom/gb10-vllm-serving@sha256:6fc26fdad81a18f0fff67ce0a05f6d90165625ea2e1cac8a6f39bfb462017028", "base_image_requirement": "exact GLM-5.2 3.5-bpw R7 image recorded by the source container inspection", "sparkcache": { - "source_sha256": "bc7cae86732c869ee8b2205d48ac5be6f580ee8b77a3e4ffd4c69dcd4f1bfae5" + "source_sha256": "bc238f96e550c7ec27d4081dd1f2e741d404aaf5c8572d89ccc5e76812be4d63" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/sparkcache/persistent_context_cache/cache_manifest.py b/sparkcache/persistent_context_cache/cache_manifest.py index fcf8d85..764aa63 100644 --- a/sparkcache/persistent_context_cache/cache_manifest.py +++ b/sparkcache/persistent_context_cache/cache_manifest.py @@ -2263,8 +2263,7 @@ def _read_context_chunks( descriptors: Sequence[Mapping[str, Any]], required: frozenset[StateRecord], ) -> tuple[ContextChunk, ...]: - result = [] - for descriptor in descriptors: + def _read_one(descriptor: Mapping[str, Any]) -> ContextChunk: encoded = ( self.root / "chunks" / f"{descriptor['sha256']}.spcc" ).read_bytes() @@ -2280,8 +2279,12 @@ def _read_context_chunks( or chunk.logical_end != descriptor["logical_end"] ): raise CacheFormatError("chunk range disagrees with descriptor") - result.append(chunk) - return tuple(result) + return chunk + + if not descriptors: + return () + with ThreadPoolExecutor(max_workers=min(8, len(descriptors))) as pool: + return tuple(pool.map(_read_one, descriptors)) def publish_prefix_aliases( self, diff --git a/sparkcache/persistent_context_cache/test_cache_manifest.py b/sparkcache/persistent_context_cache/test_cache_manifest.py index a028828..cf7dfac 100644 --- a/sparkcache/persistent_context_cache/test_cache_manifest.py +++ b/sparkcache/persistent_context_cache/test_cache_manifest.py @@ -98,6 +98,41 @@ def _clear_once_in_subprocess( class ManifestStoreTests(unittest.TestCase): + def test_page_delta_chunk_reads_overlap_and_preserve_descriptor_order( + self, + ) -> None: + identity = _identity() + chunks = (_chunk(0, 256), _chunk(256, 512)) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + store = ManifestStore(root) + digest = hashlib.sha256(b"parallel-page-delta-reads").hexdigest() + store.commit( + identity=identity, + context_digest=digest, + chunks=chunks, + span_tokens=512, + ) + manifest_path = ( + root / "manifests" / identity.storage_key / f"{digest}.json" + ) + descriptors = json.loads(manifest_path.read_bytes())["chunks"] + overlap = threading.Barrier(len(descriptors), timeout=2.0) + original_read_bytes = Path.read_bytes + + def read_with_overlap(path: Path) -> bytes: + if path.suffix == ".spcc": + overlap.wait() + return original_read_bytes(path) + + with mock.patch.object(Path, "read_bytes", read_with_overlap): + restored = store._read_context_chunks( + descriptors, + identity.required_records, + ) + + self.assertEqual(restored, chunks) + def test_page_extension_materializes_full_snapshot_after_base_root_removal( self, ) -> None: