Skip to content

vllm.distributed.kv_transfer.kv_connector.v1.umbp.runtime.embedded

MORI-backed embedded DRAM offloading.

Classes:

EmbeddedRuntime

Bases: IUMBPRuntime

MORI-backed embedded DRAM runtime.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/umbp/runtime/embedded.py
class EmbeddedRuntime(IUMBPRuntime):
    """MORI-backed embedded DRAM runtime."""

    def __init__(self, config: UMBPRuntimeConfig) -> None:
        if config.options.get("backend") == "memory":
            raise ValueError(
                "The memory backend is test-only; use MORI for embedded mode"
            )
        self.options = _resolve_embedded_options(config)
        self.lookup_dir = self.options.get("lookup_dir", "/tmp")
        instance = self.options.get("lookup_instance", "")
        if not isinstance(instance, str):
            raise ValueError("lookup_instance must be a string")
        self.lookup_instance = f"{instance}:dp{config.dp_index}"

    def create_scheduler_handle(
        self,
        namespace: str,
        topology: RankTopology,
        layout: KVLayoutDescriptor,
    ) -> UMBPSchedulerHandle:
        del layout
        return _MoriSchedulerHandle(
            namespace, topology, self.lookup_dir, self.lookup_instance
        )

    def create_worker_handle(
        self,
        namespace: str,
        topology: RankTopology,
        layout: KVLayoutDescriptor,
    ) -> UMBPWorkerHandle:
        try:
            from mori.cpp import UMBPClient, UMBPConfig
        except ImportError as exc:
            raise RuntimeError(
                "Embedded UMBP requires MORI built with BUILD_UMBP=ON"
            ) from exc

        client_config = UMBPConfig()
        _configure_dram(client_config, self.options)
        total = self.options.get("_configured_total_capacity_bytes")
        if total is None:
            logger.info(
                "Embedded UMBP DRAM capacity: %.2f GiB per rank",
                client_config.dram.capacity_bytes / 1024**3,
            )
        else:
            logger.info(
                "Embedded UMBP DRAM capacity: %.2f GiB total, %.2f GiB per rank",
                total / 1024**3,
                client_config.dram.capacity_bytes / 1024**3,
            )
        return _MoriWorkerHandle(
            UMBPClient(client_config),
            namespace,
            topology,
            self.lookup_dir,
            int(self.options.get("num_workers", 4)),
            float(self.options.get("timeout_ms", 30000)) / 1000,
            layout,
            lookup_instance=self.lookup_instance,
        )

    @classmethod
    def from_config(cls, config: UMBPRuntimeConfig) -> IUMBPRuntime:
        return cls(config)

_MoriLookupServer

Small worker-local lookup bridge for the scheduler process.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/umbp/runtime/embedded.py
class _MoriLookupServer:
    """Small worker-local lookup bridge for the scheduler process."""

    def __init__(self, path: str, client: _LookupClient) -> None:
        self.path = path
        self.client = client
        self._stop = threading.Event()
        self._thread: threading.Thread | None = None
        self._socket: socket.socket | None = None
        self._owns_path = False

    def start(self) -> None:
        Path(self.path).parent.mkdir(parents=True, exist_ok=True)
        if os.path.exists(self.path) and _socket_has_listener(self.path):
            raise RuntimeError(
                f"UMBP lookup socket {self.path} is served by another live "
                "engine; set lookup_instance to a distinct value for each "
                "engine that serves this model on this host"
            )
        with contextlib.suppress(FileNotFoundError):
            os.unlink(self.path)
        self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self._socket.bind(self.path)
        self._owns_path = True
        self._socket.listen(16)
        self._socket.settimeout(0.2)
        self._thread = threading.Thread(
            target=self._serve,
            name="umbp-embedded-lookup",
            daemon=True,
        )
        self._thread.start()

    def _serve(self) -> None:
        assert self._socket is not None
        while not self._stop.is_set():
            try:
                connection, _ = self._socket.accept()
            except TimeoutError:
                continue
            except OSError:
                break
            with connection:
                connection.settimeout(_LOOKUP_TIMEOUT_S)
                try:
                    request = b""
                    while not request.endswith(b"\n"):
                        chunk = connection.recv(65536)
                        if not chunk:
                            break
                        request += chunk
                    payload = msgspec.json.decode(request)
                    if isinstance(payload, dict) and payload.get("op") == "clear":
                        connection.sendall(
                            msgspec.json.encode(bool(self.client.clear())) + b"\n"
                        )
                        continue
                    result = self.client.batch_exists(payload)
                    connection.sendall(
                        msgspec.json.encode([bool(value) for value in result]) + b"\n"
                    )
                except Exception:
                    with contextlib.suppress(OSError):
                        connection.sendall(b"[]\n")

    def close(self) -> None:
        self._stop.set()
        if self._socket is not None:
            self._socket.close()
        if self._thread is not None and self._thread.ident is not None:
            self._thread.join(timeout=2)
        if self._owns_path:
            with contextlib.suppress(FileNotFoundError):
                os.unlink(self.path)
            self._owns_path = False

_MoriWorkerHandle

Bases: UMBPWorkerHandle

Methods:

  • store_blocks –

    Store whole blocks from registered layouts; None requests ranges.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/umbp/runtime/embedded.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
class _MoriWorkerHandle(UMBPWorkerHandle):
    def __init__(
        self,
        client: _MoriClient,
        namespace: str,
        topology: RankTopology,
        lookup_dir: str,
        max_workers: int,
        timeout_s: float,
        layout: KVLayoutDescriptor | None = None,
        lookup_instance: str = "",
    ) -> None:
        self.client = client
        self._namespace = namespace
        self._topology = topology
        self._lookup_dir = lookup_dir
        self._lookup_instance = lookup_instance
        self._lookup_server: _MoriLookupServer | None = None
        self._registered_storages: set[int] = set()
        self._gpu_devices: set[int] = set()
        self._executor = ThreadPoolExecutor(
            max_workers=max_workers,
            thread_name_prefix="umbp-embedded-transfer",
        )
        self._job_lock = threading.Lock()
        self._futures: dict[int, Future[TransferJobState]] = {}
        self._store_deadlines: dict[int, float] = {}
        self._load_deadlines: dict[int, float] = {}
        self._timeout_s = timeout_s
        self._layout = layout
        self._load_layouts: dict[int, _BlockLoadLayout] = {}

    def register_buffers(self, kv_caches: dict[str, torch.Tensor]) -> None:
        try:
            from mori.cpp import MemoryLocationType
        except ImportError as exc:
            raise RuntimeError("MORI UMBP Python bindings are unavailable") from exc

        for cache in kv_caches.values():
            storage = cache.untyped_storage()
            storage_ptr = storage.data_ptr()
            if storage_ptr in self._registered_storages:
                continue
            location = (
                MemoryLocationType.GPU
                if cache.device.type == "cuda"
                else MemoryLocationType.CPU
            )
            device = cache.device.index if cache.device.index is not None else -1
            if not self.client.register_memory(
                storage_ptr, storage.nbytes(), location, device
            ):
                raise RuntimeError(
                    f"MORI UMBP failed to register KV storage 0x{storage_ptr:x}"
                )
            self._registered_storages.add(storage_ptr)
            if location == MemoryLocationType.GPU and device >= 0:
                self._gpu_devices.add(device)

        self._register_load_layouts(kv_caches)
        if self._lookup_server is None:
            self._lookup_server = _MoriLookupServer(
                _lookup_socket_path(
                    self._namespace,
                    self._topology.local_namespace,
                    self._lookup_dir,
                    self._lookup_instance,
                ),
                self,
            )
            self._lookup_server.start()

    def _register_load_layouts(self, kv_caches: dict[str, torch.Tensor]) -> None:
        self._load_layouts = {}
        if self._layout is None:
            return
        for group_id in {r.group_id for r in self._layout.regions}:
            bases: list[int] = []
            strides: list[int] = []
            sizes: list[int] = []
            offsets: list[int] = []
            counts: list[int] = []
            for region in self._layout.regions:
                if region.group_id != group_id:
                    continue
                cache = kv_caches.get(region.layer_name)
                if cache is None or cache.ndim == 0:
                    break
                stride = cache.stride(0) * cache.element_size()
                if (
                    stride <= 0
                    or stride > region.block_stride
                    or region.block_stride % stride
                    or region.block_bytes > stride
                ):
                    break
                bases.append(cache.data_ptr())
                strides.append(stride)
                offsets.append(sum(sizes))
                sizes.append(region.block_bytes)
                counts.append(cache.shape[0])
            else:
                self._load_layouts[group_id] = _BlockLoadLayout(
                    np.asarray(bases, dtype=np.uint64),
                    np.asarray(strides, dtype=np.uint64),
                    sizes,
                    offsets,
                    min(counts),
                    sum(sizes),
                )

    def _bulk_load_args(
        self, plans: Sequence[BlockTransferPlan]
    ) -> tuple[_LoadArgs, tuple[int, ...]] | None:
        groups: dict[int, list[tuple[int, int]]] = {}
        entries: Iterable[tuple[int | None, int]]
        if isinstance(plans, (BlockLoadBatch, BlockStoreBatch)):
            entries = zip(plans.group_ids, plans.block_ids, strict=True)
            keys = plans.keys
        else:
            if any(p.ranges for p in plans):
                return None
            entries = ((p.group_id, p.block_id) for p in plans)
            keys = [p.key for p in plans]
        for index, (group_id, block_id) in enumerate(entries):
            if group_id is None:
                return None
            layout = self._load_layouts.get(group_id)
            if layout is None or not 0 <= block_id < layout.num_blocks:
                return None
            groups.setdefault(group_id, []).append((index, block_id))
        pointers: list[list[int]] = [[]] * len(plans)
        sizes: list[list[int]] = [[]] * len(plans)
        offsets: list[list[int]] = [[]] * len(plans)
        plan_bytes = [0] * len(plans)
        for group_id, group_entries in groups.items():
            layout = self._load_layouts[group_id]
            ids = np.asarray([block for _, block in group_entries], dtype=np.uint64)
            addresses = (
                layout.bases[None, :] + ids[:, None] * layout.strides[None, :]
            ).tolist()
            for (index, _), row in zip(group_entries, addresses, strict=True):
                pointers[index] = row
                sizes[index] = layout.sizes
                offsets[index] = layout.offsets
                plan_bytes[index] = layout.num_bytes
        return (
            (keys, pointers, sizes, offsets),
            tuple(plan_bytes),
        )

    def load_blocks(
        self, plans: Sequence[BlockTransferPlan]
    ) -> TransferJobState | None:
        prepared = self._bulk_load_args(plans)
        if prepared is None:
            return None
        args, plan_bytes = prepared
        return self._submit_load(
            plans if isinstance(plans, BlockLoadBatch) else tuple(plans),
            args,
            plan_bytes,
        )

    def batch_exists(self, keys: Sequence[str]) -> Sequence[bool]:
        return [bool(value) for value in self.client.batch_exists(keys)]

    def clear(self) -> bool:
        return bool(self.client.clear())

    @staticmethod
    def _range_args(plans: Sequence[BlockTransferPlan], *, for_store: bool = True):
        keys, object_sizes, pointers, sizes, offsets = [], [], [], [], []
        for plan in plans:
            plan_pointers, plan_sizes, plan_offsets = [], [], []
            object_size = 0
            for item in plan.ranges:
                plan_pointers.append(item.base_address)
                plan_sizes.append(item.length)
                plan_offsets.append(item.object_offset)
                if for_store:
                    end = item.object_offset + item.length
                    if end > object_size:
                        object_size = end
            keys.append(plan.key)
            if for_store:
                object_sizes.append(object_size)
            pointers.append(plan_pointers)
            sizes.append(plan_sizes)
            offsets.append(plan_offsets)
        return keys, object_sizes, pointers, sizes, offsets

    def load(self, plans: Sequence[BlockTransferPlan]) -> TransferJobState:
        return self._submit_load(tuple(plans))

    def _submit_load(
        self,
        plans: tuple[BlockTransferPlan, ...] | BlockLoadBatch,
        range_args: _LoadArgs | None = None,
        plan_bytes: tuple[int, ...] | None = None,
    ) -> TransferJobState:
        job = TransferJobState(plans, plan_bytes=plan_bytes)
        job.start()
        future = self._executor.submit(self._load_sync, job, plans, range_args)
        with self._job_lock:
            self._futures[id(job)] = future
        return job

    def _load_sync(
        self,
        job: TransferJobState,
        plans: tuple[BlockTransferPlan, ...] | BlockLoadBatch,
        range_args: _LoadArgs | None = None,
    ) -> TransferJobState:
        with self._job_lock:
            self._load_deadlines[id(job)] = time.monotonic() + self._timeout_s
        if not plans:
            job.complete()
            return job
        if range_args is None:
            keys, _, pointers, sizes, offsets = self._range_args(plans, for_store=False)
        else:
            keys, pointers, sizes, offsets = range_args
        request_id = (
            plans.request_id
            if isinstance(plans, BlockLoadBatch)
            else plans[0].request_id
        )
        started_at = time.monotonic()
        logger.debug(
            "MORI UMBP range load started request=%s plans=%d ranges=%d bytes=%d",
            request_id,
            len(plans),
            sum(len(plan_sizes) for plan_sizes in sizes),
            sum(sum(plan_sizes) for plan_sizes in sizes),
        )
        results = self.client.batch_get_ranges_into_ptr(keys, pointers, sizes, offsets)
        logger.debug(
            "MORI UMBP range load returned request=%s plans=%d elapsed=%.3fs",
            request_id,
            len(plans),
            time.monotonic() - started_at,
        )
        completed = [key for key, ok in zip(keys, results, strict=True) if ok]
        failed = [key for key, ok in zip(keys, results, strict=True) if not ok]
        if completed:
            job.complete(completed)
        if failed:
            job.fail(failed, "MORI UMBP range load failed")
        return job

    def store_blocks(
        self, plans: Sequence[BlockTransferPlan]
    ) -> TransferJobState | None:
        """Store whole blocks from registered layouts; None requests ranges."""
        prepared = self._bulk_load_args(plans)
        if prepared is None:
            return None
        (keys, pointers, sizes, offsets), plan_bytes = prepared
        return self._submit_store(
            plans if isinstance(plans, BlockStoreBatch) else tuple(plans),
            (keys, list(plan_bytes), pointers, sizes, offsets),
            plan_bytes,
        )

    def store(self, plans: Sequence[BlockTransferPlan]) -> TransferJobState:
        return self._submit_store(tuple(plans))

    def _submit_store(
        self,
        plans: tuple[BlockTransferPlan, ...] | BlockStoreBatch,
        range_args: _StoreArgs | None = None,
        plan_bytes: tuple[int, ...] | None = None,
    ) -> TransferJobState:
        job = TransferJobState(plans, plan_bytes=plan_bytes)
        job.start()
        ready_events: list[torch.Event] = []
        for device in self._gpu_devices:
            with torch.device(f"cuda:{device}"):
                event = torch.Event()
                event.record(torch.accelerator.current_stream())
                ready_events.append(event)
        future = self._executor.submit(
            self._store_sync, job, plans, ready_events, range_args
        )
        with self._job_lock:
            self._futures[id(job)] = future
        return job

    def _store_sync(
        self,
        job: TransferJobState,
        plans: tuple[BlockTransferPlan, ...] | BlockStoreBatch,
        ready_events: list[torch.Event],
        range_args: _StoreArgs | None = None,
    ) -> TransferJobState:
        # Timed from here: a store queued behind others has not stalled.
        with self._job_lock:
            self._store_deadlines[id(job)] = time.monotonic() + self._timeout_s
        for event in ready_events:
            event.synchronize()
        if not plans:
            job.complete()
            return job
        keys, object_sizes, pointers, sizes, offsets = (
            self._range_args(plans) if range_args is None else range_args
        )
        results = self.client.batch_put_ranges_from_ptr(
            keys, object_sizes, pointers, sizes, offsets
        )
        failed_indices = [index for index, ok in enumerate(results) if not ok]
        for attempt in range(_STORE_RETRIES):
            if not failed_indices:
                break
            self.client.flush()
            time.sleep(_STORE_RETRY_BACKOFF_S * (attempt + 1))
            for index in failed_indices.copy():
                retry = self.client.batch_put_ranges_from_ptr(
                    [keys[index]],
                    [object_sizes[index]],
                    [pointers[index]],
                    [sizes[index]],
                    [offsets[index]],
                )
                if retry and retry[0]:
                    results[index] = True
                    failed_indices.remove(index)
            if failed_indices:
                logger.warning(
                    "MORI UMBP store retry %d left %d/%d objects pending",
                    attempt + 1,
                    len(failed_indices),
                    len(plans),
                )
        if not self.client.flush():
            job.fail([plan.key for plan in plans], "MORI UMBP flush failed")
            return job
        completed = [plan.key for plan, ok in zip(plans, results, strict=True) if ok]
        failed = [plan.key for plan, ok in zip(plans, results, strict=True) if not ok]
        if completed:
            job.complete(completed)
        if failed:
            job.fail(failed, "MORI UMBP range store failed")
        return job

    def wait(self, job: TransferJobState) -> TransferJobState:
        with self._job_lock:
            future = self._futures.get(id(job))
        if future is None:
            return job
        try:
            return future.result(timeout=self._timeout_s)
        except TimeoutError as exc:
            if not future.done():
                # The transfer still owns GPU pointers. Abort this engine step
                # rather than reporting completion and allowing block reuse.
                raise TimeoutError(
                    "MORI UMBP transfer timed out with buffers still in use"
                ) from exc
            job.fail([plan.key for plan in job.plans], str(exc))
            return job
        except Exception as exc:
            job.fail([plan.key for plan in job.plans], str(exc))
            return job
        finally:
            if future.done():
                with self._job_lock:
                    self._futures.pop(id(job), None)
                    self._store_deadlines.pop(id(job), None)
                    self._load_deadlines.pop(id(job), None)

    def poll(self, job: TransferJobState) -> TransferJobState | None:
        with self._job_lock:
            future = self._futures.get(id(job))
        if future is None:
            if job.status in (
                TransferJobStatus.COMPLETED,
                TransferJobStatus.FAILED,
                TransferJobStatus.CANCELLED,
            ):
                return job
            return None
        if not future.done():
            with self._job_lock:
                deadline = self._store_deadlines.get(
                    id(job), self._load_deadlines.get(id(job))
                )
            if deadline is not None and time.monotonic() >= deadline:
                raise TimeoutError(
                    "MORI UMBP transfer timed out with buffers still in use"
                )
            return None
        with self._job_lock:
            self._futures.pop(id(job), None)
            self._store_deadlines.pop(id(job), None)
            self._load_deadlines.pop(id(job), None)
        try:
            return future.result()
        except Exception as exc:
            job.fail([plan.key for plan in job.plans], str(exc))
            return job

    def publish(self, job: TransferJobState) -> None:
        if job.status.value != "completed":
            raise RuntimeError("cannot publish an incomplete MORI UMBP job")

    def cancel(self, job: TransferJobState) -> TransferJobState:
        with self._job_lock:
            future = self._futures.get(id(job))
        if future is None:
            return job
        if future.cancel():
            with self._job_lock:
                self._futures.pop(id(job), None)
                self._store_deadlines.pop(id(job), None)
                self._load_deadlines.pop(id(job), None)
            job.cancel("preempted")
            return job
        return self.wait(job)

    def take_evicted_keys(self) -> Sequence[str]:
        return ()

    def close(self) -> None:
        self._executor.shutdown(wait=True, cancel_futures=True)
        if self._lookup_server is not None:
            self._lookup_server.close()
        for storage_ptr in self._registered_storages:
            self.client.deregister_memory(storage_ptr)
        self._registered_storages.clear()
        close = getattr(self.client, "close", None)
        if callable(close):
            close()
        else:
            self.client.flush()

store_blocks(plans)

Store whole blocks from registered layouts; None requests ranges.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/umbp/runtime/embedded.py
def store_blocks(
    self, plans: Sequence[BlockTransferPlan]
) -> TransferJobState | None:
    """Store whole blocks from registered layouts; None requests ranges."""
    prepared = self._bulk_load_args(plans)
    if prepared is None:
        return None
    (keys, pointers, sizes, offsets), plan_bytes = prepared
    return self._submit_store(
        plans if isinstance(plans, BlockStoreBatch) else tuple(plans),
        (keys, list(plan_bytes), pointers, sizes, offsets),
        plan_bytes,
    )

_configure_dram(client_config, options)

Apply embedded-only host-DRAM options to MORI's client config.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/umbp/runtime/embedded.py
def _configure_dram(client_config: Any, options: dict[str, Any]) -> None:
    """Apply embedded-only host-DRAM options to MORI's client config."""
    dram = client_config.dram
    dram.capacity_bytes = options.get("capacity_bytes", 64 * 1024**3)
    option_fields = {
        "dram_use_shared_memory": "use_shared_memory",
        "dram_shm_name": "shm_name",
        "dram_high_watermark": "high_watermark",
        "dram_low_watermark": "low_watermark",
        "dram_use_hugepages": "use_hugepages",
        "dram_hugepage_size": "hugepage_size",
        "dram_numa_node": "numa_node",
        "dram_prefault": "prefault",
    }
    for option, field in option_fields.items():
        if option in options:
            setattr(dram, field, options[option])