From c161fd0dc00eb33dce8b342acb570757cbfc3897 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] 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. Cache namespace impact: none. CacheIdentity values, digest salts, chunk geometry, and page-delta wire schemas are unchanged. Validation: python -m pytest sparkcache -q (740 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 +- 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 16e4a2e..f581692 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": "837441e44ca690b38a468005a8fafb6a36700b6c4fd45555d13ab814b411bce9" }, "model": { "repository": "deepseek-ai/DeepSeek-V4-Flash-0731", diff --git a/deploy/glm52_35bpw/profile.json b/deploy/glm52_35bpw/profile.json index 4ef0b23..9502d8e 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": "837441e44ca690b38a468005a8fafb6a36700b6c4fd45555d13ab814b411bce9" }, "model": { "repository": "brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78", diff --git a/sparkcache/README.md b/sparkcache/README.md index 36d9534..db20678 100644 --- a/sparkcache/README.md +++ b/sparkcache/README.md @@ -208,14 +208,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 c0e938e..020c8a2 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 9336817..43a98ca 100644 --- a/sparkcache/test_defect_regressions.py +++ b/sparkcache/test_defect_regressions.py @@ -849,6 +849,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, )