Skip to content

Commit d16c9aa

Browse files
authored
feat: Add batched_contains() override for NixlDynamicStorageBackend (LMCache#2966)
Add batched_contains() override for NixlDynamicStorageBackend Signed-off-by: Jason Goldschmidt <jason.goldschmidt@dell.com>
1 parent a421106 commit d16c9aa

3 files changed

Lines changed: 436 additions & 9 deletions

File tree

.buildkite/pipeline.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ steps:
3838
--cov-report=xml:coverage-test.xml --html=durations/test.html \
3939
--ignore=tests/disagg --ignore=tests/v1/test_pos_kernels.py \
4040
--ignore=tests/v1/test_nixl_storage.py \
41+
--ignore=tests/v1/test_nixl_batched_contains.py \
4142
--ignore=tests/skipped \
4243
--ignore=tests/v1/storage_backend/test_eic.py
4344

lmcache/v1/storage_backend/nixl_storage_backend.py

Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@
7070

7171
logger = init_logger(__name__)
7272

73+
# Max concurrency for parallel S3 HEAD requests in batched_contains().
74+
_CONTAINS_BATCH_SIZE = 16
75+
7376

7477
@dataclass
7578
class NixlStorageConfig:
@@ -557,6 +560,40 @@ def nixl_desc_exists(self, meta_info: str) -> bool:
557560
logger.warning(f"NIXL Desc {meta_info} query failed: {exc}")
558561
return False
559562

563+
def batched_nixl_desc_exists(
564+
self, reg_list: List[tuple[int, int, int, str]]
565+
) -> int:
566+
"""Check if multiple descriptors exist via a single ``query_memory`` call.
567+
568+
:param reg_list: List of tuples ``(0, 0, 0, meta_info)`` where
569+
*meta_info* is the formatted object-key string.
570+
:return: Number of consecutive descriptors that exist from the
571+
start of the list.
572+
:raises: No exceptions are raised. Errors from the underlying
573+
``query_memory`` call are caught internally and logged as
574+
warnings; the method returns ``0`` in that case.
575+
"""
576+
if not reg_list:
577+
return 0
578+
579+
try:
580+
resp = self.nixl_agent.query_memory(
581+
reg_list, self.backend, mem_type=self.mem_type
582+
)
583+
# nixl api query_memory returns a list of nixlRegDesc
584+
# Count consecutive descriptors that exist (resp[i] is not None)
585+
consecutive_count = 0
586+
for reg_desc in resp:
587+
if reg_desc is not None:
588+
consecutive_count += 1
589+
else:
590+
break
591+
592+
return consecutive_count
593+
except Exception as exc:
594+
logger.warning(f"NIXL batched query failed: {exc}")
595+
return 0
596+
560597
def close(self):
561598
self.nixl_agent.release_dlist_handle(self.mem_xfer_handler)
562599
self.nixl_agent.deregister_memory(self.mem_reg_descs)
@@ -1323,6 +1360,29 @@ def exists_in_put_tasks(self, key: CacheEngineKey) -> bool:
13231360
with self.progress_lock:
13241361
return key in self.progress_set
13251362

1363+
def _exists_in_put_tasks_or_cache(self, key: CacheEngineKey) -> tuple[bool, bool]:
1364+
"""Check whether key exists in put tasks or presence cache.
1365+
1366+
This method only checks the local data structures and does not
1367+
call the expensive key_exists operation.
1368+
1369+
:param key: The key to check
1370+
:return: Tuple of (found, result) where:
1371+
- found: True if we determined the result locally
1372+
- result: True if key exists, False if key doesn't exist
1373+
(in put tasks)
1374+
"""
1375+
# Check if already in progress
1376+
if self.exists_in_put_tasks(key):
1377+
logger.debug(f"Key {key.chunk_hash:x} is in put tasks")
1378+
return True, False
1379+
1380+
# Check presence cache before hitting remote storage if not prefetching
1381+
if self._cache_contains(key.chunk_hash):
1382+
return True, True
1383+
1384+
return False, False
1385+
13261386
def contains(self, key: CacheEngineKey, pin: bool = False) -> bool:
13271387
"""
13281388
Check whether key is in the storage backend.
@@ -1336,21 +1396,70 @@ def contains(self, key: CacheEngineKey, pin: bool = False) -> bool:
13361396
13371397
:return: True if the key exists, False otherwise
13381398
"""
1339-
# Check if already in progress
1340-
if self.exists_in_put_tasks(key):
1341-
logger.debug(f"Key {key.chunk_hash:x} is in put tasks")
1342-
return False
1343-
1344-
# Check presence cache before hitting remote storage if not prefetching
1345-
if self._cache_contains(key.chunk_hash):
1346-
return True
1399+
# Check local data structures first
1400+
found, local_result = self._exists_in_put_tasks_or_cache(key)
1401+
if found:
1402+
return local_result
13471403

13481404
xfer_state = self.key_exists(key)
13491405
if xfer_state:
13501406
self._cache_add(key.chunk_hash)
13511407

13521408
return xfer_state
13531409

1410+
def batched_contains(
1411+
self,
1412+
keys: List[CacheEngineKey],
1413+
pin: bool = False,
1414+
) -> int:
1415+
"""Check whether the keys are in the storage backend.
1416+
1417+
Overrides the sequential base-class implementation to issue a
1418+
single batched ``query_memory`` call for the keys that cannot
1419+
be resolved from local data structures (put-task set and
1420+
presence cache).
1421+
1422+
:param List[CacheEngineKey] keys: The keys of the MemoryObj.
1423+
:param bool pin: Whether to pin the key (not implemented).
1424+
:return: Number of contiguous hit chunks from the start of *keys*.
1425+
:raises: No exceptions are raised. Errors from the underlying
1426+
NIXL batched query are caught internally and logged as
1427+
warnings.
1428+
"""
1429+
if not keys:
1430+
return 0
1431+
1432+
# First, do fast sequential check of local data structures
1433+
true_count = 0
1434+
for key in keys:
1435+
found, result = self._exists_in_put_tasks_or_cache(key)
1436+
if found:
1437+
if result:
1438+
true_count += 1
1439+
else:
1440+
# Found in put tasks (False), stop the loop
1441+
return true_count
1442+
else:
1443+
# Not found locally, break to do expensive checks
1444+
break
1445+
1446+
# If we checked all keys locally, return the count
1447+
if true_count == len(keys):
1448+
return true_count
1449+
1450+
# For remaining keys, use the new batched_nixl_desc_exists method
1451+
remaining_keys = keys[true_count:]
1452+
reg_list = [(0, 0, 0, self._format_object_key(key)) for key in remaining_keys]
1453+
1454+
# Use the agent's batched_nixl_desc_exists method
1455+
consecutive_hits = self.agent.batched_nixl_desc_exists(reg_list)
1456+
1457+
# Update cache for the hits and return total count
1458+
for i in range(consecutive_hits):
1459+
self._cache_add(remaining_keys[i].chunk_hash)
1460+
1461+
return true_count + consecutive_hits
1462+
13541463
async def batched_async_contains(
13551464
self,
13561465
lookup_id: str,
@@ -1368,7 +1477,7 @@ async def batched_async_contains(
13681477
"""
13691478
n = len(keys)
13701479
idx = 0
1371-
batch_size = 16
1480+
batch_size = _CONTAINS_BATCH_SIZE
13721481

13731482
while idx < n:
13741483
batch = keys[idx : idx + batch_size]

0 commit comments

Comments
 (0)