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 01/15] 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 02/15] 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 @@

08 Practical effects at C1, C Prefix and concurrencyComparisonRecorded effect 8,192 tokens · C1qualified Python page restore147.2–194.0 ms cache service per rank - 16,384 tokens · C8Python/Torch vs native placement9.45–10.64 s vs 1.2–2.1 s client latency + 16,384 tokens · C8Python/Torch vs SparkCache CUDA placement9.45–10.64 s vs 1.2–2.1 s client latency 131,072 tokens · C1reconstruction pipeline vs cold direct restore1.29–1.46 s vs 131–250 ms cache service per rank 131,072 tokens · C16independent vs shared restore16 × 813 MB became 1 × 813 MB per rank; the host-warm restore took 104–165 ms per rank; standard-chat client p50 fell from 3.363 s to 2.980 s 131,072 tokens · pretokenized C16standalone measurement2.698 s client p50 and 2.701 s maximum; not compared with chat timing @@ -540,7 +540,7 @@

08 Practical effects at C1, C

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 03/15] 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 04/15] 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: From b37a2e1a38c4941da3b4d1cb72d0931eb48948e9 Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 00:54:57 -0500 Subject: [PATCH 05/15] Group page deltas into authenticated macro objects Per-file overhead dominated the 256K restore because physical delta objects followed the 256-token logical boundary. Page-delta manifest v2 stores ordered 64-MiB authenticated extents and reads or publishes them in bounded batches. Version 1 remains readable; cache identity, digest salts, logical chunk geometry, page sharing, and the page-tail namespace are unchanged. GPU-free tests cover exact restore, corruption, bounded memory, file count, compatibility, and capacity behavior. --- README.md | 11 + deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- sparkcache/README.md | 19 +- .../cache_manifest.py | 284 ++++++++++---- .../test_cache_manifest.py | 4 +- .../test_page_delta_macro_objects.py | 346 ++++++++++++++++++ sparkcache/test_defect_regressions.py | 11 +- .../test_spark_context_cache_connector.py | 2 +- 9 files changed, 586 insertions(+), 95 deletions(-) create mode 100644 sparkcache/persistent_context_cache/test_page_delta_macro_objects.py diff --git a/README.md b/README.md index b947429..004b12c 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,17 @@ byte-identical opaque pages. Restore reconstructs and verifies the complete snapshot before Python or native page placement. Arbitrary earlier-prefix aliases cannot be derived from opaque page snapshots. +Page-delta publication writes `sparkcache-page-delta-manifest/v2` metadata over +authenticated byte extents of at most 64 MiB. This physical grouping reduces +the 1,024 delta files implied by a 262,144-token logical boundary to at most 24 +objects for a 1,575,821,491-byte delta. Reads retain at most four extent +payloads in addition to one assembled delta buffer. The logical admission and +digest boundary remains 256 tokens. Version 1 page-delta manifests remain +readable; cache identity, digest salts, and the `page-tail-cow-v1` namespace do +not change. Restore still materializes one authenticated delta buffer and the +verified reconstructed snapshot before placement. Direct placement from base +and delta extents is unsupported by this schema. + Opaque HMA snapshots cannot be shortened by truncating chunk lists. SparkCache therefore uses the page-semantic format and distinct namespace described above. At most two page deltas may form one graph; the following publication compacts diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index 7377823..45a2848 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": "bc238f96e550c7ec27d4081dd1f2e741d404aaf5c8572d89ccc5e76812be4d63" + "source_sha256": "9b8b2a6863d91f07354dab67d608cc15f551f1a5a7682b89873c7ae6ba468ee5" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 14dc752..30fa0a5 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": "bc238f96e550c7ec27d4081dd1f2e741d404aaf5c8572d89ccc5e76812be4d63" + "source_sha256": "9b8b2a6863d91f07354dab67d608cc15f551f1a5a7682b89873c7ae6ba468ee5" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/sparkcache/README.md b/sparkcache/README.md index b48f193..b41fdc4 100644 --- a/sparkcache/README.md +++ b/sparkcache/README.md @@ -226,12 +226,19 @@ when placement completes and intentionally excludes that bookkeeping. 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 + `sparkcache-page-delta-manifest/v2` schema embeds its authenticated base + graph and groups delta bytes into immutable objects of at most 64 MiB. A + 1,575,821,491-byte delta therefore uses at most 24 physical delta objects + instead of 1,024 objects derived from logical token chunks. Ordered restore + batches retain at most four object payloads in addition to one assembled + delta buffer. Version 1 manifests remain readable. Cache identity, digest + salts, the 256-token logical boundary, and the `page-tail-cow-v1` namespace + are unchanged. Capacity maintenance retains 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. Direct placement from base and delta + extents is unsupported by this schema. 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 diff --git a/sparkcache/persistent_context_cache/cache_manifest.py b/sparkcache/persistent_context_cache/cache_manifest.py index 764aa63..9946de4 100644 --- a/sparkcache/persistent_context_cache/cache_manifest.py +++ b/sparkcache/persistent_context_cache/cache_manifest.py @@ -42,6 +42,18 @@ _PREFIX_ALIAS_SCHEMA = "sparkcache-prefix-alias/v1" _TAIL_MANIFEST_SCHEMA = "sparkcache-tail-manifest/v1" _PAGE_DELTA_MANIFEST_SCHEMA = "sparkcache-page-delta-manifest/v1" +_PAGE_DELTA_MANIFEST_SCHEMA_V2 = "sparkcache-page-delta-manifest/v2" +_PAGE_DELTA_MANIFEST_SCHEMAS = frozenset( + (_PAGE_DELTA_MANIFEST_SCHEMA, _PAGE_DELTA_MANIFEST_SCHEMA_V2) +) +# The v2 physical geometry is independent of the 256-token digest and +# admission boundary. A 64-MiB extent reduces a 1.58-GB delta to 24 objects; +# bounded batches cap temporary payload bytes at 128 MiB while publishing and +# 256 MiB while reading, in addition to the assembled authenticated delta. +_PAGE_DELTA_OBJECT_BYTES = 64 * 1024 * 1024 +_MAX_PAGE_DELTA_OBJECT_BYTES = 64 * 1024 * 1024 +_PAGE_DELTA_WRITE_BATCH_SIZE = 2 +_PAGE_DELTA_READ_BATCH_SIZE = 4 # Two delta roots cap reconstruction at two full-snapshot applications. A # following extension is compacted by the connector into a fresh flat root. _MAX_PAGE_DELTA_DEPTH = 2 @@ -140,6 +152,13 @@ class PageDeltaDepthExceeded(ValueError): """Another page delta would exceed the bounded reconstruction depth.""" +def _is_page_delta_root(value: Any) -> bool: + return ( + isinstance(value, Mapping) + and value.get("schema") in _PAGE_DELTA_MANIFEST_SCHEMAS + ) + + @dataclass(frozen=True) class CacheIdentity: target_checkpoint: str @@ -1139,6 +1158,19 @@ def _validate_page_delta_root( ) -> tuple[Mapping[str, Any], tuple[Mapping[str, Any], ...]]: if not isinstance(manifest, dict): raise CacheFormatError("page delta manifest is not an object") + schema = manifest.get("schema") + if schema == _PAGE_DELTA_MANIFEST_SCHEMA: + delta_keys = {"delta_chunks"} + elif schema == _PAGE_DELTA_MANIFEST_SCHEMA_V2: + delta_keys = { + "delta_encoded_bytes", + "delta_object_bytes", + "delta_objects", + "delta_sha256", + "logical_chunk_tokens", + } + else: + raise _IncompatibleManifestError("page delta manifest schema differs") _strict_keys( manifest, { @@ -1154,8 +1186,8 @@ def _validate_page_delta_root( "layout_sha256", "base_block_counts", "result_block_counts", - "delta_chunks", "metadata_sha256", + *delta_keys, }, "page delta manifest", ) @@ -1171,8 +1203,7 @@ def _validate_page_delta_root( if _sha256(_canonical_json(authenticated)) != metadata_digest: raise CacheFormatError("page delta metadata checksum mismatch") if ( - manifest["schema"] != _PAGE_DELTA_MANIFEST_SCHEMA - or manifest["format_abi"] != FORMAT_ABI + manifest["format_abi"] != FORMAT_ABI or identity.publication_schema != "page-tail-cow-v1" or manifest["identity"] != identity.to_wire() or manifest["context_digest"] != context_digest @@ -1191,20 +1222,71 @@ def _validate_page_delta_root( or len(manifest["base_block_counts"]) != len(manifest["result_block_counts"]) ): raise CacheFormatError("page delta manifest geometry differs") - delta_chunks = manifest["delta_chunks"] - synthetic = { - "format_abi": FORMAT_ABI, - "identity": identity.to_wire(), - "context_digest": context_digest, - "committed_tokens": manifest["committed_tokens"], - "chunks": delta_chunks, - } - descriptors = _validate_manifest_metadata( - synthetic, - EntryKey(identity.storage_key, context_digest), - expected_identity=identity, - ) - return manifest["base_root"], descriptors + if schema == _PAGE_DELTA_MANIFEST_SCHEMA: + synthetic = { + "format_abi": FORMAT_ABI, + "identity": identity.to_wire(), + "context_digest": context_digest, + "committed_tokens": manifest["committed_tokens"], + "chunks": manifest["delta_chunks"], + } + descriptors = _validate_manifest_metadata( + synthetic, + EntryKey(identity.storage_key, context_digest), + expected_identity=identity, + ) + return manifest["base_root"], descriptors + + try: + _validate_digest(manifest["delta_sha256"], "page delta payload sha256") + except ValueError as error: + raise CacheFormatError(str(error)) from error + encoded_bytes = manifest["delta_encoded_bytes"] + object_bytes = manifest["delta_object_bytes"] + if ( + type(encoded_bytes) is not int + or encoded_bytes <= 0 + or type(object_bytes) is not int + or not 0 < object_bytes <= _MAX_PAGE_DELTA_OBJECT_BYTES + or manifest["logical_chunk_tokens"] != identity.chunk_tokens + ): + raise CacheFormatError("page delta object geometry differs") + objects = manifest["delta_objects"] + if not isinstance(objects, list) or not objects: + raise CacheFormatError("page delta object descriptors are invalid") + expected_start = 0 + descriptors: list[Mapping[str, Any]] = [] + for index, descriptor in enumerate(objects): + if not isinstance(descriptor, dict): + raise CacheFormatError("page delta object descriptor is not an object") + _strict_keys( + descriptor, + {"sha256", "bytes", "encoded_start", "encoded_end"}, + "page delta object descriptor", + ) + try: + _validate_digest(descriptor["sha256"], "page delta object sha256") + except ValueError as error: + raise CacheFormatError(str(error)) from error + size = descriptor["bytes"] + start = descriptor["encoded_start"] + end = descriptor["encoded_end"] + if ( + type(size) is not int + or type(start) is not int + or type(end) is not int + or size <= 0 + or start != expected_start + or end != start + size + or (index < len(objects) - 1 and size != object_bytes) + or size > object_bytes + ): + raise CacheFormatError("page delta object descriptor geometry differs") + descriptors.append(descriptor) + expected_start = end + if expected_start != encoded_bytes: + raise CacheFormatError("page delta object coverage differs") + return manifest["base_root"], tuple(descriptors) def _decode_chunk( @@ -1270,6 +1352,29 @@ def _decode_chunk( raise CacheFormatError(str(error)) from error +def _read_page_delta_object_batch( + object_root: Path, + descriptors: Sequence[Mapping[str, Any]], +) -> tuple[bytes, ...]: + """Read one bounded batch of authenticated page-delta byte extents.""" + + def read_one(descriptor: Mapping[str, Any]) -> bytes: + encoded = (object_root / f"{descriptor['sha256']}.spcc").read_bytes() + if ( + len(encoded) != descriptor["bytes"] + or _sha256(encoded) != descriptor["sha256"] + ): + raise CacheFormatError("page delta object checksum mismatch") + return encoded + + if not descriptors: + return () + with ThreadPoolExecutor( + max_workers=min(len(descriptors), _PAGE_DELTA_READ_BATCH_SIZE) + ) as pool: + return tuple(pool.map(read_one, descriptors)) + + class ManifestTransaction: """Incrementally publish chunks, then expose them with one final manifest. @@ -1551,7 +1656,9 @@ def _capacity_entry(self, path: Path) -> _CapacityEntry: manifest = json.loads(path.read_bytes()) segments: tuple[str, ...] = () schema_name = manifest.get("schema") if isinstance(manifest, dict) else None - if schema_name in (_TAIL_MANIFEST_SCHEMA, _PAGE_DELTA_MANIFEST_SCHEMA): + if schema_name == _TAIL_MANIFEST_SCHEMA or schema_name in ( + _PAGE_DELTA_MANIFEST_SCHEMAS + ): identity_wire = dict(manifest.get("identity", {})) if "record_schema" in identity_wire: schema = identity_wire["record_schema"] @@ -2229,7 +2336,7 @@ def _page_graph_descriptors( context_digest=context_digest, ) base_digest = manifest["base_context_digest"] - if base_root.get("schema") == _PAGE_DELTA_MANIFEST_SCHEMA: + if _is_page_delta_root(base_root): base_chunks = self._page_graph_descriptors( base_root, identity=identity, @@ -2251,8 +2358,7 @@ def _page_delta_root_count(manifest: Mapping[str, Any]) -> int: count = 0 root: Any = manifest while ( - isinstance(root, Mapping) - and root.get("schema") == _PAGE_DELTA_MANIFEST_SCHEMA + _is_page_delta_root(root) ): count += 1 root = root.get("base_root") @@ -2286,6 +2392,38 @@ def _read_one(descriptor: Mapping[str, Any]) -> ContextChunk: with ThreadPoolExecutor(max_workers=min(8, len(descriptors))) as pool: return tuple(pool.map(_read_one, descriptors)) + def _read_page_delta_objects( + self, + descriptors: Sequence[Mapping[str, Any]], + *, + encoded_bytes: int, + encoded_sha256: str, + ) -> bytearray: + """Read ordered macro objects with bounded transient payload memory.""" + + if encoded_bytes <= 0 or not descriptors: + raise CacheFormatError("page delta object coverage is empty") + result = bytearray(encoded_bytes) + digest = hashlib.sha256() + expected_start = 0 + object_root = self.root / "chunks" + for first in range(0, len(descriptors), _PAGE_DELTA_READ_BATCH_SIZE): + batch = tuple( + descriptors[first : first + _PAGE_DELTA_READ_BATCH_SIZE] + ) + payloads = _read_page_delta_object_batch(object_root, batch) + for descriptor, payload in zip(batch, payloads, strict=True): + start = int(descriptor["encoded_start"]) + end = int(descriptor["encoded_end"]) + if start != expected_start or end != start + len(payload): + raise CacheFormatError("page delta object coverage differs") + result[start:end] = payload + digest.update(payload) + expected_start = end + if expected_start != encoded_bytes or digest.hexdigest() != encoded_sha256: + raise CacheFormatError("page delta payload checksum mismatch") + return result + def publish_prefix_aliases( self, *, @@ -2620,11 +2758,9 @@ def commit_page_extension( ) from sparkcache.spark_context_cache_codec import ( context_prefix_digest, - pack_positions, ) from sparkcache.spark_context_cache_hybrid import ( encode_page_delta, - split_snapshot, ) with _RootGuard(self.root, shared=True, blocking=True): @@ -2668,50 +2804,35 @@ def commit_page_extension( base_boundary_tokens=base_boundary_tokens, result_boundary_tokens=result_boundary_tokens, ) - part_count = ( - result_boundary_tokens + identity.chunk_tokens - 1 - ) // identity.chunk_tokens - parts = split_snapshot(delta, part_count) - chunks = tuple( - ContextChunk( - index * identity.chunk_tokens, - min( - result_boundary_tokens, - (index + 1) * identity.chunk_tokens, - ), - { - StateRecord.LOGICAL_POSITIONS: pack_positions( - range( - index * identity.chunk_tokens, - min( - result_boundary_tokens, - (index + 1) * identity.chunk_tokens, - ), - ) - ), - StateRecord.TARGET_CKV: part, - }, - ) - for index, part in enumerate(parts) - ) descriptors: list[dict[str, Any]] = [] objects: list[tuple[Path, bytes]] = [] - for chunk in chunks: - encoded = _encode_chunk(chunk) - digest = _sha256(encoded) + delta_view = memoryview(delta) + for start in range(0, len(delta), _PAGE_DELTA_OBJECT_BYTES): + end = min(len(delta), start + _PAGE_DELTA_OBJECT_BYTES) + encoded = delta_view[start:end].tobytes() + object_digest = _sha256(encoded) descriptors.append( { - "sha256": digest, + "sha256": object_digest, "bytes": len(encoded), - "logical_start": chunk.logical_start, - "logical_end": chunk.logical_end, + "encoded_start": start, + "encoded_end": end, } ) - objects.append((self.root / "chunks" / f"{digest}.spcc", encoded)) - _publish_immutable_batch(objects) + objects.append( + ( + self.root / "chunks" / f"{object_digest}.spcc", + encoded, + ) + ) + if len(objects) == _PAGE_DELTA_WRITE_BATCH_SIZE: + _publish_immutable_batch(objects) + objects.clear() + if objects: + _publish_immutable_batch(objects) base_root = dict(base._manifest) root = { - "schema": _PAGE_DELTA_MANIFEST_SCHEMA, + "schema": _PAGE_DELTA_MANIFEST_SCHEMA_V2, "format_abi": FORMAT_ABI, "identity": identity.to_wire(), "context_digest": result_context_digest, @@ -2723,7 +2844,11 @@ def commit_page_extension( "layout_sha256": layout.digest, "base_block_counts": list(base_block_counts), "result_block_counts": list(result_block_counts), - "delta_chunks": descriptors, + "delta_encoded_bytes": len(delta), + "delta_object_bytes": _PAGE_DELTA_OBJECT_BYTES, + "delta_objects": descriptors, + "delta_sha256": _sha256(delta), + "logical_chunk_tokens": identity.chunk_tokens, } root["metadata_sha256"] = _sha256(_canonical_json(root)) encoded_root = _canonical_json(root) @@ -2759,7 +2884,7 @@ def restore_page_snapshot( if not lookup.is_hit or lookup._manifest is None: raise ValueError("cannot restore a cache miss") manifest = lookup._manifest - if manifest.get("schema") != _PAGE_DELTA_MANIFEST_SCHEMA: + if not _is_page_delta_root(manifest): chunks = self.restore(lookup) if chunks is None: raise CacheFormatError("page snapshot restore failed") @@ -2788,7 +2913,7 @@ def restore_page_snapshot( _manifest=base_root, root_kind=( "page_delta" - if base_root.get("schema") == _PAGE_DELTA_MANIFEST_SCHEMA + if _is_page_delta_root(base_root) else "manifest" ), ) @@ -2799,13 +2924,20 @@ def restore_page_snapshot( result_boundary_tokens=manifest["base_committed_tokens"], _depth=_depth + 1, ) - delta_chunks = self._read_context_chunks( - delta_descriptors, - identity.required_records, - ) - encoded_delta = b"".join( - chunk.records[StateRecord.TARGET_CKV] for chunk in delta_chunks - ) + if manifest["schema"] == _PAGE_DELTA_MANIFEST_SCHEMA_V2: + encoded_delta = self._read_page_delta_objects( + delta_descriptors, + encoded_bytes=manifest["delta_encoded_bytes"], + encoded_sha256=manifest["delta_sha256"], + ) + else: + delta_chunks = self._read_context_chunks( + delta_descriptors, + identity.required_records, + ) + encoded_delta = b"".join( + chunk.records[StateRecord.TARGET_CKV] for chunk in delta_chunks + ) from sparkcache.spark_context_cache_hybrid import apply_page_delta return apply_page_delta( @@ -2930,10 +3062,7 @@ def lookup( identity=identity, context_digest=context_digest, ) - is_page_delta = ( - isinstance(manifest, dict) - and manifest.get("schema") == _PAGE_DELTA_MANIFEST_SCHEMA - ) + is_page_delta = _is_page_delta_root(manifest) if is_page_delta: chunks = self._page_graph_descriptors( manifest, @@ -2949,8 +3078,6 @@ def lookup( for descriptor in chunks: digest = descriptor["sha256"] encoded_bytes = descriptor["bytes"] - logical_start = descriptor["logical_start"] - logical_end = descriptor["logical_end"] if verify_chunks: encoded_chunk = ( self.root / "chunks" / f"{digest}.spcc" @@ -2960,6 +3087,10 @@ def lookup( or _sha256(encoded_chunk) != digest ): raise CacheFormatError("chunk checksum mismatch") + if "encoded_start" in descriptor: + continue + logical_start = descriptor["logical_start"] + logical_end = descriptor["logical_end"] # The descriptor digest authenticates the complete encoded # chunk: prefix, header (including record digests and # offsets), and every payload byte. Re-hashing each record @@ -3154,9 +3285,8 @@ def invalidate( try: manifest = json.loads(raw) schema_name = manifest.get("schema") - if schema_name in ( - _TAIL_MANIFEST_SCHEMA, - _PAGE_DELTA_MANIFEST_SCHEMA, + if schema_name == _TAIL_MANIFEST_SCHEMA or schema_name in ( + _PAGE_DELTA_MANIFEST_SCHEMAS ): identity_wire = dict(manifest["identity"]) if "record_schema" in identity_wire: diff --git a/sparkcache/persistent_context_cache/test_cache_manifest.py b/sparkcache/persistent_context_cache/test_cache_manifest.py index cf7dfac..b3309c1 100644 --- a/sparkcache/persistent_context_cache/test_cache_manifest.py +++ b/sparkcache/persistent_context_cache/test_cache_manifest.py @@ -220,7 +220,7 @@ def test_page_extension_materializes_full_snapshot_after_base_root_removal( page_root_path, *( root / "chunks" / f"{item['sha256']}.spcc" - for item in page_root["delta_chunks"] + for item in page_root["delta_objects"] ), ] self.assertGreaterEqual( @@ -250,7 +250,7 @@ def test_page_extension_materializes_full_snapshot_after_base_root_removal( chained_root_path, *( root / "chunks" / f"{item['sha256']}.spcc" - for item in chained_root["delta_chunks"] + for item in chained_root["delta_objects"] ), ] self.assertGreaterEqual( diff --git a/sparkcache/persistent_context_cache/test_page_delta_macro_objects.py b/sparkcache/persistent_context_cache/test_page_delta_macro_objects.py new file mode 100644 index 0000000..f77ff54 --- /dev/null +++ b/sparkcache/persistent_context_cache/test_page_delta_macro_objects.py @@ -0,0 +1,346 @@ +from __future__ import annotations + +import dataclasses +import hashlib +import json +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +import sparkcache.persistent_context_cache.cache_manifest as cache_manifest +from sparkcache.persistent_context_cache.cache_manifest import ( + CacheIdentity, + ContextChunk, + ManifestStore, + StateRecord, +) +from sparkcache.spark_context_cache_codec import context_prefix_digest, pack_positions +from sparkcache.spark_context_cache_hybrid import ( + PageGroup, + PageLayer, + PageLayout, + encode_page_snapshot, + split_snapshot, +) + + +def _identity() -> CacheIdentity: + return CacheIdentity( + target_checkpoint="1" * 64, + draft_checkpoint="2" * 64, + quantization_layout="nvfp4-ds-mla-v1", + rope_layout="glm53-hybrid-v1", + tp_degree=4, + dcp_degree=1, + chunk_tokens=256, + record_schema=("target_ckv", "logical_positions"), + publication_schema="page-tail-cow-v1", + ) + + +@dataclasses.dataclass(frozen=True) +class _Fixture: + identity: CacheIdentity + layout: PageLayout + tokens: tuple[int, ...] + salt: str + base_digest: str + result_digest: str + result_snapshot: bytes + result_tokens: int + result_blocks: int + + +def _commit_fixture(store: ManifestStore, *, result_blocks: int = 16) -> _Fixture: + identity = _identity() + layout = PageLayout( + (PageGroup(256, (PageLayer("page", "u8", (1024,), 1024),)),) + ) + result_tokens = result_blocks * 256 + tokens = tuple(range(result_tokens)) + salt = "page-delta-macro-object-test" + base_digest = context_prefix_digest(tokens, salt, token_count=256) + result_digest = context_prefix_digest(tokens, salt, token_count=result_tokens) + base_snapshot = encode_page_snapshot(layout, (1,), {"page": b"A" * 1024}) + result_payload = b"A" * 1024 + b"".join( + bytes((index % 251,)) * 1024 for index in range(1, result_blocks) + ) + result_snapshot = encode_page_snapshot( + layout, + (result_blocks,), + {"page": result_payload}, + ) + store.commit( + identity=identity, + context_digest=base_digest, + chunks=( + ContextChunk( + 0, + 256, + { + StateRecord.LOGICAL_POSITIONS: pack_positions(range(256)), + StateRecord.TARGET_CKV: base_snapshot, + }, + ), + ), + span_tokens=256, + ) + store.commit_page_extension( + identity=identity, + base_context_digest=base_digest, + token_ids=tokens, + identity_salt=salt, + layout=layout, + base_block_counts=(1,), + result_block_counts=(result_blocks,), + base_boundary_tokens=256, + result_boundary_tokens=result_tokens, + result_snapshot=result_snapshot, + ) + return _Fixture( + identity=identity, + layout=layout, + tokens=tokens, + salt=salt, + base_digest=base_digest, + result_digest=result_digest, + result_snapshot=result_snapshot, + result_tokens=result_tokens, + result_blocks=result_blocks, + ) + + +def _manifest(root: Path, fixture: _Fixture) -> tuple[Path, dict[str, object]]: + path = ( + root + / "manifests" + / fixture.identity.storage_key + / f"{fixture.result_digest}.json" + ) + return path, json.loads(path.read_bytes()) + + +class PageDeltaMacroObjectTests(unittest.TestCase): + def test_v2_uses_fewer_physical_objects_than_logical_token_chunks(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + store = ManifestStore(root) + published_batch_sizes: list[int] = [] + original_publish = cache_manifest._publish_immutable_batch + + def record_publish(objects: list[tuple[Path, bytes]]) -> None: + published_batch_sizes.append(len(objects)) + original_publish(objects) + + with ( + mock.patch.object(cache_manifest, "_PAGE_DELTA_OBJECT_BYTES", 2048), + mock.patch.object( + cache_manifest, + "_publish_immutable_batch", + side_effect=record_publish, + ), + ): + fixture = _commit_fixture(store) + + _path, manifest = _manifest(root, fixture) + objects = manifest["delta_objects"] + self.assertEqual(manifest["schema"], "sparkcache-page-delta-manifest/v2") + self.assertEqual(manifest["logical_chunk_tokens"], 256) + self.assertLess(len(objects), fixture.result_tokens // 256) + self.assertLess( + manifest["delta_encoded_bytes"], + len(fixture.result_snapshot), + ) + base_descriptor = manifest["base_root"]["chunks"][0] + self.assertTrue( + (root / "chunks" / f"{base_descriptor['sha256']}.spcc").is_file() + ) + self.assertLessEqual( + max(published_batch_sizes), + cache_manifest._PAGE_DELTA_WRITE_BATCH_SIZE, + ) + self.assertEqual( + [item["encoded_start"] for item in objects], + [0, *[item["encoded_end"] for item in objects[:-1]]], + ) + lookup = store.lookup(fixture.identity, fixture.result_digest) + self.assertTrue(lookup.is_hit, lookup.reason) + self.assertEqual( + store.restore_page_snapshot( + lookup, + layout=fixture.layout, + result_block_counts=(fixture.result_blocks,), + result_boundary_tokens=fixture.result_tokens, + ), + fixture.result_snapshot, + ) + + def test_live_scale_payload_needs_at_most_24_macro_objects(self) -> None: + encoded_bytes = 1_575_821_491 + + object_count = ( + encoded_bytes + cache_manifest._PAGE_DELTA_OBJECT_BYTES - 1 + ) // cache_manifest._PAGE_DELTA_OBJECT_BYTES + + self.assertEqual(object_count, 24) + self.assertLess(object_count, 1_024) + + def test_corrupt_macro_object_fails_closed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + store = ManifestStore(root) + with mock.patch.object(cache_manifest, "_PAGE_DELTA_OBJECT_BYTES", 2048): + fixture = _commit_fixture(store) + _path, manifest = _manifest(root, fixture) + descriptor = manifest["delta_objects"][0] + object_path = root / "chunks" / f"{descriptor['sha256']}.spcc" + encoded = object_path.read_bytes() + object_path.write_bytes(encoded[:-1] + bytes((encoded[-1] ^ 0xFF,))) + + lookup = store.lookup(fixture.identity, fixture.result_digest) + + self.assertFalse(lookup.is_hit) + self.assertEqual(lookup.reason, "corrupt") + + def test_corrupt_macro_descriptor_fails_closed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + store = ManifestStore(root) + with mock.patch.object(cache_manifest, "_PAGE_DELTA_OBJECT_BYTES", 2048): + fixture = _commit_fixture(store) + manifest_path, manifest = _manifest(root, fixture) + manifest["delta_objects"][0]["encoded_end"] += 1 + manifest_path.write_bytes(cache_manifest._canonical_json(manifest)) + + lookup = store.lookup(fixture.identity, fixture.result_digest) + + self.assertFalse(lookup.is_hit) + self.assertEqual(lookup.reason, "corrupt") + + def test_restore_reads_only_one_bounded_object_batch_at_a_time(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + store = ManifestStore(root) + with mock.patch.object(cache_manifest, "_PAGE_DELTA_OBJECT_BYTES", 2048): + fixture = _commit_fixture(store, result_blocks=24) + _path, manifest = _manifest(root, fixture) + self.assertGreater( + len(manifest["delta_objects"]), + cache_manifest._PAGE_DELTA_READ_BATCH_SIZE, + ) + observed_batch_sizes: list[int] = [] + original = cache_manifest._read_page_delta_object_batch + + def record_batch( + object_root: Path, + descriptors: tuple[dict[str, object], ...], + ) -> tuple[bytes, ...]: + observed_batch_sizes.append(len(descriptors)) + return original(object_root, descriptors) + + lookup = store.lookup( + fixture.identity, + fixture.result_digest, + verify_chunks=False, + ) + with mock.patch.object( + cache_manifest, + "_read_page_delta_object_batch", + side_effect=record_batch, + ): + restored = store.restore_page_snapshot( + lookup, + layout=fixture.layout, + result_block_counts=(fixture.result_blocks,), + result_boundary_tokens=fixture.result_tokens, + ) + + self.assertEqual(restored, fixture.result_snapshot) + self.assertGreater(len(observed_batch_sizes), 1) + self.assertLessEqual( + max(observed_batch_sizes), + cache_manifest._PAGE_DELTA_READ_BATCH_SIZE, + ) + + def test_v1_page_delta_remains_readable(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + store = ManifestStore(root) + with mock.patch.object(cache_manifest, "_PAGE_DELTA_OBJECT_BYTES", 2048): + fixture = _commit_fixture(store) + manifest_path, manifest = _manifest(root, fixture) + encoded_delta = store._read_page_delta_objects( + manifest["delta_objects"], + encoded_bytes=manifest["delta_encoded_bytes"], + encoded_sha256=manifest["delta_sha256"], + ) + parts = split_snapshot( + encoded_delta, + fixture.result_tokens // fixture.identity.chunk_tokens, + ) + descriptors = [] + for index, part in enumerate(parts): + chunk = ContextChunk( + index * fixture.identity.chunk_tokens, + (index + 1) * fixture.identity.chunk_tokens, + { + StateRecord.LOGICAL_POSITIONS: pack_positions( + range( + index * fixture.identity.chunk_tokens, + (index + 1) * fixture.identity.chunk_tokens, + ) + ), + StateRecord.TARGET_CKV: bytes(part), + }, + ) + encoded = cache_manifest._encode_chunk(chunk) + digest = hashlib.sha256(encoded).hexdigest() + cache_manifest._publish_immutable( + root / "chunks" / f"{digest}.spcc", + encoded, + ) + descriptors.append( + { + "sha256": digest, + "bytes": len(encoded), + "logical_start": chunk.logical_start, + "logical_end": chunk.logical_end, + } + ) + legacy = { + key: value + for key, value in manifest.items() + if key + not in { + "delta_objects", + "delta_encoded_bytes", + "delta_object_bytes", + "delta_sha256", + "logical_chunk_tokens", + "metadata_sha256", + } + } + legacy["schema"] = "sparkcache-page-delta-manifest/v1" + legacy["delta_chunks"] = descriptors + legacy["metadata_sha256"] = hashlib.sha256( + cache_manifest._canonical_json(legacy) + ).hexdigest() + manifest_path.write_bytes(cache_manifest._canonical_json(legacy)) + + lookup = store.lookup(fixture.identity, fixture.result_digest) + + self.assertTrue(lookup.is_hit, lookup.reason) + self.assertEqual( + store.restore_page_snapshot( + lookup, + layout=fixture.layout, + result_block_counts=(fixture.result_blocks,), + result_boundary_tokens=fixture.result_tokens, + ), + fixture.result_snapshot, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/sparkcache/test_defect_regressions.py b/sparkcache/test_defect_regressions.py index e2f68b7..0fb626a 100644 --- a/sparkcache/test_defect_regressions.py +++ b/sparkcache/test_defect_regressions.py @@ -979,13 +979,10 @@ class MambaSpec: 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 + encoded_delta = connector._store._read_page_delta_objects( + manifest["delta_objects"], + encoded_bytes=manifest["delta_encoded_bytes"], + encoded_sha256=manifest["delta_sha256"], ) self.assertLess( len(encoded_delta), diff --git a/sparkcache/test_spark_context_cache_connector.py b/sparkcache/test_spark_context_cache_connector.py index 43b7f03..343a1b4 100644 --- a/sparkcache/test_spark_context_cache_connector.py +++ b/sparkcache/test_spark_context_cache_connector.py @@ -606,7 +606,7 @@ def native_placement(**kwargs): ).read_bytes() ) delta_path = ( - root / "chunks" / f"{manifest['delta_chunks'][0]['sha256']}.spcc" + root / "chunks" / f"{manifest['delta_objects'][0]['sha256']}.spcc" ) damaged = bytearray(delta_path.read_bytes()) damaged[-1] ^= 1 From 08e297769a796da2668ea58d0ed5c0d9b588565b Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 00:18:14 -0500 Subject: [PATCH 06/15] Consume proven recurrent replay-boundary blocks SparkCache opts into vLLM recurrent-boundary hand-offs and accepts them only when request, group, token boundary, recurrent topology, and non-null physical block agree. Every aligned recurrent group must have exactly one proven block before a store plan is created. Absent, incomplete, contradictory, or preemption-stale metadata cancels publication. SparkCache never scans arithmetic, running, or speculative slots for a substitute. GPU-free coverage exercises the 6,912-token boundary with seven DFlash verification slots through SparkContextCacheConnector and ManifestStore, including malformed coverage and request-lifetime cleanup. The exact GLM vLLM lease contract accepts and requires the recurrent-boundary runtime postimages and SchedulerOutput interface. Cache namespace impact: none. CacheIdentity values, digest salts, chunk geometry, manifest schemas, and page-delta wire bytes are unchanged. Deployable SparkCache source SHA-256: 01cc59bf2c45af60f02813b771a484e65829e4f464eb27aa1756ca7067c82c9f. Validation: python -m pytest sparkcache -q (752 passed, 7 skipped); python -m pytest deploy -q (108 passed, 1 skipped); python -m ruff check .; source-attestation tests (2 passed). One concurrent validation run exceeded an existing sub-millisecond timing assertion; the isolated test and the following full SparkCache run passed. --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- .../source-receipt.json | 2 +- sparkcache/README.md | 21 +- ...st_glm53_b12x_kda_adaptive_mtp_contract.py | 27 +- ...-contract-glm53-b12x-kda-adaptive-mtp.json | 37 ++- sparkcache/spark_context_cache_connector.py | 162 +++++++++- sparkcache/test_defect_regressions.py | 296 ++++++++++++++++++ 8 files changed, 517 insertions(+), 32 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index 45a2848..155efff 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": "9b8b2a6863d91f07354dab67d608cc15f551f1a5a7682b89873c7ae6ba468ee5" + "source_sha256": "88633ef676b4dfe258a6fa9b788ddeb22cad68349d0cae0c503ee404d1724f7b" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 30fa0a5..35f1571 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": "9b8b2a6863d91f07354dab67d608cc15f551f1a5a7682b89873c7ae6ba468ee5" + "source_sha256": "88633ef676b4dfe258a6fa9b788ddeb22cad68349d0cae0c503ee404d1724f7b" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json index 2296e78..b32aa50 100644 --- a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json +++ b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json @@ -62,7 +62,7 @@ ], "contract": { "path": "sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json", - "sha256": "6defde9551cbb586fd09bb2d3020495531b6573397875a767eaae1dbad126024" + "sha256": "45d7a92b38b836a4f829f02df85e339cfeea860e1080e4663a8340af6c125125" }, "result": "All four patches apply in order to the LF source tree, and the eleven-file SparkCache contract verifies the resulting source bytes, including the live-tensor B12X KDA implementation." } diff --git a/sparkcache/README.md b/sparkcache/README.md index b41fdc4..3998788 100644 --- a/sparkcache/README.md +++ b/sparkcache/README.md @@ -225,9 +225,15 @@ when placement completes and intentionally excludes that bookkeeping. `sparkcache-hybrid-page-delta/v1` codec reuses only byte-identical page 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/v2` schema embeds its authenticated base - graph and groups delta bytes into immutable objects of at most 64 MiB. A + while retaining earlier byte-identical pages. For an aligned recurrent group, + vLLM may retain the replay-boundary page outside the advancing request block + table. Its `SchedulerOutput.recurrent_boundary_blocks` hand-off names the + pinned physical block by request, group, and token boundary. SparkCache uses + that block only after all three identities and the recurrent topology match; + missing or contradictory metadata skips publication rather than scanning + later running or speculative state. The + `sparkcache-page-delta-manifest/v2` schema embeds its authenticated base graph + and groups delta bytes into immutable objects of at most 64 MiB. A 1,575,821,491-byte delta therefore uses at most 24 physical delta objects instead of 1,024 objects derived from logical token chunks. Ordered restore batches retain at most four object payloads in addition to one assembled @@ -235,10 +241,11 @@ when placement completes and intentionally excludes that bookkeeping. salts, the 256-token logical boundary, and the `page-tail-cow-v1` namespace are unchanged. Capacity maintenance retains 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. Direct placement from base and delta - extents is unsupported by this schema. A graph contains at most two deltas. - The following extension publishes a fresh flat snapshot, bounding + Python/Torch or SparkCache CUDA placement. GPU-free regression coverage + exists; live model-serving qualification does not. Direct placement from + base and delta extents is unsupported by this schema. 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 diff --git a/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py b/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py index bceaaf6..a7547e7 100644 --- a/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py +++ b/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py @@ -19,6 +19,13 @@ CONTAINERFILE = ROOT / "deploy/glm53_flash/Containerfile.b12x-kda-adaptive-mtp" VLLM_COMMIT = "0b67266a0f37d6146a8403fb8482403c62f412d5" SOURCE_ROLE = "source_built_glm53_b12x_kda_adaptive_mtp" +RECURRENT_BOUNDARY_ROLE = "recurrent_boundary_contract" +RECURRENT_BOUNDARY_FILES = { + "vllm/v1/core/kv_cache_manager.py", + "vllm/v1/core/sched/output.py", + "vllm/v1/core/sched/scheduler.py", + "vllm/v1/core/single_type_kv_cache_manager.py", +} KDA_PATH = "vllm/model_executor/layers/mamba/gdn/kimi_gdn_linear_attn.py" E105_KDA_SHA256 = ( "a879af0081f69ba8288ef909e1d69b5bbb85bdff7e5aa0d3c11ad892bfea8410" @@ -71,12 +78,24 @@ def test_glm53_b12x_kda_adaptive_mtp_contract_attests_the_complete_sparkcache_vl "vllm/v1/kv_cache_interface.py", KDA_PATH, } - assert all( - set(record["accepted_sha256"]) == {SOURCE_ROLE} - for record in contract["files"] - ) + for record in contract["files"]: + expected_roles = {SOURCE_ROLE} + if record["path"] in RECURRENT_BOUNDARY_FILES: + expected_roles.add(RECURRENT_BOUNDARY_ROLE) + assert set(record["accepted_sha256"]) == expected_roles assert all(record["required_symbols"] for record in contract["files"]) + by_path = {record["path"]: record for record in contract["files"]} + assert by_path["vllm/v1/core/sched/output.py"]["accepted_sha256"][ + RECURRENT_BOUNDARY_ROLE + ] == "9911b3f9d21815a185285852b5a6176e5484e1ab0ff5c30f7caaa68ea0fab543" + assert "SchedulerOutput.recurrent_boundary_blocks" in by_path[ + "vllm/v1/core/sched/output.py" + ]["required_symbols"] + assert "KVCacheManager.take_recurrent_boundary_blocks" in by_path[ + "vllm/v1/core/kv_cache_manager.py" + ]["required_symbols"] + def test_glm53_b12x_kda_contract_rejects_the_e105_kda_source( monkeypatch: pytest.MonkeyPatch, diff --git a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json index 02e8007..9ac1dd6 100644 --- a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json +++ b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json @@ -24,9 +24,10 @@ ] }, { - "path": "vllm/v1/core/sched/scheduler.py", - "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "6d397c97f31e67a75efc01b5ddd89fa58db425de14fa43965ef2d6146b6b9bdb" + "path": "vllm/v1/core/sched/scheduler.py", + "accepted_sha256": { + "source_built_glm53_b12x_kda_adaptive_mtp": "6d397c97f31e67a75efc01b5ddd89fa58db425de14fa43965ef2d6146b6b9bdb", + "recurrent_boundary_contract": "260f36ce8fabf70c193b20009ea465eea7b1b6c8e9fb72f2307a01ba8fcf7b2a" }, "required_symbols": [ "Scheduler.schedule", @@ -39,9 +40,10 @@ ] }, { - "path": "vllm/v1/core/kv_cache_manager.py", - "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "ee03dc9ce2b720c0be6e9f572d23580ba96eff68fe3406250557e83071654af0" + "path": "vllm/v1/core/kv_cache_manager.py", + "accepted_sha256": { + "source_built_glm53_b12x_kda_adaptive_mtp": "ee03dc9ce2b720c0be6e9f572d23580ba96eff68fe3406250557e83071654af0", + "recurrent_boundary_contract": "c5b83d382c96b2bf8c466a993ed77123a14a971e2661797128533319388d0b5f" }, "required_symbols": [ "KVCacheManager.get_block_ids", @@ -51,17 +53,20 @@ "KVCacheManager.expire_shared_prefix_leases", "KVCacheManager.discard_shared_prefix_lease", "KVCacheManager.evict_shared_prefix_leases_until_free", - "KVCacheManager.take_kv_cache_block_copies" + "KVCacheManager.take_kv_cache_block_copies", + "KVCacheManager.take_recurrent_boundary_blocks" ] }, { - "path": "vllm/v1/core/sched/output.py", - "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "65235eba652e5a3ccee18bf3cbfeac9bf4da8fb9c61e961580f612cfb7e593bc" + "path": "vllm/v1/core/sched/output.py", + "accepted_sha256": { + "source_built_glm53_b12x_kda_adaptive_mtp": "65235eba652e5a3ccee18bf3cbfeac9bf4da8fb9c61e961580f612cfb7e593bc", + "recurrent_boundary_contract": "9911b3f9d21815a185285852b5a6176e5484e1ab0ff5c30f7caaa68ea0fab543" }, "required_symbols": [ "SchedulerOutput.preempted_req_ids", - "SchedulerOutput.kv_cache_block_copies" + "SchedulerOutput.kv_cache_block_copies", + "SchedulerOutput.recurrent_boundary_blocks" ] }, { @@ -96,9 +101,10 @@ ] }, { - "path": "vllm/v1/core/single_type_kv_cache_manager.py", - "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "e4b1c5c38b63b708fd55aa40a9ab0d008b266d006a63dcfcef55890ac1371cb8" + "path": "vllm/v1/core/single_type_kv_cache_manager.py", + "accepted_sha256": { + "source_built_glm53_b12x_kda_adaptive_mtp": "e4b1c5c38b63b708fd55aa40a9ab0d008b266d006a63dcfcef55890ac1371cb8", + "recurrent_boundary_contract": "f67a1850a7e0288baaa6d42e7ec55b22b09c156720767e23acaabedcae333c8a" }, "required_symbols": [ "SingleTypeKVCacheManager.add_local_computed_blocks", @@ -107,7 +113,8 @@ "SingleTypeKVCacheManager.take_pending_cow_copies", "SingleTypeKVCacheManager.pop_blocks_for_free", "MambaManager.remove_skipped_blocks", - "MambaManager.allocate_new_blocks" + "MambaManager.allocate_new_blocks", + "SingleTypeKVCacheManager.take_pending_aligned_recurrent_boundaries" ] }, { diff --git a/sparkcache/spark_context_cache_connector.py b/sparkcache/spark_context_cache_connector.py index 09375bf..22b98fd 100644 --- a/sparkcache/spark_context_cache_connector.py +++ b/sparkcache/spark_context_cache_connector.py @@ -194,6 +194,11 @@ class _ReqPlan: # stored prefix. Empty fields select ordinary full-snapshot publication. base_context_digest: str = "" base_span_tokens: int = 0 + # vLLM may retain a recurrent replay-boundary page outside the request's + # arithmetic block-table slot after later forward work advances the live + # state. Each pair is an exact (group index, physical block id) override + # proven by vLLM for this plan's span_tokens boundary. + recurrent_boundary_blocks: tuple[tuple[int, int], ...] = () # Authenticated row-prefix roots whose descriptors must match the leading # descriptors of this plan. Workers validate these roots before the # leader's blocks may back a shorter shared-prefix lease. @@ -533,6 +538,11 @@ def is_empty(self) -> bool: class SparkContextCacheConnector(KVConnectorBase_V1, SupportsHMA): """Store/restore each rank's DCP shard on rank-local NVMe.""" + # Exact-vLLM runtimes use this opt-in before pinning and exporting aligned + # recurrent replay-boundary blocks. wait_for_save synchronously detaches + # every referenced page before request/preemption cleanup may release it. + supports_recurrent_boundary_blocks = True + configure_streaming_snapshot_runtime = staticmethod( configure_streaming_snapshot_runtime ) @@ -743,6 +753,9 @@ def __init__( # checkpoint/test adapters that seed that tuple remain compatible. self._store_token_ids: dict[str, tuple[int, ...]] = {} self._store_bases: dict[str, tuple[str, int]] = {} + self._store_recurrent_boundaries: dict[ + str, tuple[tuple[int, int], ...] + ] = {} self.counters: dict[str, int] = { "store_committed": 0, "store_failed": 0, @@ -751,6 +764,7 @@ def __init__( "store_skipped_present": 0, "store_skipped_quorum": 0, "page_delta_compactions": 0, + "recurrent_boundary_metadata_rejected": 0, "prefix_alias_publication_attempted": 0, "prefix_alias_publication_failed": 0, "prefix_aliases_published": 0, @@ -1002,11 +1016,33 @@ def _select_group_blocks_for_span( self, groups: tuple[tuple[int, ...], ...], span_tokens: int, + *, + recurrent_boundary_blocks: Sequence[tuple[int, int]] = (), ) -> tuple[tuple[int, ...], ...]: if len(groups) != len(self._group_topology): raise HybridCodecError("request block tables disagree with page groups") + overrides: dict[int, int] = {} + for entry in recurrent_boundary_blocks: + if ( + not isinstance(entry, (list, tuple)) + or len(entry) != 2 + or any(type(value) is not int for value in entry) + ): + raise HybridCodecError("recurrent boundary override is malformed") + group_index, block_id = entry + if ( + not 0 <= group_index < len(self._group_topology) + or group_index in overrides + or block_id <= 0 + or self._group_topology[group_index]["reuse_policy"] + != "recurrent_align" + ): + raise HybridCodecError("recurrent boundary override is incompatible") + overrides[group_index] = block_id trimmed = [] - for group, topology in zip(groups, self._group_topology): + for group_index, (group, topology) in enumerate( + zip(groups, self._group_topology) + ): block_size = int(topology["block_size"]) required = (span_tokens + block_size - 1) // block_size if len(group) < required: @@ -1039,7 +1075,10 @@ def _select_group_blocks_for_span( # SparkCache manifest names one exact full span; its constituent # chunks are never matched as independent prefixes, so only the # reuse window at that span's final boundary is required. - chosen = group[required - selected : required] + if policy == "recurrent_align" and group_index in overrides: + chosen = (overrides[group_index],) + else: + chosen = group[required - selected : required] if any(block <= 0 for block in chosen): raise HybridCodecError( "selected page window contains vLLM's null block" @@ -1569,6 +1608,76 @@ def _append_streaming_snapshot_offer( ) ) + def _validated_recurrent_boundary_blocks( + self, + scheduler_output: "SchedulerOutput", + request_id: str, + boundary_tokens: int, + ) -> tuple[tuple[int, int], ...] | None: + """Validate vLLM's exact recurrent replay-boundary block hand-off. + + An empty tuple is valid only when the registered topology has no + aligned recurrent group. None means the metadata is absent, + incomplete, or contradictory, so publication must be skipped. + SparkCache never derives a replacement from another non-null table + entry because later entries can hold running or speculative state + beyond ``boundary_tokens``. + """ + + def reject(reason: str) -> None: + self.counters["recurrent_boundary_metadata_rejected"] += 1 + logger.warning( + "spark-context-cache: recurrent boundary metadata rejected" + " request=%s boundary=%d: %s", + request_id, + boundary_tokens, + reason, + ) + return None + + required_groups = { + group_index + for group_index, topology in enumerate(self._group_topology) + if topology["reuse_policy"] == "recurrent_align" + } + if not required_groups: + return () + raw = getattr(scheduler_output, "recurrent_boundary_blocks", None) + if raw is None: + return reject("vLLM supplied no recurrent boundary mapping") + if not isinstance(raw, Mapping): + return reject("top-level value is not a mapping") + entries = raw.get(request_id) + if entries is None: + return reject("request has no recurrent boundary entries") + if not isinstance(entries, (list, tuple)): + return reject("request value is not a sequence") + + overrides: list[tuple[int, int]] = [] + seen_groups: set[int] = set() + for entry in entries: + if not isinstance(entry, (list, tuple)) or len(entry) != 3: + return reject("entry is not a group, block, boundary triple") + group_index, block_id, entry_boundary = entry + if any(type(value) is not int for value in entry): + return reject("entry values are not integers") + if not 0 <= group_index < len(self._group_topology): + return reject("group index is outside the registered topology") + if group_index in seen_groups: + return reject("multiple blocks claim the same recurrent group") + topology = self._group_topology[group_index] + if topology["reuse_policy"] != "recurrent_align": + return reject("group is not an aligned recurrent cache") + if block_id <= 0: + return reject("physical block is vLLM's null block") + if entry_boundary != boundary_tokens: + return reject("entry boundary differs from the store plan") + seen_groups.add(group_index) + overrides.append((group_index, block_id)) + if seen_groups != required_groups: + return reject("entries do not cover every aligned recurrent group") + return tuple(sorted(overrides)) + def build_connector_meta( self, scheduler_output: "SchedulerOutput" ) -> KVConnectorMetadata: @@ -1577,6 +1686,11 @@ def build_connector_meta( sorted(getattr(scheduler_output, "preempted_req_ids", None) or ()) ) ) + # vLLM releases request-lifetime recurrent boundary pins on preemption. + # Keep token/table accumulation for a possible resume, but require a + # fresh hash-proven hand-off before the resumed request may publish. + for request_id in meta.preempted_request_ids: + self._store_recurrent_boundaries.pop(request_id, None) for request_id, ( digest, span, @@ -1627,6 +1741,15 @@ def build_connector_meta( if self._has_full_quorum(digest): self.counters["store_skipped_quorum"] += 1 continue + recurrent_boundary_blocks = ( + self._validated_recurrent_boundary_blocks( + scheduler_output, + req_id, + span, + ) + ) + if recurrent_boundary_blocks is None: + continue already = new_req.num_computed_tokens + scheduled if self._streaming_snapshots_enabled: self._append_streaming_snapshot_offer( @@ -1648,6 +1771,10 @@ def build_connector_meta( [list(group) for group in group_blocks], ) self._store_token_ids[req_id] = exact_token_ids + if recurrent_boundary_blocks: + self._store_recurrent_boundaries[req_id] = ( + recurrent_boundary_blocks + ) elif already >= span: meta.plans.append( _ReqPlan( @@ -1660,6 +1787,9 @@ def build_connector_meta( token_ids=exact_token_ids, base_context_digest=base_digest, base_span_tokens=base_span, + recurrent_boundary_blocks=( + recurrent_boundary_blocks + ), ) ) else: @@ -1673,6 +1803,10 @@ def build_connector_meta( [list(group) for group in group_blocks], ) self._store_token_ids[req_id] = exact_token_ids + if recurrent_boundary_blocks: + self._store_recurrent_boundaries[req_id] = ( + recurrent_boundary_blocks + ) if base_digest: self._store_bases[req_id] = (base_digest, base_span) cached = scheduler_output.scheduled_cached_reqs @@ -1682,6 +1816,19 @@ def build_connector_meta( digest, span, done, blocks_by_group = self._store_progress[req_id] exact_token_ids = self._store_token_ids.get(req_id, ()) base_digest, base_span = self._store_bases.get(req_id, ("", 0)) + recurrent_boundary_blocks = self._validated_recurrent_boundary_blocks( + scheduler_output, + req_id, + span, + ) + if recurrent_boundary_blocks is None: + del self._store_progress[req_id] + self._store_token_ids.pop(req_id, None) + self._store_bases.pop(req_id, None) + self._store_recurrent_boundaries.pop(req_id, None) + continue + if recurrent_boundary_blocks: + self._store_recurrent_boundaries[req_id] = recurrent_boundary_blocks new_block_ids = cached.new_block_ids[index] appended = ( [ @@ -1715,6 +1862,7 @@ def build_connector_meta( del self._store_progress[req_id] self._store_token_ids.pop(req_id, None) self._store_bases.pop(req_id, None) + self._store_recurrent_boundaries.pop(req_id, None) if self._has_full_quorum(digest): self.counters["store_skipped_quorum"] += 1 continue @@ -1737,6 +1885,10 @@ def build_connector_meta( del self._store_progress[req_id] self._store_token_ids.pop(req_id, None) self._store_bases.pop(req_id, None) + recurrent_boundary_blocks = self._store_recurrent_boundaries.pop( + req_id, + (), + ) if self._has_full_quorum(digest): self.counters["store_skipped_quorum"] += 1 continue @@ -1752,6 +1904,7 @@ def build_connector_meta( token_ids=exact_token_ids, base_context_digest=base_digest, base_span_tokens=base_span, + recurrent_boundary_blocks=recurrent_boundary_blocks, ) ) else: @@ -3491,6 +3644,7 @@ def request_finished( self._store_progress.pop(request_id, None) self._store_token_ids.pop(request_id, None) self._store_bases.pop(request_id, None) + self._store_recurrent_boundaries.pop(request_id, None) runtime = self._streaming_runtime if runtime is None: return False, None @@ -3747,7 +3901,9 @@ def _snapshot_hybrid_store(self, plan: _ReqPlan) -> _HybridStoreSnapshot: if layout is None: raise RuntimeError("block-page layout was not registered") groups = self._select_group_blocks_for_span( - plan.group_block_ids, plan.span_tokens + plan.group_block_ids, + plan.span_tokens, + recurrent_boundary_blocks=plan.recurrent_boundary_blocks, ) if len(groups) != len(layout.groups): raise HybridCodecError("request block tables disagree with page groups") diff --git a/sparkcache/test_defect_regressions.py b/sparkcache/test_defect_regressions.py index 0fb626a..70e4bd2 100644 --- a/sparkcache/test_defect_regressions.py +++ b/sparkcache/test_defect_regressions.py @@ -1020,6 +1020,302 @@ class MambaSpec: ) +class DefectD17RecurrentBoundaryMetadataTests(unittest.TestCase): + """D-17: vLLM identifies off-table recurrent replay boundaries.""" + + BOUNDARY = 6912 + PROMPT_TOKENS = 6992 + BOUNDARY_BLOCK = 42 + + @staticmethod + def _config() -> types.SimpleNamespace: + class FullAttentionSpec: + block_size = 2304 + storage_block_size = 2304 + page_size_bytes = 64 + + class MambaSpec: + block_size = 2304 + storage_block_size = 2304 + page_size_bytes = 64 + mamba_cache_mode = "align" + tokens_per_state = 2304 + num_speculative_blocks = 7 + num_prefill_checkpoint_blocks = 0 + + return 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",), + ), + ) + ) + + @classmethod + def _tables(cls) -> tuple[tuple[int, ...], ...]: + # The recurrent replay-boundary slot is null after vLLM advances the + # running state. Entries 71..78 are the later running state and seven + # DFlash verification slots, so none can substitute for block 42. + return ((11, 12, 13, 14), (0, 0, 0, 71, 72, 73, 74, 75, 76, 77, 78)) + + @classmethod + def _scheduler_output( + cls, + recurrent_boundary_blocks: object = None, + ) -> types.SimpleNamespace: + request_id = "dflash-recurrent-boundary" + output = types.SimpleNamespace( + scheduled_new_reqs=[ + types.SimpleNamespace( + req_id=request_id, + prompt_token_ids=list(range(cls.PROMPT_TOKENS)), + block_ids=cls._tables(), + num_computed_tokens=0, + ) + ], + scheduled_cached_reqs=types.SimpleNamespace( + req_ids=[], + resumed_req_ids=set(), + num_computed_tokens=[], + new_block_ids=[], + ), + num_scheduled_tokens={request_id: cls.PROMPT_TOKENS}, + preempted_req_ids=set(), + ) + if recurrent_boundary_blocks is not None: + output.recurrent_boundary_blocks = recurrent_boundary_blocks + return output + + def test_explicit_boundary_block_round_trips_through_manifest_store(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + config = self._config() + scheduler = _make_connector( + root, + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=config, + extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, + ) + output = self._scheduler_output( + { + "dflash-recurrent-boundary": [ + (1, self.BOUNDARY_BLOCK, self.BOUNDARY) + ] + } + ) + + self.assertTrue(scheduler.supports_recurrent_boundary_blocks) + metadata = scheduler.build_connector_meta(output) + + self.assertEqual(len(metadata.plans), 1) + plan = metadata.plans[0] + self.assertEqual(plan.span_tokens, self.BOUNDARY) + self.assertEqual(plan.recurrent_boundary_blocks, ((1, 42),)) + worker = _make_connector( + root, + 0, + block_size=256, + tp=1, + dcp=1, + kv_cache_config=config, + extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, + ) + pools = { + name: ( + torch.arange(128 * 64, dtype=torch.int32) + .add(offset) + .remainder(251) + .to(torch.uint8) + .reshape(128, 1, 64) + ) + for name, offset in (("full", 0), ("recurrent", 29)) + } + worker.register_kv_caches(pools) + expected_full = pools["full"][[11, 12, 13]].clone() + expected_recurrent = pools["recurrent"][[self.BOUNDARY_BLOCK]].clone() + + worker._store_one(plan) + + lookup = worker._store.lookup(worker._identity(0), plan.digest) + self.assertTrue(lookup.is_hit, lookup.reason) + destination = ( + (90, 91, 92), + (0, 0, 93, 94, 95, 96, 97, 98, 99, 100, 101), + ) + pools["full"][[90, 91, 92]].zero_() + pools["recurrent"][[93]].zero_() + self.assertTrue( + worker._load_one( + _ReqPlan( + "dflash-recurrent-restore", + plan.digest, + self.BOUNDARY, + destination[0], + False, + block_ids_by_group=destination, + ) + ) + ) + self.assertTrue(torch.equal(pools["full"][[90, 91, 92]], expected_full)) + self.assertTrue( + torch.equal(pools["recurrent"][[93]], expected_recurrent) + ) + + def test_missing_or_wrong_request_metadata_skips_publication(self) -> None: + for boundary_metadata in ( + None, + {"another-request": [(1, self.BOUNDARY_BLOCK, self.BOUNDARY)]}, + ): + with self.subTest(boundary_metadata=boundary_metadata): + with tempfile.TemporaryDirectory() as directory: + connector = _make_connector( + Path(directory), + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=self._config(), + extra_config={ + "spark_cache_model_profile": "glm53-flash-hybrid" + }, + ) + output = self._scheduler_output(boundary_metadata) + full, recurrent = output.scheduled_new_reqs[0].block_ids + recurrent = list(recurrent) + recurrent[2] = 69 # stale or recycled, not boundary-proven + output.scheduled_new_reqs[0].block_ids = (full, tuple(recurrent)) + metadata = connector.build_connector_meta(output) + self.assertEqual(metadata.plans, []) + self.assertEqual( + connector.counters[ + "recurrent_boundary_metadata_rejected" + ], + 1, + ) + + def test_contradictory_boundary_metadata_skips_publication(self) -> None: + invalid_entries = ( + [], + [(1, self.BOUNDARY_BLOCK, self.BOUNDARY - 256)], + [(2, self.BOUNDARY_BLOCK, self.BOUNDARY)], + [(1, self.BOUNDARY_BLOCK, self.BOUNDARY), (1, 43, self.BOUNDARY)], + [(1, 0, self.BOUNDARY)], + [(0, self.BOUNDARY_BLOCK, self.BOUNDARY)], + ) + for entries in invalid_entries: + with self.subTest(entries=entries): + with tempfile.TemporaryDirectory() as directory: + connector = _make_connector( + Path(directory), + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=self._config(), + extra_config={ + "spark_cache_model_profile": "glm53-flash-hybrid" + }, + ) + metadata = connector.build_connector_meta( + self._scheduler_output( + {"dflash-recurrent-boundary": entries} + ) + ) + self.assertEqual(metadata.plans, []) + self.assertEqual( + connector.counters[ + "recurrent_boundary_metadata_rejected" + ], + 1, + ) + + def test_partial_recurrent_group_coverage_skips_publication(self) -> None: + config = self._config() + second_recurrent = types.SimpleNamespace( + kv_cache_spec=config.kv_cache_groups[1].kv_cache_spec, + is_eagle_group=False, + layer_names=("recurrent-2",), + ) + config.kv_cache_groups = (*config.kv_cache_groups, second_recurrent) + output = self._scheduler_output( + {"dflash-recurrent-boundary": [(1, 42, self.BOUNDARY)]} + ) + output.scheduled_new_reqs[0].block_ids = ( + *self._tables(), + (0, 0, 0, 81, 82, 83, 84, 85, 86, 87, 88), + ) + with tempfile.TemporaryDirectory() as directory: + connector = _make_connector( + Path(directory), + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=config, + extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, + ) + + metadata = connector.build_connector_meta(output) + + self.assertEqual(metadata.plans, []) + self.assertEqual( + connector.counters["recurrent_boundary_metadata_rejected"], + 1, + ) + + def test_preemption_discards_request_lifetime_boundary_block(self) -> None: + with tempfile.TemporaryDirectory() as directory: + connector = _make_connector( + Path(directory), + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=self._config(), + extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, + ) + request_id = "dflash-recurrent-boundary" + connector._store_recurrent_boundaries[request_id] = ( + (1, self.BOUNDARY_BLOCK), + ) + output = types.SimpleNamespace( + scheduled_new_reqs=[], + scheduled_cached_reqs=types.SimpleNamespace( + req_ids=[], + resumed_req_ids=set(), + num_computed_tokens=[], + new_block_ids=[], + ), + num_scheduled_tokens={}, + preempted_req_ids={request_id}, + ) + + metadata = connector.build_connector_meta(output) + + self.assertEqual(metadata.preempted_request_ids, (request_id,)) + self.assertNotIn(request_id, connector._store_recurrent_boundaries) + + class DigestNamespaceTests(unittest.TestCase): """D-4: context digests are identical across roles and physical ranks.""" From 49c517ed76e09dd2f7e78eb3ad5fe83382bda6fb Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:38:22 -0500 Subject: [PATCH 07/15] Make the recurrent lease state coherent The GLM recurrent lease document now names one final runtime state across all eleven files. Four recurrent postimages and seven unchanged files therefore share a coherent verifier state; the impossible pre-producer state is no longer advertised.\n\nThe four SparkCache patches remain exact preimages for the separate recurrent producer. Add regression coverage for coherent final-state verification and state that the final contract is valid only after producer composition.\n\nCache namespace impact: none. CacheIdentity values, digest salts, chunk geometry, manifest schemas, and page-delta bytes are unchanged.\n\nValidation: python -m pytest sparkcache -q (760 passed, 7 skipped); python -m pytest deploy -q (108 passed, 1 skipped); python -m ruff check . --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- .../source-receipt.json | 4 +- sparkcache/runtime_patches/README.md | 8 ++- ...st_glm53_b12x_kda_adaptive_mtp_contract.py | 63 +++++++++++++++---- ...-contract-glm53-b12x-kda-adaptive-mtp.json | 18 +++--- 6 files changed, 67 insertions(+), 30 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index 155efff..c586573 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": "88633ef676b4dfe258a6fa9b788ddeb22cad68349d0cae0c503ee404d1724f7b" + "source_sha256": "83853050f790b18af95d424fec837abeb1a9a33f0538b5e4b97c16fb9c681781" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 35f1571..547ad8b 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": "88633ef676b4dfe258a6fa9b788ddeb22cad68349d0cae0c503ee404d1724f7b" + "source_sha256": "83853050f790b18af95d424fec837abeb1a9a33f0538b5e4b97c16fb9c681781" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json index b32aa50..f4a4d28 100644 --- a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json +++ b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json @@ -62,7 +62,7 @@ ], "contract": { "path": "sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json", - "sha256": "45d7a92b38b836a4f829f02df85e339cfeea860e1080e4663a8340af6c125125" + "sha256": "f36ed14eaf1f97a5dffa94bda8151b1e0fa182afc0d121b757b70bebc6a43811" }, - "result": "All four patches apply in order to the LF source tree, and the eleven-file SparkCache contract verifies the resulting source bytes, including the live-tensor B12X KDA implementation." + "result": "All four SparkCache patches apply in order to the LF source tree and produce the exact preimages required by the recurrent-boundary producer. The eleven-file final-runtime contract verifies only after that producer creates its four recurrent postimages; it also covers the unchanged live-tensor B12X KDA surface." } diff --git a/sparkcache/runtime_patches/README.md b/sparkcache/runtime_patches/README.md index f13d033..9b2596b 100644 --- a/sparkcache/runtime_patches/README.md +++ b/sparkcache/runtime_patches/README.md @@ -63,8 +63,12 @@ exact-input overlays under `patches/vllm-glm53-b12x-kda-adaptive-mtp`. The three KDA commits after `e10536a` change only the KDA implementation and its model tests. Ten SparkCache ownership files remain byte-identical, while the eleventh contract file binds the live-tensor B12X KDA implementation. -The contract has **implemented** status. Four-rank TP4/DCP1 serving remains -unqualified until a receipt names an immutable image built from this revision. +The four SparkCache patches produce the exact preimages consumed by the +recurrent-boundary producer. The contract names one coherent final runtime +state across all eleven files and is valid only after that producer creates its +four recurrent postimages. It has **implemented** status. Four-rank TP4/DCP1 +serving remains unqualified until a receipt names an immutable image built from +this revision. ```bash python -m sparkcache.runtime_patches.verify_lease_contract \ diff --git a/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py b/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py index a7547e7..85a7d3e 100644 --- a/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py +++ b/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py @@ -79,10 +79,7 @@ def test_glm53_b12x_kda_adaptive_mtp_contract_attests_the_complete_sparkcache_vl KDA_PATH, } for record in contract["files"]: - expected_roles = {SOURCE_ROLE} - if record["path"] in RECURRENT_BOUNDARY_FILES: - expected_roles.add(RECURRENT_BOUNDARY_ROLE) - assert set(record["accepted_sha256"]) == expected_roles + assert set(record["accepted_sha256"]) == {RECURRENT_BOUNDARY_ROLE} assert all(record["required_symbols"] for record in contract["files"]) by_path = {record["path"]: record for record in contract["files"]} @@ -147,11 +144,11 @@ def test_glm53_b12x_kda_adaptive_mtp_overlay_has_exact_preimage_and_postimage_re assert patch_positions == sorted(patch_positions) -def test_glm53_b12x_kda_adaptive_mtp_patch_sequence_terminates_at_attested_contract_postimages() -> None: +def test_glm53_b12x_kda_adaptive_mtp_patch_sequence_precedes_the_final_contract() -> None: receipts = json.loads(PREIMAGES.read_text(encoding="utf-8")) contract = json.loads(CONTRACT.read_text(encoding="utf-8")) accepted = { - record["path"]: record["accepted_sha256"][SOURCE_ROLE] + record["path"]: record["accepted_sha256"][RECURRENT_BOUNDARY_ROLE] for record in contract["files"] } scheduler_recovery = receipts["030-sparkcache-hma-load-failure.patch"] @@ -163,13 +160,53 @@ def test_glm53_b12x_kda_adaptive_mtp_patch_sequence_terminates_at_attested_contr scheduler_recovery["accepted_postimage_sha256"][SOURCE_ROLE] == scheduler_attach["accepted_preimage_sha256"][SOURCE_ROLE] ) - assert ( - scheduler_attach["accepted_postimage_sha256"][SOURCE_ROLE] - == accepted[scheduler_attach["target_path"]] - ) - assert ( - manager_lease["accepted_postimage_sha256"][SOURCE_ROLE] - == accepted[manager_lease["target_path"]] + assert scheduler_attach["accepted_postimage_sha256"][SOURCE_ROLE] != accepted[ + scheduler_attach["target_path"] + ] + assert manager_lease["accepted_postimage_sha256"][SOURCE_ROLE] != accepted[ + manager_lease["target_path"] + ] + + +def test_glm53_recurrent_contract_verifies_one_coherent_final_source_state( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + contract = json.loads(CONTRACT.read_text(encoding="utf-8")) + accepted_by_path: dict[Path, str] = {} + for record in contract["files"]: + relative = Path(record["path"]) + accepted_by_path[relative] = record["accepted_sha256"][ + RECURRENT_BOUNDARY_ROLE + ] + classes: dict[str, list[str]] = {} + for symbol in record["required_symbols"]: + class_name, member_name = symbol.split(".") + classes.setdefault(class_name, []).append(member_name) + source = tmp_path / relative + source.parent.mkdir(parents=True, exist_ok=True) + source.write_text( + "\n\n".join( + "class " + + class_name + + ":\n" + + "\n".join( + f" def {member_name}(self):\n pass" + for member_name in members + ) + for class_name, members in classes.items() + ) + + "\n", + encoding="utf-8", + ) + + def exact_composed_digest(path: Path) -> str: + return accepted_by_path[path.resolve().relative_to(tmp_path.resolve())] + + monkeypatch.setattr(verifier, "_sha256", exact_composed_digest) + verified = verifier.verify_contract(tmp_path, CONTRACT) + assert [path.relative_to(tmp_path) for path in verified] == list( + accepted_by_path ) diff --git a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json index 9ac1dd6..7de6ead 100644 --- a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json +++ b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json @@ -5,7 +5,7 @@ { "path": "vllm/distributed/kv_transfer/kv_connector/v1/base.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "bc1965431087676876f58360cd9cc07ab6c06febe6d747695f10b051fd85c412" + "recurrent_boundary_contract": "bc1965431087676876f58360cd9cc07ab6c06febe6d747695f10b051fd85c412" }, "required_symbols": [ "KVConnectorBase_V1.handle_preemptions", @@ -16,7 +16,7 @@ { "path": "vllm/distributed/kv_transfer/kv_connector/utils.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "115512dca36b0711223f55f5ff304abffccc6d078923ba4c6bbb3cdb7fdd39a2" + "recurrent_boundary_contract": "115512dca36b0711223f55f5ff304abffccc6d078923ba4c6bbb3cdb7fdd39a2" }, "required_symbols": [ "KVOutputAggregator.from_connector", @@ -26,7 +26,6 @@ { "path": "vllm/v1/core/sched/scheduler.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "6d397c97f31e67a75efc01b5ddd89fa58db425de14fa43965ef2d6146b6b9bdb", "recurrent_boundary_contract": "260f36ce8fabf70c193b20009ea465eea7b1b6c8e9fb72f2307a01ba8fcf7b2a" }, "required_symbols": [ @@ -42,7 +41,6 @@ { "path": "vllm/v1/core/kv_cache_manager.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "ee03dc9ce2b720c0be6e9f572d23580ba96eff68fe3406250557e83071654af0", "recurrent_boundary_contract": "c5b83d382c96b2bf8c466a993ed77123a14a971e2661797128533319388d0b5f" }, "required_symbols": [ @@ -60,7 +58,6 @@ { "path": "vllm/v1/core/sched/output.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "65235eba652e5a3ccee18bf3cbfeac9bf4da8fb9c61e961580f612cfb7e593bc", "recurrent_boundary_contract": "9911b3f9d21815a185285852b5a6176e5484e1ab0ff5c30f7caaa68ea0fab543" }, "required_symbols": [ @@ -72,7 +69,7 @@ { "path": "vllm/v1/worker/gpu_model_runner.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "5ac63bf43acc6b254282426a8a9b989cc07fcdbd07223101bdc99f91f4e4ce5f" + "recurrent_boundary_contract": "5ac63bf43acc6b254282426a8a9b989cc07fcdbd07223101bdc99f91f4e4ce5f" }, "required_symbols": [ "GPUModelRunner.execute_model", @@ -82,7 +79,7 @@ { "path": "vllm/v1/core/block_pool.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "ddee56dccb2208411b3a035918e917ce8f56a9858471e9ca12b420d5d79bc69c" + "recurrent_boundary_contract": "ddee56dccb2208411b3a035918e917ce8f56a9858471e9ca12b420d5d79bc69c" }, "required_symbols": [ "BlockPool.touch", @@ -92,7 +89,7 @@ { "path": "vllm/v1/core/kv_cache_coordinator.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "37be4bb55d40d78e2210b28c0a3f55e1027872af21335de5ca3c61f09c3856af" + "recurrent_boundary_contract": "37be4bb55d40d78e2210b28c0a3f55e1027872af21335de5ca3c61f09c3856af" }, "required_symbols": [ "KVCacheCoordinator.allocate_new_computed_blocks", @@ -103,7 +100,6 @@ { "path": "vllm/v1/core/single_type_kv_cache_manager.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "e4b1c5c38b63b708fd55aa40a9ab0d008b266d006a63dcfcef55890ac1371cb8", "recurrent_boundary_contract": "f67a1850a7e0288baaa6d42e7ec55b22b09c156720767e23acaabedcae333c8a" }, "required_symbols": [ @@ -120,7 +116,7 @@ { "path": "vllm/v1/kv_cache_interface.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "72acc5ae3f61f7ecbebdb3a3a16b7c69c064d6fd7af23a1fcc3cf946bdaee952" + "recurrent_boundary_contract": "72acc5ae3f61f7ecbebdb3a3a16b7c69c064d6fd7af23a1fcc3cf946bdaee952" }, "required_symbols": [ "MambaSpec.max_num_blocks_per_req" @@ -129,7 +125,7 @@ { "path": "vllm/model_executor/layers/mamba/gdn/kimi_gdn_linear_attn.py", "accepted_sha256": { - "source_built_glm53_b12x_kda_adaptive_mtp": "8bf8bc579dd4a80224dc1633e7513f2a0c58e07db72a736c7e41d28d3c35f3b9" + "recurrent_boundary_contract": "8bf8bc579dd4a80224dc1633e7513f2a0c58e07db72a736c7e41d28d3c35f3b9" }, "required_symbols": [ "KimiGatedDeltaNetAttention._initialize_b12x_kda_decode", From 37bbc6067021278981b570fe67f1ed7ffbb76e54 Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:25:28 -0500 Subject: [PATCH 08/15] Validate recurrent proof at publication time vLLM exposes a hash-proven aligned recurrent block only after the prefill step that produces it, while nonaligned publication boundaries have no separate hand-off. Retain recurrent new-request store state through the following cached step, validate only when that step can publish, and require mappings only for recurrent groups exactly aligned at the store boundary. Nonaligned groups use the authoritative partial page in the accumulated request table; unexpected overrides and missing aligned proofs still fail closed. Cache namespace impact: none. CacheIdentity values, digest salts, 256-token geometry, manifest schemas, page-delta bytes, and the page-tail-cow-v1 namespace are unchanged. Validation: python -m pytest sparkcache -q (763 passed, 7 skipped); python -m pytest deploy -q (108 passed, 1 skipped); python -m ruff check .; git diff --check. --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- sparkcache/README.md | 18 +- sparkcache/spark_context_cache_connector.py | 96 ++++----- sparkcache/test_defect_regressions.py | 208 +++++++++++++++++++- 5 files changed, 262 insertions(+), 64 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index c586573..8bbc09b 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": "83853050f790b18af95d424fec837abeb1a9a33f0538b5e4b97c16fb9c681781" + "source_sha256": "05f74f69e514ca5984bb6108e3bb0831efadc216b73899b1fb3cfcc29b3492ab" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 547ad8b..df45886 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": "83853050f790b18af95d424fec837abeb1a9a33f0538b5e4b97c16fb9c681781" + "source_sha256": "05f74f69e514ca5984bb6108e3bb0831efadc216b73899b1fb3cfcc29b3492ab" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/sparkcache/README.md b/sparkcache/README.md index 3998788..86dc078 100644 --- a/sparkcache/README.md +++ b/sparkcache/README.md @@ -225,13 +225,17 @@ when placement completes and intentionally excludes that bookkeeping. `sparkcache-hybrid-page-delta/v1` codec reuses only byte-identical page 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. For an aligned recurrent group, - vLLM may retain the replay-boundary page outside the advancing request block - table. Its `SchedulerOutput.recurrent_boundary_blocks` hand-off names the - pinned physical block by request, group, and token boundary. SparkCache uses - that block only after all three identities and the recurrent topology match; - missing or contradictory metadata skips publication rather than scanning - later running or speculative state. The + while retaining earlier byte-identical pages. At an exact recurrent-page + boundary, vLLM may retain the replay-boundary page outside the advancing + request block table. Its `SchedulerOutput.recurrent_boundary_blocks` hand-off + names the pinned physical block by request, group, and token boundary. + SparkCache defers a new recurrent request until a later cached scheduler step, + when the preceding forward's hand-off can be observed. It then requires one + matching entry for every recurrent group whose block size exactly divides the + publication boundary; missing or contradictory proof skips publication + rather than scanning later running or speculative state. At a boundary inside + a recurrent page, the request table's partial page remains authoritative and + an unexpected override is rejected. The `sparkcache-page-delta-manifest/v2` schema embeds its authenticated base graph and groups delta bytes into immutable objects of at most 64 MiB. A 1,575,821,491-byte delta therefore uses at most 24 physical delta objects diff --git a/sparkcache/spark_context_cache_connector.py b/sparkcache/spark_context_cache_connector.py index 22b98fd..5c2564f 100644 --- a/sparkcache/spark_context_cache_connector.py +++ b/sparkcache/spark_context_cache_connector.py @@ -1616,12 +1616,13 @@ def _validated_recurrent_boundary_blocks( ) -> tuple[tuple[int, int], ...] | None: """Validate vLLM's exact recurrent replay-boundary block hand-off. - An empty tuple is valid only when the registered topology has no - aligned recurrent group. None means the metadata is absent, - incomplete, or contradictory, so publication must be skipped. - SparkCache never derives a replacement from another non-null table - entry because later entries can hold running or speculative state - beyond ``boundary_tokens``. + An empty tuple is valid when the registered topology has no recurrent + group exactly aligned at ``boundary_tokens``. A nonaligned recurrent + group's partial page remains authoritative in the request block table. + None means required metadata is absent, incomplete, or contradictory, + so publication must be skipped. SparkCache never derives an aligned + replacement from another non-null table entry because later entries + can hold running or speculative state beyond ``boundary_tokens``. """ def reject(reason: str) -> None: @@ -1635,20 +1636,31 @@ def reject(reason: str) -> None: ) return None - required_groups = { + recurrent_groups = { group_index for group_index, topology in enumerate(self._group_topology) if topology["reuse_policy"] == "recurrent_align" } - if not required_groups: + if not recurrent_groups: return () + required_groups = { + group_index + for group_index in recurrent_groups + if boundary_tokens + % int(self._group_topology[group_index]["block_size"]) + == 0 + } raw = getattr(scheduler_output, "recurrent_boundary_blocks", None) if raw is None: + if not required_groups: + return () return reject("vLLM supplied no recurrent boundary mapping") if not isinstance(raw, Mapping): return reject("top-level value is not a mapping") entries = raw.get(request_id) if entries is None: + if not required_groups: + return () return reject("request has no recurrent boundary entries") if not isinstance(entries, (list, tuple)): return reject("request value is not a sequence") @@ -1668,6 +1680,8 @@ def reject(reason: str) -> None: topology = self._group_topology[group_index] if topology["reuse_policy"] != "recurrent_align": return reject("group is not an aligned recurrent cache") + if group_index not in required_groups: + return reject("recurrent group is not aligned at the store boundary") if block_id <= 0: return reject("physical block is vLLM's null block") if entry_boundary != boundary_tokens: @@ -1741,15 +1755,6 @@ def build_connector_meta( if self._has_full_quorum(digest): self.counters["store_skipped_quorum"] += 1 continue - recurrent_boundary_blocks = ( - self._validated_recurrent_boundary_blocks( - scheduler_output, - req_id, - span, - ) - ) - if recurrent_boundary_blocks is None: - continue already = new_req.num_computed_tokens + scheduled if self._streaming_snapshots_enabled: self._append_streaming_snapshot_offer( @@ -1771,10 +1776,25 @@ def build_connector_meta( [list(group) for group in group_blocks], ) self._store_token_ids[req_id] = exact_token_ids - if recurrent_boundary_blocks: - self._store_recurrent_boundaries[req_id] = ( - recurrent_boundary_blocks - ) + elif any( + topology["reuse_policy"] == "recurrent_align" + for topology in self._group_topology + ): + # vLLM can only expose the hash-proven replay-boundary + # block after the scheduled prefill has run. Preserve the + # complete new-request table even when this step promises + # the whole span; the following cached/decode step either + # supplies the aligned proof or publishes the authoritative + # nonaligned partial page from this table. + self._store_progress[req_id] = ( + digest, + span, + already, + [list(group) for group in group_blocks], + ) + self._store_token_ids[req_id] = exact_token_ids + if base_digest: + self._store_bases[req_id] = (base_digest, base_span) elif already >= span: meta.plans.append( _ReqPlan( @@ -1787,9 +1807,6 @@ def build_connector_meta( token_ids=exact_token_ids, base_context_digest=base_digest, base_span_tokens=base_span, - recurrent_boundary_blocks=( - recurrent_boundary_blocks - ), ) ) else: @@ -1803,10 +1820,6 @@ def build_connector_meta( [list(group) for group in group_blocks], ) self._store_token_ids[req_id] = exact_token_ids - if recurrent_boundary_blocks: - self._store_recurrent_boundaries[req_id] = ( - recurrent_boundary_blocks - ) if base_digest: self._store_bases[req_id] = (base_digest, base_span) cached = scheduler_output.scheduled_cached_reqs @@ -1816,19 +1829,6 @@ def build_connector_meta( digest, span, done, blocks_by_group = self._store_progress[req_id] exact_token_ids = self._store_token_ids.get(req_id, ()) base_digest, base_span = self._store_bases.get(req_id, ("", 0)) - recurrent_boundary_blocks = self._validated_recurrent_boundary_blocks( - scheduler_output, - req_id, - span, - ) - if recurrent_boundary_blocks is None: - del self._store_progress[req_id] - self._store_token_ids.pop(req_id, None) - self._store_bases.pop(req_id, None) - self._store_recurrent_boundaries.pop(req_id, None) - continue - if recurrent_boundary_blocks: - self._store_recurrent_boundaries[req_id] = recurrent_boundary_blocks new_block_ids = cached.new_block_ids[index] appended = ( [ @@ -1882,13 +1882,19 @@ def build_connector_meta( block_ids=blocks, ) elif done >= span: + recurrent_boundary_blocks = ( + self._validated_recurrent_boundary_blocks( + scheduler_output, + req_id, + span, + ) + ) del self._store_progress[req_id] self._store_token_ids.pop(req_id, None) self._store_bases.pop(req_id, None) - recurrent_boundary_blocks = self._store_recurrent_boundaries.pop( - req_id, - (), - ) + self._store_recurrent_boundaries.pop(req_id, None) + if recurrent_boundary_blocks is None: + continue if self._has_full_quorum(digest): self.counters["store_skipped_quorum"] += 1 continue diff --git a/sparkcache/test_defect_regressions.py b/sparkcache/test_defect_regressions.py index 70e4bd2..a621576 100644 --- a/sparkcache/test_defect_regressions.py +++ b/sparkcache/test_defect_regressions.py @@ -1025,6 +1025,8 @@ class DefectD17RecurrentBoundaryMetadataTests(unittest.TestCase): BOUNDARY = 6912 PROMPT_TOKENS = 6992 + NONALIGNED_BOUNDARY = 8192 + NONALIGNED_PROMPT_TOKENS = 8256 BOUNDARY_BLOCK = 42 @staticmethod @@ -1093,6 +1095,31 @@ def _scheduler_output( output.recurrent_boundary_blocks = recurrent_boundary_blocks return output + @classmethod + def _cached_scheduler_output( + cls, + *, + num_computed_tokens: int, + recurrent_boundary_blocks: object = None, + group_count: int = 2, + num_scheduled_tokens: int = 1, + ) -> types.SimpleNamespace: + request_id = "dflash-recurrent-boundary" + output = types.SimpleNamespace( + scheduled_new_reqs=[], + scheduled_cached_reqs=types.SimpleNamespace( + req_ids=[request_id], + resumed_req_ids=set(), + num_computed_tokens=[num_computed_tokens], + new_block_ids=[tuple(() for _ in range(group_count))], + ), + num_scheduled_tokens={request_id: num_scheduled_tokens}, + preempted_req_ids=set(), + ) + if recurrent_boundary_blocks is not None: + output.recurrent_boundary_blocks = recurrent_boundary_blocks + return output + def test_explicit_boundary_block_round_trips_through_manifest_store(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -1108,12 +1135,18 @@ def test_explicit_boundary_block_round_trips_through_manifest_store(self) -> Non kv_cache_config=config, extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, ) - output = self._scheduler_output( - { + first_metadata = scheduler.build_connector_meta( + self._scheduler_output() + ) + self.assertEqual(first_metadata.plans, []) + self.assertIn("dflash-recurrent-boundary", scheduler._store_progress) + output = self._cached_scheduler_output( + num_computed_tokens=self.PROMPT_TOKENS, + recurrent_boundary_blocks={ "dflash-recurrent-boundary": [ (1, self.BOUNDARY_BLOCK, self.BOUNDARY) ] - } + }, ) self.assertTrue(scheduler.supports_recurrent_boundary_blocks) @@ -1173,6 +1206,139 @@ def test_explicit_boundary_block_round_trips_through_manifest_store(self) -> Non torch.equal(pools["recurrent"][[93]], expected_recurrent) ) + def test_nonaligned_boundary_uses_request_table_without_mapping(self) -> None: + with tempfile.TemporaryDirectory() as directory: + connector = _make_connector( + Path(directory), + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=self._config(), + extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, + ) + output = self._scheduler_output() + request = output.scheduled_new_reqs[0] + request.prompt_token_ids = list(range(self.NONALIGNED_PROMPT_TOKENS)) + output.num_scheduled_tokens[request.req_id] = ( + self.NONALIGNED_PROMPT_TOKENS + ) + + first_metadata = connector.build_connector_meta(output) + self.assertEqual(first_metadata.plans, []) + self.assertIn(request.req_id, connector._store_progress) + metadata = connector.build_connector_meta( + self._cached_scheduler_output( + num_computed_tokens=self.NONALIGNED_PROMPT_TOKENS, + ) + ) + + self.assertEqual(len(metadata.plans), 1) + plan = metadata.plans[0] + self.assertEqual(plan.span_tokens, self.NONALIGNED_BOUNDARY) + self.assertEqual(plan.recurrent_boundary_blocks, ()) + self.assertEqual( + connector._select_group_blocks_for_span( + plan.block_ids_by_group, + plan.span_tokens, + recurrent_boundary_blocks=plan.recurrent_boundary_blocks, + ), + ((11, 12, 13, 14), (71,)), + ) + self.assertEqual( + connector.counters["recurrent_boundary_metadata_rejected"], + 0, + ) + + def test_nonaligned_boundary_rejects_unexpected_mapping(self) -> None: + output = self._scheduler_output() + request = output.scheduled_new_reqs[0] + request.prompt_token_ids = list(range(self.NONALIGNED_PROMPT_TOKENS)) + output.num_scheduled_tokens[request.req_id] = self.NONALIGNED_PROMPT_TOKENS + with tempfile.TemporaryDirectory() as directory: + connector = _make_connector( + Path(directory), + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=self._config(), + extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, + ) + + first_metadata = connector.build_connector_meta(output) + self.assertEqual(first_metadata.plans, []) + metadata = connector.build_connector_meta( + self._cached_scheduler_output( + num_computed_tokens=self.NONALIGNED_PROMPT_TOKENS, + recurrent_boundary_blocks={ + "dflash-recurrent-boundary": [ + ( + 1, + self.BOUNDARY_BLOCK, + self.NONALIGNED_BOUNDARY, + ) + ] + }, + ) + ) + + self.assertEqual(metadata.plans, []) + self.assertEqual( + connector.counters["recurrent_boundary_metadata_rejected"], + 1, + ) + + def test_chunked_prefill_validates_only_at_publication_step(self) -> None: + with tempfile.TemporaryDirectory() as directory: + connector = _make_connector( + Path(directory), + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=self._config(), + extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, + ) + first = self._scheduler_output() + first.num_scheduled_tokens["dflash-recurrent-boundary"] = 2304 + + self.assertEqual(connector.build_connector_meta(first).plans, []) + middle = self._cached_scheduler_output( + num_computed_tokens=2304, + num_scheduled_tokens=2304, + ) + self.assertEqual(connector.build_connector_meta(middle).plans, []) + self.assertIn("dflash-recurrent-boundary", connector._store_progress) + self.assertEqual( + connector.counters["recurrent_boundary_metadata_rejected"], + 0, + ) + final = self._cached_scheduler_output( + num_computed_tokens=4608, + num_scheduled_tokens=2304, + recurrent_boundary_blocks={ + "dflash-recurrent-boundary": [ + (1, self.BOUNDARY_BLOCK, self.BOUNDARY) + ] + }, + ) + + metadata = connector.build_connector_meta(final) + + self.assertEqual(len(metadata.plans), 1) + self.assertEqual( + metadata.plans[0].recurrent_boundary_blocks, + ((1, self.BOUNDARY_BLOCK),), + ) + self.assertNotIn("dflash-recurrent-boundary", connector._store_progress) + def test_missing_or_wrong_request_metadata_skips_publication(self) -> None: for boundary_metadata in ( None, @@ -1198,7 +1364,14 @@ def test_missing_or_wrong_request_metadata_skips_publication(self) -> None: recurrent = list(recurrent) recurrent[2] = 69 # stale or recycled, not boundary-proven output.scheduled_new_reqs[0].block_ids = (full, tuple(recurrent)) - metadata = connector.build_connector_meta(output) + first_metadata = connector.build_connector_meta(output) + self.assertEqual(first_metadata.plans, []) + metadata = connector.build_connector_meta( + self._cached_scheduler_output( + num_computed_tokens=self.PROMPT_TOKENS, + recurrent_boundary_blocks=boundary_metadata, + ) + ) self.assertEqual(metadata.plans, []) self.assertEqual( connector.counters[ @@ -1232,9 +1405,16 @@ def test_contradictory_boundary_metadata_skips_publication(self) -> None: "spark_cache_model_profile": "glm53-flash-hybrid" }, ) + first_metadata = connector.build_connector_meta( + self._scheduler_output() + ) + self.assertEqual(first_metadata.plans, []) metadata = connector.build_connector_meta( - self._scheduler_output( - {"dflash-recurrent-boundary": entries} + self._cached_scheduler_output( + num_computed_tokens=self.PROMPT_TOKENS, + recurrent_boundary_blocks={ + "dflash-recurrent-boundary": entries + }, ) ) self.assertEqual(metadata.plans, []) @@ -1253,9 +1433,7 @@ def test_partial_recurrent_group_coverage_skips_publication(self) -> None: layer_names=("recurrent-2",), ) config.kv_cache_groups = (*config.kv_cache_groups, second_recurrent) - output = self._scheduler_output( - {"dflash-recurrent-boundary": [(1, 42, self.BOUNDARY)]} - ) + output = self._scheduler_output() output.scheduled_new_reqs[0].block_ids = ( *self._tables(), (0, 0, 0, 81, 82, 83, 84, 85, 86, 87, 88), @@ -1273,7 +1451,17 @@ def test_partial_recurrent_group_coverage_skips_publication(self) -> None: extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, ) - metadata = connector.build_connector_meta(output) + first_metadata = connector.build_connector_meta(output) + self.assertEqual(first_metadata.plans, []) + metadata = connector.build_connector_meta( + self._cached_scheduler_output( + num_computed_tokens=self.PROMPT_TOKENS, + recurrent_boundary_blocks={ + "dflash-recurrent-boundary": [(1, 42, self.BOUNDARY)] + }, + group_count=3, + ) + ) self.assertEqual(metadata.plans, []) self.assertEqual( From 99e101ddbc62d24f853c9210cf1b1a164c4c265a Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:34:18 -0500 Subject: [PATCH 09/15] Latch every proven recurrent CoW target A recurrent partial page can be replaced after the initial request table is observed. Its durable publication source is the pinned block delivered by vLLM partial_tail_offloads, not the accumulated source ID. Treat absent per-request metadata as pending, latch complete validated mappings from any scheduler output, reject incomplete or conflicting evidence, and publish only after every recurrent group has a proven block. Preemption clears the latch; completion and quorum retire pending state. Cache namespace impact: none. CacheIdentity values, digest salts, 256-token geometry, manifest schemas, page-delta bytes, and page-tail-cow-v1 are unchanged. Validation: python -m pytest sparkcache -q (764 passed, 7 skipped after isolated timing rerun); python -m pytest deploy -q (108 passed, 1 skipped); python -m ruff check .; git diff --check. --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- sparkcache/README.md | 13 +- sparkcache/spark_context_cache_connector.py | 163 +++++++++++--------- sparkcache/test_defect_regressions.py | 159 +++++++++++++++---- 5 files changed, 234 insertions(+), 105 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index 8bbc09b..ce63c32 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": "05f74f69e514ca5984bb6108e3bb0831efadc216b73899b1fb3cfcc29b3492ab" + "source_sha256": "155a06101524d4c2d2f55dbbd01576e35d5c729888e216fd2f3963e275949ba0" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index df45886..26876f5 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": "05f74f69e514ca5984bb6108e3bb0831efadc216b73899b1fb3cfcc29b3492ab" + "source_sha256": "155a06101524d4c2d2f55dbbd01576e35d5c729888e216fd2f3963e275949ba0" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/sparkcache/README.md b/sparkcache/README.md index 86dc078..3b47425 100644 --- a/sparkcache/README.md +++ b/sparkcache/README.md @@ -230,12 +230,13 @@ when placement completes and intentionally excludes that bookkeeping. request block table. Its `SchedulerOutput.recurrent_boundary_blocks` hand-off names the pinned physical block by request, group, and token boundary. SparkCache defers a new recurrent request until a later cached scheduler step, - when the preceding forward's hand-off can be observed. It then requires one - matching entry for every recurrent group whose block size exactly divides the - publication boundary; missing or contradictory proof skips publication - rather than scanning later running or speculative state. At a boundary inside - a recurrent page, the request table's partial page remains authoritative and - an unexpected override is rejected. The + when the preceding forward's hand-off can be observed. It latches one matching + entry for every recurrent group, including a partial-tail CoW target when the + boundary lies inside a recurrent page. Outputs with no entry leave publication + pending; incomplete, contradictory, or changed evidence cancels it. A store is + emitted only after every recurrent group has a proven pinned block. SparkCache + never substitutes an accumulated request-table ID because vLLM may have + replaced that source block while producing the durable CoW target. The `sparkcache-page-delta-manifest/v2` schema embeds its authenticated base graph and groups delta bytes into immutable objects of at most 64 MiB. A 1,575,821,491-byte delta therefore uses at most 24 physical delta objects diff --git a/sparkcache/spark_context_cache_connector.py b/sparkcache/spark_context_cache_connector.py index 5c2564f..8cf5708 100644 --- a/sparkcache/spark_context_cache_connector.py +++ b/sparkcache/spark_context_cache_connector.py @@ -570,6 +570,11 @@ def __init__( self._storage_mode = config.storage_mode self._publication_schema = config.publication_schema self._group_topology = config.group_topology + self._recurrent_group_indexes = frozenset( + group_index + for group_index, topology in enumerate(self._group_topology) + if topology["reuse_policy"] == "recurrent_align" + ) self._chunk_tokens = config.chunk_tokens self._root = config.root self._store = ManifestStore(self._root) @@ -1613,16 +1618,19 @@ def _validated_recurrent_boundary_blocks( scheduler_output: "SchedulerOutput", request_id: str, boundary_tokens: int, + *, + latched: tuple[tuple[int, int], ...] = (), ) -> tuple[tuple[int, int], ...] | None: """Validate vLLM's exact recurrent replay-boundary block hand-off. - An empty tuple is valid when the registered topology has no recurrent - group exactly aligned at ``boundary_tokens``. A nonaligned recurrent - group's partial page remains authoritative in the request block table. - None means required metadata is absent, incomplete, or contradictory, - so publication must be skipped. SparkCache never derives an aligned - replacement from another non-null table entry because later entries - can hold running or speculative state beyond ``boundary_tokens``. + vLLM may expose a full-page boundary or a partial-tail CoW target on a + later scheduler output than the request which began the store. Absent + per-request metadata therefore preserves ``latched`` and keeps the + store pending. None means supplied metadata is incomplete, + contradictory, or conflicts with an earlier latch, so this publication + attempt must be poisoned. SparkCache never derives a replacement from + another request-table entry because it can name overwritten running or + speculative state instead of vLLM's pinned CoW target. """ def reject(reason: str) -> None: @@ -1636,32 +1644,17 @@ def reject(reason: str) -> None: ) return None - recurrent_groups = { - group_index - for group_index, topology in enumerate(self._group_topology) - if topology["reuse_policy"] == "recurrent_align" - } - if not recurrent_groups: + required_groups = self._recurrent_group_indexes + if not required_groups: return () - required_groups = { - group_index - for group_index in recurrent_groups - if boundary_tokens - % int(self._group_topology[group_index]["block_size"]) - == 0 - } raw = getattr(scheduler_output, "recurrent_boundary_blocks", None) if raw is None: - if not required_groups: - return () - return reject("vLLM supplied no recurrent boundary mapping") + return latched if not isinstance(raw, Mapping): return reject("top-level value is not a mapping") entries = raw.get(request_id) if entries is None: - if not required_groups: - return () - return reject("request has no recurrent boundary entries") + return latched if not isinstance(entries, (list, tuple)): return reject("request value is not a sequence") @@ -1680,8 +1673,6 @@ def reject(reason: str) -> None: topology = self._group_topology[group_index] if topology["reuse_policy"] != "recurrent_align": return reject("group is not an aligned recurrent cache") - if group_index not in required_groups: - return reject("recurrent group is not aligned at the store boundary") if block_id <= 0: return reject("physical block is vLLM's null block") if entry_boundary != boundary_tokens: @@ -1690,7 +1681,10 @@ def reject(reason: str) -> None: overrides.append((group_index, block_id)) if seen_groups != required_groups: return reject("entries do not cover every aligned recurrent group") - return tuple(sorted(overrides)) + validated = tuple(sorted(overrides)) + if latched and validated != latched: + return reject("entries conflict with the latched recurrent boundary") + return validated def build_connector_meta( self, scheduler_output: "SchedulerOutput" @@ -1776,16 +1770,21 @@ def build_connector_meta( [list(group) for group in group_blocks], ) self._store_token_ids[req_id] = exact_token_ids - elif any( - topology["reuse_policy"] == "recurrent_align" - for topology in self._group_topology - ): - # vLLM can only expose the hash-proven replay-boundary - # block after the scheduled prefill has run. Preserve the - # complete new-request table even when this step promises - # the whole span; the following cached/decode step either - # supplies the aligned proof or publishes the authoritative - # nonaligned partial page from this table. + elif self._recurrent_group_indexes: + recurrent_boundary_blocks = ( + self._validated_recurrent_boundary_blocks( + scheduler_output, + req_id, + span, + ) + ) + if recurrent_boundary_blocks is None: + continue + # Full-page proof and partial-tail CoW hand-offs can arrive + # after the prefill which began this store. Retain the + # complete request table and any early proof until a later + # cached step has both finished the span and proven every + # recurrent group. self._store_progress[req_id] = ( digest, span, @@ -1793,6 +1792,10 @@ def build_connector_meta( [list(group) for group in group_blocks], ) self._store_token_ids[req_id] = exact_token_ids + if recurrent_boundary_blocks: + self._store_recurrent_boundaries[req_id] = ( + recurrent_boundary_blocks + ) if base_digest: self._store_bases[req_id] = (base_digest, base_span) elif already >= span: @@ -1829,30 +1832,52 @@ def build_connector_meta( digest, span, done, blocks_by_group = self._store_progress[req_id] exact_token_ids = self._store_token_ids.get(req_id, ()) base_digest, base_span = self._store_bases.get(req_id, ("", 0)) - new_block_ids = cached.new_block_ids[index] - appended = ( - [ - list(group) - for group in self._normalize_group_blocks( - new_block_ids, - allow_empty_groups=True, - ) - ] - if new_block_ids is not None - else [[] for _ in blocks_by_group] + if self._has_full_quorum(digest): + del self._store_progress[req_id] + self._store_token_ids.pop(req_id, None) + self._store_bases.pop(req_id, None) + self._store_recurrent_boundaries.pop(req_id, None) + self.counters["store_skipped_quorum"] += 1 + continue + recurrent_boundary_blocks = self._validated_recurrent_boundary_blocks( + scheduler_output, + req_id, + span, + latched=self._store_recurrent_boundaries.get(req_id, ()), ) - if len(appended) != len(blocks_by_group): - raise RuntimeError( - "spark-context-cache: KV-cache group count changed while" - " accumulating a store" + if recurrent_boundary_blocks is None: + del self._store_progress[req_id] + self._store_token_ids.pop(req_id, None) + self._store_bases.pop(req_id, None) + self._store_recurrent_boundaries.pop(req_id, None) + continue + if recurrent_boundary_blocks: + self._store_recurrent_boundaries[req_id] = recurrent_boundary_blocks + if done < span or req_id in cached.resumed_req_ids: + new_block_ids = cached.new_block_ids[index] + appended = ( + [ + list(group) + for group in self._normalize_group_blocks( + new_block_ids, + allow_empty_groups=True, + ) + ] + if new_block_ids is not None + else [[] for _ in blocks_by_group] ) - if req_id in cached.resumed_req_ids: - blocks_by_group = appended - else: - blocks_by_group = [ - existing + added - for existing, added in zip(blocks_by_group, appended) - ] + if len(appended) != len(blocks_by_group): + raise RuntimeError( + "spark-context-cache: KV-cache group count changed while" + " accumulating a store" + ) + if req_id in cached.resumed_req_ids: + blocks_by_group = appended + else: + blocks_by_group = [ + existing + added + for existing, added in zip(blocks_by_group, appended) + ] blocks = blocks_by_group[0] done = cached.num_computed_tokens[index] + ( scheduler_output.num_scheduled_tokens.get(req_id, 0) @@ -1882,22 +1907,18 @@ def build_connector_meta( block_ids=blocks, ) elif done >= span: - recurrent_boundary_blocks = ( - self._validated_recurrent_boundary_blocks( - scheduler_output, - req_id, + if self._recurrent_group_indexes and not recurrent_boundary_blocks: + self._store_progress[req_id] = ( + digest, span, + done, + blocks_by_group, ) - ) + continue del self._store_progress[req_id] self._store_token_ids.pop(req_id, None) self._store_bases.pop(req_id, None) self._store_recurrent_boundaries.pop(req_id, None) - if recurrent_boundary_blocks is None: - continue - if self._has_full_quorum(digest): - self.counters["store_skipped_quorum"] += 1 - continue normalized = tuple(tuple(group) for group in blocks_by_group) meta.plans.append( _ReqPlan( diff --git a/sparkcache/test_defect_regressions.py b/sparkcache/test_defect_regressions.py index a621576..d5a53ae 100644 --- a/sparkcache/test_defect_regressions.py +++ b/sparkcache/test_defect_regressions.py @@ -1028,6 +1028,7 @@ class DefectD17RecurrentBoundaryMetadataTests(unittest.TestCase): NONALIGNED_BOUNDARY = 8192 NONALIGNED_PROMPT_TOKENS = 8256 BOUNDARY_BLOCK = 42 + COW_BLOCK = 142 @staticmethod def _config() -> types.SimpleNamespace: @@ -1103,15 +1104,21 @@ def _cached_scheduler_output( recurrent_boundary_blocks: object = None, group_count: int = 2, num_scheduled_tokens: int = 1, + resumed: bool = False, + new_block_ids: object = None, ) -> types.SimpleNamespace: request_id = "dflash-recurrent-boundary" output = types.SimpleNamespace( scheduled_new_reqs=[], scheduled_cached_reqs=types.SimpleNamespace( req_ids=[request_id], - resumed_req_ids=set(), + resumed_req_ids={request_id} if resumed else set(), num_computed_tokens=[num_computed_tokens], - new_block_ids=[tuple(() for _ in range(group_count))], + new_block_ids=[ + new_block_ids + if new_block_ids is not None + else tuple(() for _ in range(group_count)) + ], ), num_scheduled_tokens={request_id: num_scheduled_tokens}, preempted_req_ids=set(), @@ -1135,18 +1142,24 @@ def test_explicit_boundary_block_round_trips_through_manifest_store(self) -> Non kv_cache_config=config, extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, ) + boundary_metadata = { + "dflash-recurrent-boundary": [ + (1, self.BOUNDARY_BLOCK, self.BOUNDARY) + ] + } first_metadata = scheduler.build_connector_meta( - self._scheduler_output() + self._scheduler_output(boundary_metadata) ) self.assertEqual(first_metadata.plans, []) self.assertIn("dflash-recurrent-boundary", scheduler._store_progress) + self.assertEqual( + scheduler._store_recurrent_boundaries[ + "dflash-recurrent-boundary" + ], + ((1, self.BOUNDARY_BLOCK),), + ) output = self._cached_scheduler_output( num_computed_tokens=self.PROMPT_TOKENS, - recurrent_boundary_blocks={ - "dflash-recurrent-boundary": [ - (1, self.BOUNDARY_BLOCK, self.BOUNDARY) - ] - }, ) self.assertTrue(scheduler.supports_recurrent_boundary_blocks) @@ -1206,7 +1219,7 @@ def test_explicit_boundary_block_round_trips_through_manifest_store(self) -> Non torch.equal(pools["recurrent"][[93]], expected_recurrent) ) - def test_nonaligned_boundary_uses_request_table_without_mapping(self) -> None: + def test_nonaligned_boundary_waits_for_partial_tail_cow_mapping(self) -> None: with tempfile.TemporaryDirectory() as directory: connector = _make_connector( Path(directory), @@ -1229,34 +1242,49 @@ def test_nonaligned_boundary_uses_request_table_without_mapping(self) -> None: first_metadata = connector.build_connector_meta(output) self.assertEqual(first_metadata.plans, []) self.assertIn(request.req_id, connector._store_progress) - metadata = connector.build_connector_meta( + pending = connector.build_connector_meta( self._cached_scheduler_output( num_computed_tokens=self.NONALIGNED_PROMPT_TOKENS, ) ) + self.assertEqual(pending.plans, []) + self.assertIn(request.req_id, connector._store_progress) + metadata = connector.build_connector_meta( + self._cached_scheduler_output( + num_computed_tokens=self.NONALIGNED_PROMPT_TOKENS + 1, + recurrent_boundary_blocks={ + request.req_id: [ + (1, self.COW_BLOCK, self.NONALIGNED_BOUNDARY) + ] + }, + ) + ) self.assertEqual(len(metadata.plans), 1) plan = metadata.plans[0] self.assertEqual(plan.span_tokens, self.NONALIGNED_BOUNDARY) - self.assertEqual(plan.recurrent_boundary_blocks, ()) + self.assertEqual(plan.recurrent_boundary_blocks, ((1, self.COW_BLOCK),)) self.assertEqual( connector._select_group_blocks_for_span( plan.block_ids_by_group, plan.span_tokens, recurrent_boundary_blocks=plan.recurrent_boundary_blocks, ), - ((11, 12, 13, 14), (71,)), + ((11, 12, 13, 14), (self.COW_BLOCK,)), ) self.assertEqual( connector.counters["recurrent_boundary_metadata_rejected"], 0, ) - def test_nonaligned_boundary_rejects_unexpected_mapping(self) -> None: - output = self._scheduler_output() - request = output.scheduled_new_reqs[0] - request.prompt_token_ids = list(range(self.NONALIGNED_PROMPT_TOKENS)) - output.num_scheduled_tokens[request.req_id] = self.NONALIGNED_PROMPT_TOKENS + def test_conflicting_mapping_poisons_latched_publication(self) -> None: + output = self._scheduler_output( + { + "dflash-recurrent-boundary": [ + (1, self.BOUNDARY_BLOCK, self.BOUNDARY) + ] + } + ) with tempfile.TemporaryDirectory() as directory: connector = _make_connector( Path(directory), @@ -1277,11 +1305,7 @@ def test_nonaligned_boundary_rejects_unexpected_mapping(self) -> None: num_computed_tokens=self.NONALIGNED_PROMPT_TOKENS, recurrent_boundary_blocks={ "dflash-recurrent-boundary": [ - ( - 1, - self.BOUNDARY_BLOCK, - self.NONALIGNED_BOUNDARY, - ) + (1, self.BOUNDARY_BLOCK + 1, self.BOUNDARY) ] }, ) @@ -1339,7 +1363,7 @@ def test_chunked_prefill_validates_only_at_publication_step(self) -> None: ) self.assertNotIn("dflash-recurrent-boundary", connector._store_progress) - def test_missing_or_wrong_request_metadata_skips_publication(self) -> None: + def test_missing_or_wrong_request_metadata_keeps_publication_pending(self) -> None: for boundary_metadata in ( None, {"another-request": [(1, self.BOUNDARY_BLOCK, self.BOUNDARY)]}, @@ -1377,7 +1401,23 @@ def test_missing_or_wrong_request_metadata_skips_publication(self) -> None: connector.counters[ "recurrent_boundary_metadata_rejected" ], - 1, + 0, + ) + self.assertIn( + "dflash-recurrent-boundary", connector._store_progress + ) + connector.request_finished( + types.SimpleNamespace( + request_id="dflash-recurrent-boundary" + ), + [], + ) + self.assertNotIn( + "dflash-recurrent-boundary", connector._store_progress + ) + self.assertNotIn( + "dflash-recurrent-boundary", + connector._store_recurrent_boundaries, ) def test_contradictory_boundary_metadata_skips_publication(self) -> None: @@ -1483,9 +1523,16 @@ def test_preemption_discards_request_lifetime_boundary_block(self) -> None: extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, ) request_id = "dflash-recurrent-boundary" - connector._store_recurrent_boundaries[request_id] = ( - (1, self.BOUNDARY_BLOCK), + connector.build_connector_meta( + self._scheduler_output( + { + request_id: [ + (1, self.BOUNDARY_BLOCK, self.BOUNDARY) + ] + } + ) ) + self.assertIn(request_id, connector._store_recurrent_boundaries) output = types.SimpleNamespace( scheduled_new_reqs=[], scheduled_cached_reqs=types.SimpleNamespace( @@ -1502,6 +1549,66 @@ def test_preemption_discards_request_lifetime_boundary_block(self) -> None: self.assertEqual(metadata.preempted_request_ids, (request_id,)) self.assertNotIn(request_id, connector._store_recurrent_boundaries) + self.assertIn(request_id, connector._store_progress) + + resumed = connector.build_connector_meta( + self._cached_scheduler_output( + num_computed_tokens=self.PROMPT_TOKENS, + resumed=True, + new_block_ids=self._tables(), + ) + ) + self.assertEqual(resumed.plans, []) + self.assertNotIn(request_id, connector._store_recurrent_boundaries) + published = connector.build_connector_meta( + self._cached_scheduler_output( + num_computed_tokens=self.PROMPT_TOKENS + 1, + recurrent_boundary_blocks={ + request_id: [ + (1, self.BOUNDARY_BLOCK + 10, self.BOUNDARY) + ] + }, + ) + ) + self.assertEqual(len(published.plans), 1) + self.assertEqual( + published.plans[0].recurrent_boundary_blocks, + ((1, self.BOUNDARY_BLOCK + 10),), + ) + + def test_quorum_retires_pending_store_without_boundary_proof(self) -> None: + with tempfile.TemporaryDirectory() as directory: + connector = _make_connector( + Path(directory), + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=self._config(), + extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, + ) + request_id = "dflash-recurrent-boundary" + connector.build_connector_meta(self._scheduler_output()) + digest = connector._store_progress[request_id][0] + connector._quorum[digest] = {0} + + metadata = connector.build_connector_meta( + self._cached_scheduler_output( + num_computed_tokens=self.PROMPT_TOKENS, + recurrent_boundary_blocks="malformed-but-unneeded", + ) + ) + + self.assertEqual(metadata.plans, []) + self.assertNotIn(request_id, connector._store_progress) + self.assertNotIn(request_id, connector._store_recurrent_boundaries) + self.assertEqual(connector.counters["store_skipped_quorum"], 1) + self.assertEqual( + connector.counters["recurrent_boundary_metadata_rejected"], + 0, + ) class DigestNamespaceTests(unittest.TestCase): From 972b203a716eb20f1889583f7f408788f2a67684 Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:37:22 -0500 Subject: [PATCH 10/15] Accept the boundary-crossing producer postimage The recurrent producer now identifies a replay-boundary block when one cache_blocks call crosses that boundary, rather than requiring the caller's token count to equal it. Advance the exact single_type_kv_cache_manager postimage and lease-contract receipt while retaining the same required symbol surface. Cache namespace impact: none. CacheIdentity values, digest salts, chunk geometry, manifest schemas, page-delta bytes, and page-tail-cow-v1 are unchanged. Validation: strict eleven-file verifier passed against the composed vLLM source; SparkCache 764 passed, 7 skipped; deploy 108 passed, 1 skipped; Ruff and diff checks passed. --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json | 2 +- ...llm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index ce63c32..4535e95 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": "155a06101524d4c2d2f55dbbd01576e35d5c729888e216fd2f3963e275949ba0" + "source_sha256": "0c7547fb7e78b3af202d83690170efec2c7602a7c7ea6b407ef70c3fcdd8cfbb" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 26876f5..9c63215 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": "155a06101524d4c2d2f55dbbd01576e35d5c729888e216fd2f3963e275949ba0" + "source_sha256": "0c7547fb7e78b3af202d83690170efec2c7602a7c7ea6b407ef70c3fcdd8cfbb" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json index f4a4d28..abb2cae 100644 --- a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json +++ b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json @@ -62,7 +62,7 @@ ], "contract": { "path": "sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json", - "sha256": "f36ed14eaf1f97a5dffa94bda8151b1e0fa182afc0d121b757b70bebc6a43811" + "sha256": "70b94520f1094d99ebf0e2a3f5a61e29ca377f61f6bd052bd899f23d034957b8" }, "result": "All four SparkCache patches apply in order to the LF source tree and produce the exact preimages required by the recurrent-boundary producer. The eleven-file final-runtime contract verifies only after that producer creates its four recurrent postimages; it also covers the unchanged live-tensor B12X KDA surface." } diff --git a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json index 7de6ead..39c83ba 100644 --- a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json +++ b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json @@ -100,7 +100,7 @@ { "path": "vllm/v1/core/single_type_kv_cache_manager.py", "accepted_sha256": { - "recurrent_boundary_contract": "f67a1850a7e0288baaa6d42e7ec55b22b09c156720767e23acaabedcae333c8a" + "recurrent_boundary_contract": "2ab95dea008d65488bc2d55ccbe023c4481dba633fe92924001b9e4155ff38a2" }, "required_symbols": [ "SingleTypeKVCacheManager.add_local_computed_blocks", From bf7174e341e032d9b5cc970cca3d6c2985d364fc Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:54:38 -0500 Subject: [PATCH 11/15] Retain the exact-stop recurrent producer contract An overshooting Mamba cache_blocks call has already nulled the earlier arithmetic boundary slots, so the crossing postimage cannot prove or recover that state. Restore the verified exact-stop producer postimage and lease contract. Scheduler-level regression coverage owns the invariant that aligned GLM prefill stops at the 2,304-token boundary; nonaligned 8,192 publication uses the next-step partial-tail CoW hand-off latched by SparkCache. Cache namespace impact: none. CacheIdentity values, digest salts, chunk geometry, manifest schemas, page-delta bytes, and page-tail-cow-v1 are unchanged. --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json | 2 +- ...llm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index 4535e95..ce63c32 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": "0c7547fb7e78b3af202d83690170efec2c7602a7c7ea6b407ef70c3fcdd8cfbb" + "source_sha256": "155a06101524d4c2d2f55dbbd01576e35d5c729888e216fd2f3963e275949ba0" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 9c63215..26876f5 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": "0c7547fb7e78b3af202d83690170efec2c7602a7c7ea6b407ef70c3fcdd8cfbb" + "source_sha256": "155a06101524d4c2d2f55dbbd01576e35d5c729888e216fd2f3963e275949ba0" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json index abb2cae..f4a4d28 100644 --- a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json +++ b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json @@ -62,7 +62,7 @@ ], "contract": { "path": "sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json", - "sha256": "70b94520f1094d99ebf0e2a3f5a61e29ca377f61f6bd052bd899f23d034957b8" + "sha256": "f36ed14eaf1f97a5dffa94bda8151b1e0fa182afc0d121b757b70bebc6a43811" }, "result": "All four SparkCache patches apply in order to the LF source tree and produce the exact preimages required by the recurrent-boundary producer. The eleven-file final-runtime contract verifies only after that producer creates its four recurrent postimages; it also covers the unchanged live-tensor B12X KDA surface." } diff --git a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json index 39c83ba..7de6ead 100644 --- a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json +++ b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json @@ -100,7 +100,7 @@ { "path": "vllm/v1/core/single_type_kv_cache_manager.py", "accepted_sha256": { - "recurrent_boundary_contract": "2ab95dea008d65488bc2d55ccbe023c4481dba633fe92924001b9e4155ff38a2" + "recurrent_boundary_contract": "f67a1850a7e0288baaa6d42e7ec55b22b09c156720767e23acaabedcae333c8a" }, "required_symbols": [ "SingleTypeKVCacheManager.add_local_computed_blocks", From bd3eec1c10b259a24c5f335161d9f8be51c887cd Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:26:14 -0500 Subject: [PATCH 12/15] Ignore proven earlier recurrent checkpoints vLLM can emit a valid aligned checkpoint while a request is still advancing toward a later SparkCache publication boundary. Treat well-formed entries below the store plan as intermediate evidence: do not latch or poison them. Continue waiting until every recurrent group supplies proof at the exact target boundary. Future, malformed, null, non-recurrent, incomplete, and conflicting target entries remain fail-closed. Cache namespace impact: none. CacheIdentity values, digest salts, 256-token geometry, manifest schemas, page-delta bytes, and page-tail-cow-v1 are unchanged. Validation: SparkCache 764 passed, 7 skipped; deploy 108 passed, 1 skipped; Ruff and diff checks passed. --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- sparkcache/README.md | 8 +++-- sparkcache/spark_context_cache_connector.py | 38 +++++++++++++-------- sparkcache/test_defect_regressions.py | 15 +++++++- 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index ce63c32..4fa973c 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": "155a06101524d4c2d2f55dbbd01576e35d5c729888e216fd2f3963e275949ba0" + "source_sha256": "490d2c069c2eb755ecb93727aa47c41df38665427228895af0638b8588a049f3" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 26876f5..5a972b8 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": "155a06101524d4c2d2f55dbbd01576e35d5c729888e216fd2f3963e275949ba0" + "source_sha256": "490d2c069c2eb755ecb93727aa47c41df38665427228895af0638b8588a049f3" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/sparkcache/README.md b/sparkcache/README.md index 3b47425..e763e6f 100644 --- a/sparkcache/README.md +++ b/sparkcache/README.md @@ -232,9 +232,11 @@ when placement completes and intentionally excludes that bookkeeping. SparkCache defers a new recurrent request until a later cached scheduler step, when the preceding forward's hand-off can be observed. It latches one matching entry for every recurrent group, including a partial-tail CoW target when the - boundary lies inside a recurrent page. Outputs with no entry leave publication - pending; incomplete, contradictory, or changed evidence cancels it. A store is - emitted only after every recurrent group has a proven pinned block. SparkCache + boundary lies inside a recurrent page. Valid entries for an earlier checkpoint + are ignored while the request advances; outputs with no target-boundary entry + leave publication pending. Incomplete, future, contradictory, or changed + target evidence cancels it. A store is emitted only after every recurrent + group has a proven pinned block at the exact publication boundary. SparkCache never substitutes an accumulated request-table ID because vLLM may have replaced that source block while producing the durable CoW target. The `sparkcache-page-delta-manifest/v2` schema embeds its authenticated base graph diff --git a/sparkcache/spark_context_cache_connector.py b/sparkcache/spark_context_cache_connector.py index 8cf5708..05c4753 100644 --- a/sparkcache/spark_context_cache_connector.py +++ b/sparkcache/spark_context_cache_connector.py @@ -1623,14 +1623,16 @@ def _validated_recurrent_boundary_blocks( ) -> tuple[tuple[int, int], ...] | None: """Validate vLLM's exact recurrent replay-boundary block hand-off. - vLLM may expose a full-page boundary or a partial-tail CoW target on a - later scheduler output than the request which began the store. Absent - per-request metadata therefore preserves ``latched`` and keeps the - store pending. None means supplied metadata is incomplete, - contradictory, or conflicts with an earlier latch, so this publication - attempt must be poisoned. SparkCache never derives a replacement from - another request-table entry because it can name overwritten running or - speculative state instead of vLLM's pinned CoW target. + vLLM may expose an earlier aligned checkpoint while the request is + still advancing toward this store boundary, followed by a partial-tail + CoW target on a later scheduler output. Valid older entries and absent + per-request metadata therefore preserve ``latched`` and keep the store + pending. None means supplied metadata is incomplete, malformed, ahead + of the plan, or conflicts with an earlier same-boundary latch, so this + publication attempt must be poisoned. SparkCache never derives a + replacement from another request-table entry because it can name + overwritten running or speculative state instead of vLLM's pinned CoW + target. """ def reject(reason: str) -> None: @@ -1657,9 +1659,11 @@ def reject(reason: str) -> None: return latched if not isinstance(entries, (list, tuple)): return reject("request value is not a sequence") + if not entries: + return reject("request has no recurrent boundary entries") overrides: list[tuple[int, int]] = [] - seen_groups: set[int] = set() + seen_target_groups: set[int] = set() for entry in entries: if not isinstance(entry, (list, tuple)) or len(entry) != 3: return reject("entry is not a group, block, boundary triple") @@ -1668,18 +1672,22 @@ def reject(reason: str) -> None: return reject("entry values are not integers") if not 0 <= group_index < len(self._group_topology): return reject("group index is outside the registered topology") - if group_index in seen_groups: - return reject("multiple blocks claim the same recurrent group") topology = self._group_topology[group_index] if topology["reuse_policy"] != "recurrent_align": return reject("group is not an aligned recurrent cache") if block_id <= 0: return reject("physical block is vLLM's null block") - if entry_boundary != boundary_tokens: - return reject("entry boundary differs from the store plan") - seen_groups.add(group_index) + if entry_boundary < boundary_tokens: + continue + if entry_boundary > boundary_tokens: + return reject("entry boundary is ahead of the store plan") + if group_index in seen_target_groups: + return reject("multiple blocks claim the same recurrent group") + seen_target_groups.add(group_index) overrides.append((group_index, block_id)) - if seen_groups != required_groups: + if not overrides: + return latched + if seen_target_groups != required_groups: return reject("entries do not cover every aligned recurrent group") validated = tuple(sorted(overrides)) if latched and validated != latched: diff --git a/sparkcache/test_defect_regressions.py b/sparkcache/test_defect_regressions.py index d5a53ae..b46b30f 100644 --- a/sparkcache/test_defect_regressions.py +++ b/sparkcache/test_defect_regressions.py @@ -1238,10 +1238,18 @@ def test_nonaligned_boundary_waits_for_partial_tail_cow_mapping(self) -> None: output.num_scheduled_tokens[request.req_id] = ( self.NONALIGNED_PROMPT_TOKENS ) + output.recurrent_boundary_blocks = { + request.req_id: [(1, self.BOUNDARY_BLOCK, self.BOUNDARY)] + } first_metadata = connector.build_connector_meta(output) self.assertEqual(first_metadata.plans, []) self.assertIn(request.req_id, connector._store_progress) + self.assertNotIn(request.req_id, connector._store_recurrent_boundaries) + self.assertEqual( + connector.counters["recurrent_boundary_metadata_rejected"], + 0, + ) pending = connector.build_connector_meta( self._cached_scheduler_output( num_computed_tokens=self.NONALIGNED_PROMPT_TOKENS, @@ -1337,6 +1345,11 @@ def test_chunked_prefill_validates_only_at_publication_step(self) -> None: middle = self._cached_scheduler_output( num_computed_tokens=2304, num_scheduled_tokens=2304, + recurrent_boundary_blocks={ + "dflash-recurrent-boundary": [ + (1, self.BOUNDARY_BLOCK - 1, 2304) + ] + }, ) self.assertEqual(connector.build_connector_meta(middle).plans, []) self.assertIn("dflash-recurrent-boundary", connector._store_progress) @@ -1423,7 +1436,7 @@ def test_missing_or_wrong_request_metadata_keeps_publication_pending(self) -> No def test_contradictory_boundary_metadata_skips_publication(self) -> None: invalid_entries = ( [], - [(1, self.BOUNDARY_BLOCK, self.BOUNDARY - 256)], + [(1, self.BOUNDARY_BLOCK, self.BOUNDARY + 256)], [(2, self.BOUNDARY_BLOCK, self.BOUNDARY)], [(1, self.BOUNDARY_BLOCK, self.BOUNDARY), (1, 43, self.BOUNDARY)], [(1, 0, self.BOUNDARY)], From c56f77f97b3da907d32e888d82046359a62f0f88 Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:50:28 -0500 Subject: [PATCH 13/15] Log future recurrent proof identity Keep future-boundary evidence fail-closed, but include its observed boundary, target boundary, recurrent group, and physical block in the rejection reason. This makes the live scheduler hand-off diagnosable without changing acceptance semantics. Cache namespace impact: none. CacheIdentity values, digest salts, chunk geometry, manifests, page deltas, and page-tail-cow-v1 are unchanged. Validation: SparkCache 765 passed, 7 skipped; deploy 108 passed, 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 | 6 ++- sparkcache/test_defect_regressions.py | 46 +++++++++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index 4fa973c..cda756a 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": "490d2c069c2eb755ecb93727aa47c41df38665427228895af0638b8588a049f3" + "source_sha256": "788686e858ba4af01f535e95122c7650f412fddc40cd221a0924f4ce2b32ff98" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 5a972b8..d8e66c8 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": "490d2c069c2eb755ecb93727aa47c41df38665427228895af0638b8588a049f3" + "source_sha256": "788686e858ba4af01f535e95122c7650f412fddc40cd221a0924f4ce2b32ff98" }, "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 05c4753..4803044 100644 --- a/sparkcache/spark_context_cache_connector.py +++ b/sparkcache/spark_context_cache_connector.py @@ -1680,7 +1680,11 @@ def reject(reason: str) -> None: if entry_boundary < boundary_tokens: continue if entry_boundary > boundary_tokens: - return reject("entry boundary is ahead of the store plan") + return reject( + "entry boundary is ahead of the store plan" + f" observed={entry_boundary} target={boundary_tokens}" + f" group={group_index} block={block_id}" + ) if group_index in seen_target_groups: return reject("multiple blocks claim the same recurrent group") seen_target_groups.add(group_index) diff --git a/sparkcache/test_defect_regressions.py b/sparkcache/test_defect_regressions.py index b46b30f..c7d3a1a 100644 --- a/sparkcache/test_defect_regressions.py +++ b/sparkcache/test_defect_regressions.py @@ -1478,6 +1478,52 @@ def test_contradictory_boundary_metadata_skips_publication(self) -> None: 1, ) + def test_future_boundary_rejection_logs_observed_mapping_identity(self) -> None: + output = self._scheduler_output() + request = output.scheduled_new_reqs[0] + request.prompt_token_ids = list(range(self.NONALIGNED_PROMPT_TOKENS)) + output.num_scheduled_tokens[request.req_id] = self.NONALIGNED_PROMPT_TOKENS + with tempfile.TemporaryDirectory() as directory: + connector = _make_connector( + Path(directory), + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=self._config(), + extra_config={"spark_cache_model_profile": "glm53-flash-hybrid"}, + ) + connector.build_connector_meta(output) + + with mock.patch.object( + connector_module.logger, "warning" + ) as warning: + metadata = connector.build_connector_meta( + self._cached_scheduler_output( + num_computed_tokens=self.NONALIGNED_PROMPT_TOKENS, + recurrent_boundary_blocks={ + request.req_id: [ + ( + 1, + self.COW_BLOCK, + self.NONALIGNED_BOUNDARY + 256, + ) + ] + }, + ) + ) + + self.assertEqual(metadata.plans, []) + warning.assert_called_once() + log_args = warning.call_args.args + self.assertIn( + "entry boundary is ahead of the store plan" + " observed=8448 target=8192 group=1 block=142", + log_args[0] % log_args[1:], + ) + def test_partial_recurrent_group_coverage_skips_publication(self) -> None: config = self._config() second_recurrent = types.SimpleNamespace( From b830c84d93a80db869e0cfeed433f330bd611a7b Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 04:32:29 -0500 Subject: [PATCH 14/15] Advertise exact recurrent publication targets Expose SparkCache's 256-token recurrent publication granularity and a side-effect-free per-request boundary proposal. Proposals exist only for eligible non-streaming recurrent stores and use the same aligned-span rule as the store plan. Advance the exact lease contract for vLLM's request-local target propagation, MultiConnector union, and exact secondary-hash CoW proof. Cache namespace impact: none. CacheIdentity values, digest salts, 256-token geometry, manifests, page deltas, and page-tail-cow-v1 are unchanged. Validation: SparkCache 766 passed, 7 skipped; deploy 108 passed, 1 skipped; exact 12-file verifier, Ruff, and diff checks passed. --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- .../source-receipt.json | 2 +- sparkcache/README.md | 3 ++ ...st_glm53_b12x_kda_adaptive_mtp_contract.py | 3 ++ ...-contract-glm53-b12x-kda-adaptive-mtp.json | 28 +++++++++++---- sparkcache/spark_context_cache_connector.py | 24 +++++++++++++ sparkcache/test_defect_regressions.py | 35 +++++++++++++++++++ 8 files changed, 89 insertions(+), 10 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index cda756a..1e6d800 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": "788686e858ba4af01f535e95122c7650f412fddc40cd221a0924f4ce2b32ff98" + "source_sha256": "09b74cf425c5a4f6149cc9e9c518a50a679996c9abe2e6d176a3fd95ff66250a" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index d8e66c8..fe81b61 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": "788686e858ba4af01f535e95122c7650f412fddc40cd221a0924f4ce2b32ff98" + "source_sha256": "09b74cf425c5a4f6149cc9e9c518a50a679996c9abe2e6d176a3fd95ff66250a" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json index f4a4d28..695cd84 100644 --- a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json +++ b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json @@ -62,7 +62,7 @@ ], "contract": { "path": "sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json", - "sha256": "f36ed14eaf1f97a5dffa94bda8151b1e0fa182afc0d121b757b70bebc6a43811" + "sha256": "9d5c9a4c4d4efdc56560d63135f5e85a1e083c2bf635b52a8ba1ad2ab86d4da8" }, "result": "All four SparkCache patches apply in order to the LF source tree and produce the exact preimages required by the recurrent-boundary producer. The eleven-file final-runtime contract verifies only after that producer creates its four recurrent postimages; it also covers the unchanged live-tensor B12X KDA surface." } diff --git a/sparkcache/README.md b/sparkcache/README.md index e763e6f..6b7b264 100644 --- a/sparkcache/README.md +++ b/sparkcache/README.md @@ -229,6 +229,9 @@ when placement completes and intentionally excludes that bookkeeping. boundary, vLLM may retain the replay-boundary page outside the advancing request block table. Its `SchedulerOutput.recurrent_boundary_blocks` hand-off names the pinned physical block by request, group, and token boundary. + `recurrent_boundary_granularity` advertises SparkCache's 256-token publication + boundary to the exact vLLM scheduler without changing vLLM's native hash + geometry. SparkCache defers a new recurrent request until a later cached scheduler step, when the preceding forward's hand-off can be observed. It latches one matching entry for every recurrent group, including a partial-tail CoW target when the diff --git a/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py b/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py index 85a7d3e..b87b356 100644 --- a/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py +++ b/sparkcache/runtime_patches/test_glm53_b12x_kda_adaptive_mtp_contract.py @@ -21,6 +21,8 @@ SOURCE_ROLE = "source_built_glm53_b12x_kda_adaptive_mtp" RECURRENT_BOUNDARY_ROLE = "recurrent_boundary_contract" RECURRENT_BOUNDARY_FILES = { + "vllm/distributed/kv_transfer/kv_connector/v1/base.py", + "vllm/distributed/kv_transfer/kv_connector/v1/multi_connector.py", "vllm/v1/core/kv_cache_manager.py", "vllm/v1/core/sched/output.py", "vllm/v1/core/sched/scheduler.py", @@ -67,6 +69,7 @@ def test_glm53_b12x_kda_adaptive_mtp_contract_attests_the_complete_sparkcache_vl assert contract["vllm_commit"] == VLLM_COMMIT assert {record["path"] for record in contract["files"]} == { "vllm/distributed/kv_transfer/kv_connector/v1/base.py", + "vllm/distributed/kv_transfer/kv_connector/v1/multi_connector.py", "vllm/distributed/kv_transfer/kv_connector/utils.py", "vllm/v1/core/sched/scheduler.py", "vllm/v1/core/kv_cache_manager.py", diff --git a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json index 7de6ead..d49558b 100644 --- a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json +++ b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json @@ -5,12 +5,23 @@ { "path": "vllm/distributed/kv_transfer/kv_connector/v1/base.py", "accepted_sha256": { - "recurrent_boundary_contract": "bc1965431087676876f58360cd9cc07ab6c06febe6d747695f10b051fd85c412" + "recurrent_boundary_contract": "7460e0638c0c81808dbdbbad7114db2e840e531e9ce83360c55b9171fcc872b1" }, "required_symbols": [ "KVConnectorBase_V1.handle_preemptions", "KVConnectorBase_V1.request_finished", - "KVConnectorBase_V1.get_finished" + "KVConnectorBase_V1.get_finished", + "KVConnectorBase_V1.get_recurrent_publication_boundaries" + ] + }, + { + "path": "vllm/distributed/kv_transfer/kv_connector/v1/multi_connector.py", + "accepted_sha256": { + "recurrent_boundary_contract": "8741b86b0f3e91af06d01a52240cdc988e3073bf049f72b506f56d6ceebb1ac0" + }, + "required_symbols": [ + "MultiConnector.supports_recurrent_boundary_blocks", + "MultiConnector.get_recurrent_publication_boundaries" ] }, { @@ -26,7 +37,7 @@ { "path": "vllm/v1/core/sched/scheduler.py", "accepted_sha256": { - "recurrent_boundary_contract": "260f36ce8fabf70c193b20009ea465eea7b1b6c8e9fb72f2307a01ba8fcf7b2a" + "recurrent_boundary_contract": "74afcccd11b0ad48cab3925c623cb310effbf425825bc278bed03163848c528f" }, "required_symbols": [ "Scheduler.schedule", @@ -35,13 +46,15 @@ "Scheduler._finalize_shared_prefix_leases", "Scheduler._update_from_kv_xfer_finished", "Scheduler._update_requests_with_invalid_blocks", - "Scheduler._free_blocks" + "Scheduler._free_blocks", + "Scheduler._recurrent_publication_boundaries", + "Scheduler._recurrent_publication_boundary_at" ] }, { "path": "vllm/v1/core/kv_cache_manager.py", "accepted_sha256": { - "recurrent_boundary_contract": "c5b83d382c96b2bf8c466a993ed77123a14a971e2661797128533319388d0b5f" + "recurrent_boundary_contract": "2c646969b750f6cb4e17fe8a6bf12993d01eefbfbe74e5f8c755f7e0d929faf2" }, "required_symbols": [ "KVCacheManager.get_block_ids", @@ -52,7 +65,8 @@ "KVCacheManager.discard_shared_prefix_lease", "KVCacheManager.evict_shared_prefix_leases_until_free", "KVCacheManager.take_kv_cache_block_copies", - "KVCacheManager.take_recurrent_boundary_blocks" + "KVCacheManager.take_recurrent_boundary_blocks", + "KVCacheManager.allocate_slots" ] }, { @@ -100,7 +114,7 @@ { "path": "vllm/v1/core/single_type_kv_cache_manager.py", "accepted_sha256": { - "recurrent_boundary_contract": "f67a1850a7e0288baaa6d42e7ec55b22b09c156720767e23acaabedcae333c8a" + "recurrent_boundary_contract": "abbedbd7b9165bbb005c9ba7ceb4367ee1987ac47c632a629dd17ed08ae1db0c" }, "required_symbols": [ "SingleTypeKVCacheManager.add_local_computed_blocks", diff --git a/sparkcache/spark_context_cache_connector.py b/sparkcache/spark_context_cache_connector.py index 4803044..d0e74d6 100644 --- a/sparkcache/spark_context_cache_connector.py +++ b/sparkcache/spark_context_cache_connector.py @@ -543,6 +543,30 @@ class SparkContextCacheConnector(KVConnectorBase_V1, SupportsHMA): # every referenced page before request/preemption cleanup may release it. supports_recurrent_boundary_blocks = True + @property + def recurrent_boundary_granularity(self) -> int: + """Token boundary used for connector-owned recurrent publication.""" + + return self._chunk_tokens + + def get_recurrent_publication_boundaries( + self, request: "Request" + ) -> tuple[int, ...]: + """Propose the exact eligible store boundary without mutating state.""" + + if ( + not self._cache_available + or not self._store_enabled + or self._streaming_snapshots_enabled + or not self._recurrent_group_indexes + ): + return () + prompt_token_ids = getattr(request, "prompt_token_ids", None) or () + span = self._aligned_span(len(prompt_token_ids)) + if not self._min_span <= span <= self._max_span: + return () + return (span,) + configure_streaming_snapshot_runtime = staticmethod( configure_streaming_snapshot_runtime ) diff --git a/sparkcache/test_defect_regressions.py b/sparkcache/test_defect_regressions.py index c7d3a1a..79ff7ff 100644 --- a/sparkcache/test_defect_regressions.py +++ b/sparkcache/test_defect_regressions.py @@ -1163,6 +1163,15 @@ def test_explicit_boundary_block_round_trips_through_manifest_store(self) -> Non ) self.assertTrue(scheduler.supports_recurrent_boundary_blocks) + self.assertEqual(scheduler.recurrent_boundary_granularity, 256) + self.assertEqual( + scheduler.get_recurrent_publication_boundaries( + types.SimpleNamespace( + prompt_token_ids=list(range(self.NONALIGNED_PROMPT_TOKENS)) + ) + ), + (self.NONALIGNED_BOUNDARY,), + ) metadata = scheduler.build_connector_meta(output) self.assertEqual(len(metadata.plans), 1) @@ -1250,6 +1259,7 @@ def test_nonaligned_boundary_waits_for_partial_tail_cow_mapping(self) -> None: connector.counters["recurrent_boundary_metadata_rejected"], 0, ) + pending = connector.build_connector_meta( self._cached_scheduler_output( num_computed_tokens=self.NONALIGNED_PROMPT_TOKENS, @@ -1285,6 +1295,31 @@ def test_nonaligned_boundary_waits_for_partial_tail_cow_mapping(self) -> None: 0, ) + def test_publication_boundary_proposal_obeys_store_policy(self) -> None: + request = types.SimpleNamespace( + prompt_token_ids=list(range(self.NONALIGNED_PROMPT_TOKENS)) + ) + with tempfile.TemporaryDirectory() as directory: + disabled = _make_connector( + Path(directory), + 0, + block_size=256, + role=KVConnectorRole.SCHEDULER, + override_worker_rank=False, + tp=1, + dcp=1, + kv_cache_config=self._config(), + extra_config={ + "spark_cache_model_profile": "glm53-flash-hybrid", + "spark_cache_store": "0", + }, + ) + + self.assertEqual( + disabled.get_recurrent_publication_boundaries(request), + (), + ) + def test_conflicting_mapping_poisons_latched_publication(self) -> None: output = self._scheduler_output( { From 65b6642df1afc64366430d3aef9aca01f5c5e1c3 Mon Sep 17 00:00:00 2001 From: FujitsuPolycom <87842395+FujitsuPolycom@users.noreply.github.com> Date: Sun, 30 Aug 2026 05:19:58 -0500 Subject: [PATCH 15/15] Pin intermediate allocations below publication targets Advance the exact scheduler postimage after fixing restored-prefix scheduling: a future connector proposal is passed to KVCacheManager only when the current allocation's finalized end reaches the target. Intermediate 8K allocations retain no request-local publication target. Cache namespace impact: none. CacheIdentity, digest salts, chunk geometry, manifests, page deltas, and page-tail-cow-v1 are unchanged. Validation: strict twelve-file verifier and focused source/profile tests passed; the preceding full SparkCache run passed 766 tests with 7 skips. --- deploy/deepseek_v4/tp4_profile.json | 2 +- deploy/glm52_35bpw/profile.json | 2 +- patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json | 2 +- ...llm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/deepseek_v4/tp4_profile.json b/deploy/deepseek_v4/tp4_profile.json index 1e6d800..9a83a20 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": "09b74cf425c5a4f6149cc9e9c518a50a679996c9abe2e6d176a3fd95ff66250a" + "source_sha256": "a2add45a9f97446f6c2a843355161da9a5499ff7501b4750d2163591785d7345" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index fe81b61..c26f883 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": "09b74cf425c5a4f6149cc9e9c518a50a679996c9abe2e6d176a3fd95ff66250a" + "source_sha256": "a2add45a9f97446f6c2a843355161da9a5499ff7501b4750d2163591785d7345" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json index 695cd84..ac9aff9 100644 --- a/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json +++ b/patches/vllm-glm53-b12x-kda-adaptive-mtp/source-receipt.json @@ -62,7 +62,7 @@ ], "contract": { "path": "sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json", - "sha256": "9d5c9a4c4d4efdc56560d63135f5e85a1e083c2bf635b52a8ba1ad2ab86d4da8" + "sha256": "8adbdfa3fd4b06b213c3aab45255a0b039f1c9940a4b1fad0efd004d263227c9" }, "result": "All four SparkCache patches apply in order to the LF source tree and produce the exact preimages required by the recurrent-boundary producer. The eleven-file final-runtime contract verifies only after that producer creates its four recurrent postimages; it also covers the unchanged live-tensor B12X KDA surface." } diff --git a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json index d49558b..f8f108e 100644 --- a/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json +++ b/sparkcache/runtime_patches/vllm-kv-block-lease-contract-glm53-b12x-kda-adaptive-mtp.json @@ -37,7 +37,7 @@ { "path": "vllm/v1/core/sched/scheduler.py", "accepted_sha256": { - "recurrent_boundary_contract": "74afcccd11b0ad48cab3925c623cb310effbf425825bc278bed03163848c528f" + "recurrent_boundary_contract": "494bfd8758e32d3b265fe4df92e8c93b91292899baf5fafe8b490d27bd15cd0f" }, "required_symbols": [ "Scheduler.schedule",