Skip to content

Commit be1309c

Browse files
committed
[None][perf] kv_cache_manager_v2: batch block-key SHA-256 hashing
Hasher.update hashed each token of a block with its own int.to_bytes(8) + sha256.update() call. For long warm prefix matches this is the dominant cost of BlockRadixTree.match, which the attention-DP KV-cache-aware router (KVCacheAwareADPRouter) runs as a per-request probe on every DP rank before routing -- and which create_kv_cache repeats for the actual reuse lookup. Pack the whole token block into bytes once (array("Q", block).tobytes()) and do a single sha256.update(). All NVIDIA GPU host platforms (x86_64, aarch64/ Grace) are little-endian, so this is byte-identical to the per-token to_bytes(8, "little") loop -- block reuse / cross-run cache-hit behavior is unchanged. Multimodal blocks (which contain bytes items) fall back to the per-token loop via except (TypeError, OverflowError). Speeds up the probe and the create-time reuse lookup equally. On a GB300 Grace node the real BlockRadixTree.match warm-prefix cost at ISL~38k drops 2.85-3.05x at tokens_per_block=128/256 (DeepseekV4CacheManager). Adds TestBlockKeyHashing to lock in the bit-identical contract incl. multi-modal blocks. Signed-off-by: Lance Liao <108499334+lancelly@users.noreply.github.com>
1 parent e47f26e commit be1309c

2 files changed

Lines changed: 50 additions & 7 deletions

File tree

tensorrt_llm/runtime/kv_cache_manager_v2/_block_radix_tree.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# limitations under the License.
1515

1616
import hashlib
17+
from array import array
1718
from typing import TYPE_CHECKING, Iterable, Iterator, NamedTuple, Sequence, TypeVar, cast
1819

1920
from . import rawref
@@ -90,11 +91,21 @@ def update(self, data: int | bytes | Sequence[int | bytes]) -> "Hasher":
9091
elif type(data) is bytes:
9192
self._hasher.update(data)
9293
else:
93-
for item in data: # type: ignore
94-
assert (
95-
NDEBUG or (type(item) is int and (0 <= item < (1 << 64))) or type(item) is bytes
96-
)
97-
self._hasher.update(item.to_bytes(8, "little") if (type(item) is int) else item) # type: ignore
94+
# Hash the whole token block in one C call instead of one per token.
95+
# array("Q", data).tobytes() packs each int as 8 native-endian bytes;
96+
# all NVIDIA GPU host platforms (x86_64, aarch64/Grace) are little-endian
97+
# so this is byte-identical to the per-token to_bytes(8, "little") loop.
98+
# Falls back to that loop for multimodal blocks (which contain bytes items).
99+
try:
100+
self._hasher.update(array("Q", data).tobytes()) # type: ignore
101+
except (TypeError, OverflowError):
102+
for item in data: # type: ignore
103+
assert (
104+
NDEBUG
105+
or (type(item) is int and (0 <= item < (1 << 64)))
106+
or type(item) is bytes
107+
)
108+
self._hasher.update(item.to_bytes(8, "little") if (type(item) is int) else item) # type: ignore
98109
return self
99110

100111
@property

tests/unittest/kv_cache_manager_v2_tests/test_kv_cache_manager_v2.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import array
1616
import functools
1717
import gc
18+
import hashlib
1819
import itertools
1920
import os
2021
import random
@@ -52,7 +53,7 @@
5253
TokenIdExt,
5354
_KVCache,
5455
)
55-
from kv_cache_manager_v2._block_radix_tree import traverse_post_order
56+
from kv_cache_manager_v2._block_radix_tree import Hasher, traverse_post_order
5657
from kv_cache_manager_v2._common import (
5758
BAD_PAGE_INDEX,
5859
GPU_LEVEL,
@@ -105,7 +106,10 @@
105106
TokenIdExt,
106107
_KVCache,
107108
)
108-
from tensorrt_llm.runtime.kv_cache_manager_v2._block_radix_tree import traverse_post_order
109+
from tensorrt_llm.runtime.kv_cache_manager_v2._block_radix_tree import (
110+
Hasher,
111+
traverse_post_order,
112+
)
109113
from tensorrt_llm.runtime.kv_cache_manager_v2._common import (
110114
BAD_PAGE_INDEX,
111115
GPU_LEVEL,
@@ -2556,5 +2560,33 @@ def test_shrink_touched_pool(self) -> None:
25562560
allocator.release(s)
25572561

25582562

2563+
class TestBlockKeyHashing(unittest.TestCase):
2564+
"""Verify Hasher.update produces bit-identical digests to the per-token reference (no GPU needed)."""
2565+
2566+
@staticmethod
2567+
def _ref_update(seed: bytes, block: "list[int | bytes]") -> bytes:
2568+
h = hashlib.sha256()
2569+
h.update(seed)
2570+
for item in block:
2571+
h.update(item.to_bytes(8, "little") if type(item) is int else item)
2572+
return h.digest()
2573+
2574+
def test_update_int_block_matches_reference(self) -> None:
2575+
rng = random.Random(123)
2576+
seed = b"\xaa\xbb\xcc"
2577+
for n in (0, 1, 7, 32, 33, 257):
2578+
block = [rng.randint(0, (1 << 60)) for _ in range(n)]
2579+
self.assertEqual(
2580+
Hasher(seed).update(block).digest,
2581+
self._ref_update(seed, block),
2582+
f"int block of length {n}",
2583+
)
2584+
2585+
def test_update_mixed_multimodal_block(self) -> None:
2586+
block = [randbytes(32), 5, 6, randbytes(32)] + list(range(20))
2587+
seed = b"\x01"
2588+
self.assertEqual(Hasher(seed).update(block).digest, self._ref_update(seed, block))
2589+
2590+
25592591
if __name__ == "__main__":
25602592
unittest.main()

0 commit comments

Comments
 (0)