[P/D][Mooncake] Add KVConnectorStats for transfer observability (#40414)
Signed-off-by: Zhewen Li <zhewenli@inferact.ai> Co-authored-by: Zhewen Li <zhewenli@inferact.ai>
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import threading
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector import (
|
||||
MooncakeConnector,
|
||||
MooncakeConnectorWorker,
|
||||
SendBlockMeta,
|
||||
)
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.stats import (
|
||||
MooncakeKVConnectorStats,
|
||||
)
|
||||
|
||||
|
||||
def test_is_empty_on_fresh_stats():
|
||||
stats = MooncakeKVConnectorStats()
|
||||
assert stats.is_empty()
|
||||
assert stats.num_successful_transfers == 0
|
||||
|
||||
|
||||
def test_record_transfer_and_reduce():
|
||||
stats = MooncakeKVConnectorStats()
|
||||
# 1 MB transfer in 1 ms -> 1000 MB/s throughput
|
||||
stats.record_transfer(duration_s=0.001, total_bytes=1 * 2**20, num_descs=4)
|
||||
# 2 MB transfer in 2 ms
|
||||
stats.record_transfer(duration_s=0.002, total_bytes=2 * 2**20, num_descs=6)
|
||||
assert not stats.is_empty()
|
||||
assert stats.num_successful_transfers == 2
|
||||
|
||||
reduced = stats.reduce()
|
||||
assert reduced["Num successful transfers"] == 2
|
||||
# avg = (1 + 2) / 2 = 1.5 ms
|
||||
assert reduced["Avg xfer time (ms)"] == 1.5
|
||||
assert reduced["Avg MB per transfer"] == 1.5
|
||||
# 3 MB total / 3 ms total = 1000 MB/s
|
||||
assert reduced["Throughput (MB/s)"] == 1000.0
|
||||
assert reduced["Avg number of descriptors"] == 5.0
|
||||
assert reduced["Num failed transfers"] == 0
|
||||
assert reduced["Num failed recvs"] == 0
|
||||
assert reduced["Num KV expired reqs"] == 0
|
||||
|
||||
|
||||
def test_record_failures_keeps_stats_non_empty():
|
||||
stats = MooncakeKVConnectorStats()
|
||||
stats.record_failed_transfer()
|
||||
stats.record_failed_recv()
|
||||
stats.record_kv_expired_req()
|
||||
assert not stats.is_empty()
|
||||
|
||||
reduced = stats.reduce()
|
||||
# No successful transfers -> latency/throughput all zero, but failure
|
||||
# counters still surface.
|
||||
assert reduced["Num successful transfers"] == 0
|
||||
assert reduced["Num failed transfers"] == 1
|
||||
assert reduced["Num failed recvs"] == 1
|
||||
assert reduced["Num KV expired reqs"] == 1
|
||||
|
||||
|
||||
def test_aggregate_sums_observations():
|
||||
a = MooncakeKVConnectorStats()
|
||||
b = MooncakeKVConnectorStats()
|
||||
a.record_transfer(duration_s=0.001, total_bytes=1 * 2**20, num_descs=1)
|
||||
b.record_transfer(duration_s=0.002, total_bytes=2 * 2**20, num_descs=2)
|
||||
b.record_failed_transfer()
|
||||
|
||||
a.aggregate(b)
|
||||
|
||||
assert a.num_successful_transfers == 2
|
||||
reduced = a.reduce()
|
||||
assert reduced["Num successful transfers"] == 2
|
||||
assert reduced["Num failed transfers"] == 1
|
||||
|
||||
|
||||
def test_aggregate_with_empty_other_is_noop():
|
||||
a = MooncakeKVConnectorStats()
|
||||
a.record_transfer(duration_s=0.001, total_bytes=1, num_descs=1)
|
||||
b = MooncakeKVConnectorStats()
|
||||
|
||||
a.aggregate(b)
|
||||
|
||||
assert a.num_successful_transfers == 1
|
||||
|
||||
|
||||
def test_getstate_drops_lock_and_setstate_recreates_it():
|
||||
# KVConnectorStats subclasses must be picklable (worker→scheduler IPC),
|
||||
# but threading.Lock isn't — so __getstate__ strips it and __setstate__
|
||||
# rebuilds a fresh per-process lock.
|
||||
original = MooncakeKVConnectorStats()
|
||||
original.record_transfer(duration_s=0.01, total_bytes=2048, num_descs=3)
|
||||
|
||||
state = original.__getstate__()
|
||||
assert "_lock" not in state
|
||||
|
||||
rebuilt = MooncakeKVConnectorStats.__new__(MooncakeKVConnectorStats)
|
||||
rebuilt.__setstate__(state)
|
||||
assert rebuilt.data == original.data
|
||||
# Lock works on the receiver side.
|
||||
rebuilt.record_transfer(duration_s=0.02, total_bytes=4096, num_descs=5)
|
||||
assert rebuilt.num_successful_transfers == 2
|
||||
|
||||
|
||||
def test_concurrent_writers_keep_row_lengths_aligned():
|
||||
# Multiple writers + a snapshot reader must never produce a snapshot
|
||||
# with mismatched column lengths — reduce()'s
|
||||
# len(descs) == num_successful_transfers assertion would fire.
|
||||
stats = MooncakeKVConnectorStats()
|
||||
stop = threading.Event()
|
||||
writer_count = 4
|
||||
snapshots: list[MooncakeKVConnectorStats] = []
|
||||
|
||||
def writer():
|
||||
i = 0
|
||||
while not stop.is_set():
|
||||
stats.record_transfer(
|
||||
duration_s=0.001 + i * 1e-9,
|
||||
total_bytes=1024 + i,
|
||||
num_descs=1 + (i % 8),
|
||||
)
|
||||
i += 1
|
||||
|
||||
def snapper():
|
||||
while not stop.is_set():
|
||||
snap = stats.clone_and_reset()
|
||||
if not snap.is_empty():
|
||||
# Force the same path the logger walks; reduce() will
|
||||
# blow up on torn rows via its internal assert.
|
||||
snap.reduce()
|
||||
snapshots.append(snap)
|
||||
|
||||
threads = [threading.Thread(target=writer) for _ in range(writer_count)]
|
||||
snapshotter = threading.Thread(target=snapper)
|
||||
for t in threads:
|
||||
t.start()
|
||||
snapshotter.start()
|
||||
# Short fixed window — long enough to interleave thousands of ops.
|
||||
threading.Event().wait(0.2)
|
||||
stop.set()
|
||||
for t in threads:
|
||||
t.join()
|
||||
snapshotter.join()
|
||||
|
||||
# Final drain so we don't lose the in-flight tail.
|
||||
final = stats.clone_and_reset()
|
||||
if not final.is_empty():
|
||||
final.reduce()
|
||||
snapshots.append(final)
|
||||
|
||||
# Every snapshot's columns must have identical lengths (the invariant
|
||||
# the lock protects), and the union must contain at least one row.
|
||||
total_rows = 0
|
||||
for snap in snapshots:
|
||||
n = len(snap.data["transfer_duration"])
|
||||
assert len(snap.data["bytes_transferred"]) == n
|
||||
assert len(snap.data["num_descriptors"]) == n
|
||||
total_rows += n
|
||||
assert total_rows > 0
|
||||
|
||||
|
||||
def test_clone_and_reset_hands_off_old_data():
|
||||
stats = MooncakeKVConnectorStats()
|
||||
stats.record_transfer(duration_s=0.001, total_bytes=1, num_descs=1)
|
||||
stats.record_failed_recv()
|
||||
|
||||
snapshot = stats.clone_and_reset()
|
||||
|
||||
assert snapshot.num_successful_transfers == 1
|
||||
assert not snapshot.is_empty()
|
||||
# Original is now empty.
|
||||
assert stats.is_empty()
|
||||
assert stats.num_successful_transfers == 0
|
||||
# Recording on the original does not mutate the snapshot.
|
||||
stats.record_transfer(duration_s=0.005, total_bytes=2, num_descs=2)
|
||||
assert snapshot.num_successful_transfers == 1
|
||||
|
||||
|
||||
def test_build_kv_connector_stats_none_returns_empty_instance():
|
||||
out = MooncakeConnector.build_kv_connector_stats()
|
||||
assert isinstance(out, MooncakeKVConnectorStats)
|
||||
assert out.is_empty()
|
||||
|
||||
|
||||
def test_build_kv_connector_stats_with_data_round_trips():
|
||||
original = MooncakeKVConnectorStats()
|
||||
original.record_transfer(duration_s=0.01, total_bytes=1024, num_descs=3)
|
||||
original.record_failed_transfer()
|
||||
|
||||
# Serialized form is the .data dict; build should reconstruct an instance
|
||||
# that behaves the same.
|
||||
rebuilt = MooncakeConnector.build_kv_connector_stats(data=original.data)
|
||||
|
||||
assert isinstance(rebuilt, MooncakeKVConnectorStats)
|
||||
assert rebuilt.num_successful_transfers == 1
|
||||
assert rebuilt.reduce()["Num failed transfers"] == 1
|
||||
|
||||
|
||||
def _bare_worker() -> MooncakeConnectorWorker:
|
||||
"""Construct a MooncakeConnectorWorker skipping __init__ (full init requires
|
||||
a live TransferEngine). Only the attributes touched by the methods under
|
||||
test are populated; role flags and async_zmq_ctx keep __del__'s shutdown
|
||||
path a no-op."""
|
||||
worker = MooncakeConnectorWorker.__new__(MooncakeConnectorWorker)
|
||||
worker.xfer_stats = MooncakeKVConnectorStats()
|
||||
worker.engine = MagicMock()
|
||||
worker.async_zmq_ctx = MagicMock()
|
||||
worker.is_kv_consumer = True
|
||||
worker.is_kv_producer = True
|
||||
return worker
|
||||
|
||||
|
||||
def test_send_blocks_records_success():
|
||||
worker = _bare_worker()
|
||||
worker.engine.batch_transfer_sync_write.return_value = 0
|
||||
|
||||
ret = worker._send_blocks(
|
||||
"host:1234",
|
||||
src_ptrs=[0x1000, 0x2000],
|
||||
dst_ptrs=[0x3000, 0x4000],
|
||||
lengths=[1024, 2048],
|
||||
)
|
||||
|
||||
assert ret == 0
|
||||
assert worker.xfer_stats.num_successful_transfers == 1
|
||||
data = worker.xfer_stats.data
|
||||
assert data["bytes_transferred"] == [1024 + 2048]
|
||||
assert data["num_descriptors"] == [2]
|
||||
assert data["num_failed_transfers"] == []
|
||||
|
||||
|
||||
def test_send_blocks_records_failure():
|
||||
worker = _bare_worker()
|
||||
worker.engine.batch_transfer_sync_write.return_value = 1 # non-zero = fail
|
||||
|
||||
ret = worker._send_blocks("host:1234", [0x1000], [0x2000], [4096])
|
||||
|
||||
assert ret == 1
|
||||
assert worker.xfer_stats.num_successful_transfers == 0
|
||||
assert worker.xfer_stats.data["num_failed_transfers"] == [1]
|
||||
|
||||
|
||||
def test_get_kv_connector_stats_returns_none_when_empty():
|
||||
worker = _bare_worker()
|
||||
|
||||
assert worker.get_kv_connector_stats() is None
|
||||
|
||||
|
||||
def test_get_kv_connector_stats_returns_and_resets():
|
||||
worker = _bare_worker()
|
||||
worker.engine.batch_transfer_sync_write.return_value = 0
|
||||
worker._send_blocks("host:1234", [0x1000], [0x2000], [4096])
|
||||
|
||||
snapshot = worker.get_kv_connector_stats()
|
||||
assert isinstance(snapshot, MooncakeKVConnectorStats)
|
||||
assert snapshot.num_successful_transfers == 1
|
||||
|
||||
# Second call returns None because the worker's stats were reset.
|
||||
assert worker.get_kv_connector_stats() is None
|
||||
|
||||
|
||||
def test_expired_request_bumps_counter():
|
||||
import asyncio
|
||||
|
||||
worker = _bare_worker()
|
||||
worker.reqs_need_send = {
|
||||
"tid1": SendBlockMeta(
|
||||
p_req_id="req1",
|
||||
transfer_id="tid1",
|
||||
local_block_ids=[0, 1],
|
||||
ready=asyncio.Event(),
|
||||
expire_time=-1.0, # Already expired.
|
||||
sending=0,
|
||||
),
|
||||
}
|
||||
worker.finished_sending_reqs = set()
|
||||
|
||||
asyncio.run(worker.fetch_finished_sending_reqs())
|
||||
|
||||
assert worker.xfer_stats.data["num_kv_expired_reqs"] == [1]
|
||||
# Expired transfer also cleaned out of reqs_need_send.
|
||||
assert "tid1" not in worker.reqs_need_send
|
||||
@@ -31,10 +31,14 @@ from vllm.distributed.kv_transfer.kv_connector.v1.base import (
|
||||
KVConnectorRole,
|
||||
SupportsHMA,
|
||||
)
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.metrics import KVConnectorStats
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_utils import (
|
||||
MooncakeBootstrapServer,
|
||||
RegisterWorkerPayload,
|
||||
)
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.stats import (
|
||||
MooncakeKVConnectorStats,
|
||||
)
|
||||
from vllm.distributed.parallel_state import (
|
||||
get_pp_group,
|
||||
get_tensor_model_parallel_rank,
|
||||
@@ -457,6 +461,25 @@ class MooncakeConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
def wait_for_save(self):
|
||||
pass
|
||||
|
||||
def get_kv_connector_stats(self) -> KVConnectorStats | None:
|
||||
"""Return worker-local transfer stats since the last call.
|
||||
|
||||
Note the P/D asymmetry: because Mooncake is P-push (P calls
|
||||
batch_transfer_sync_write), P records successful transfer latency,
|
||||
bytes, and descriptor counts, while D only records failures
|
||||
(recv/ZMQ errors). Aggregated NIXL-style dashboards will find
|
||||
successful-transfer metrics on the P worker, not D.
|
||||
"""
|
||||
if self.connector_worker is None:
|
||||
return None
|
||||
return self.connector_worker.get_kv_connector_stats()
|
||||
|
||||
@classmethod
|
||||
def build_kv_connector_stats(
|
||||
cls, data: dict[str, Any] | None = None
|
||||
) -> KVConnectorStats | None:
|
||||
return MooncakeKVConnectorStats(data=data or {})
|
||||
|
||||
|
||||
class MooncakeConnectorScheduler:
|
||||
"""Implementation of Scheduler side methods"""
|
||||
@@ -816,6 +839,8 @@ class MooncakeConnectorWorker:
|
||||
self.finished_sending_reqs: set[ReqId] = set()
|
||||
self.finished_recving_reqs: set[ReqId] = set()
|
||||
|
||||
self.xfer_stats = MooncakeKVConnectorStats()
|
||||
|
||||
self.block_size = vllm_config.cache_config.block_size
|
||||
self.model_config = vllm_config.model_config
|
||||
self.cache_config = vllm_config.cache_config
|
||||
@@ -1340,11 +1365,23 @@ class MooncakeConnectorWorker:
|
||||
ret_value = self.engine.batch_transfer_sync_write(
|
||||
remote_session, src_ptrs, dst_ptrs, lengths
|
||||
)
|
||||
duration = time.perf_counter() - start_time
|
||||
if ret_value == 0:
|
||||
logger.debug(
|
||||
"Sending to %s done, took %s",
|
||||
self.xfer_stats.record_transfer(
|
||||
duration_s=duration,
|
||||
total_bytes=sum(lengths),
|
||||
num_descs=len(src_ptrs),
|
||||
)
|
||||
logger.debug("Sending to %s done, took %s", remote_session, duration)
|
||||
else:
|
||||
self.xfer_stats.record_failed_transfer()
|
||||
logger.warning(
|
||||
"Sending to %s failed (ret=%s) after %s (%d descriptors, %d bytes)",
|
||||
remote_session,
|
||||
time.perf_counter() - start_time,
|
||||
ret_value,
|
||||
duration,
|
||||
len(src_ptrs),
|
||||
sum(lengths),
|
||||
)
|
||||
return ret_value
|
||||
|
||||
@@ -1445,6 +1482,7 @@ class MooncakeConnectorWorker:
|
||||
send_meta.p_req_id,
|
||||
envs.VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT,
|
||||
)
|
||||
self.xfer_stats.record_kv_expired_req()
|
||||
finished_sending_reqs.add(send_meta.p_req_id)
|
||||
expired_transfer_id.append(transfer_id)
|
||||
|
||||
@@ -1485,6 +1523,13 @@ class MooncakeConnectorWorker:
|
||||
|
||||
return finished_sending_reqs or None, finished_recving_reqs or None
|
||||
|
||||
def get_kv_connector_stats(self) -> KVConnectorStats | None:
|
||||
"""Return transfer stats collected since the last call, or None
|
||||
if nothing has been recorded in this interval."""
|
||||
if self.xfer_stats.is_empty():
|
||||
return None
|
||||
return self.xfer_stats.clone_and_reset()
|
||||
|
||||
async def receive_kv_from_single_worker(
|
||||
self,
|
||||
worker_addr: str,
|
||||
@@ -1531,6 +1576,7 @@ class MooncakeConnectorWorker:
|
||||
req_ids,
|
||||
response.err_msg,
|
||||
)
|
||||
self.xfer_stats.record_failed_recv()
|
||||
return
|
||||
self.process_pulling_result(response, pull_metas)
|
||||
if response.status == MooncakeXferResponseStatus.FINISH:
|
||||
@@ -1539,6 +1585,7 @@ class MooncakeConnectorWorker:
|
||||
logger.debug("ZMQ context terminated, exiting Mooncake receiver thread.")
|
||||
except Exception as e:
|
||||
logger.error("MooncakeXferMetadata transfer failed for %s: %s", req_ids, e)
|
||||
self.xfer_stats.record_failed_recv()
|
||||
return
|
||||
|
||||
def process_pulling_result(
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Stats container for the Mooncake connector."""
|
||||
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.metrics import (
|
||||
KVConnectorStats,
|
||||
)
|
||||
|
||||
# TODO(mooncake-stats): add MooncakePromMetrics (mirror NixlPromMetrics)
|
||||
# and wire it via MooncakeConnector.build_prom_metrics in a follow-up PR.
|
||||
|
||||
|
||||
@dataclass
|
||||
class MooncakeKVConnectorStats(KVConnectorStats):
|
||||
"""Container for Mooncake KV transfer performance metrics.
|
||||
|
||||
`_lock` serializes record_* against clone_and_reset so each row's
|
||||
appends are atomic and column lengths stay aligned. Writers run on
|
||||
the sender pool / receiver loop / sender loop; reader runs on the
|
||||
main worker thread.
|
||||
"""
|
||||
|
||||
def __post_init__(self):
|
||||
self._lock = threading.Lock()
|
||||
if not self.data:
|
||||
self.reset()
|
||||
|
||||
# threading.Lock is not picklable; strip it from the wire form and
|
||||
# rebuild a fresh per-process lock on the receiver side.
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
state.pop("_lock", None)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def reset(self):
|
||||
self.data: dict[str, list[float | int]] = {
|
||||
"transfer_duration": [],
|
||||
"bytes_transferred": [],
|
||||
"num_descriptors": [],
|
||||
"num_failed_transfers": [],
|
||||
"num_failed_recvs": [],
|
||||
"num_kv_expired_reqs": [],
|
||||
}
|
||||
|
||||
def record_transfer(self, duration_s: float, total_bytes: int, num_descs: int):
|
||||
with self._lock:
|
||||
self.data["transfer_duration"].append(duration_s)
|
||||
self.data["bytes_transferred"].append(total_bytes)
|
||||
self.data["num_descriptors"].append(num_descs)
|
||||
|
||||
# Failure counters store a list of 1s so a future Prom counter can iterate
|
||||
# with .inc(list_item), mirroring NIXL's NixlPromMetrics.observe.
|
||||
def record_failed_transfer(self):
|
||||
with self._lock:
|
||||
self.data["num_failed_transfers"].append(1)
|
||||
|
||||
def record_failed_recv(self):
|
||||
with self._lock:
|
||||
self.data["num_failed_recvs"].append(1)
|
||||
|
||||
def record_kv_expired_req(self):
|
||||
with self._lock:
|
||||
self.data["num_kv_expired_reqs"].append(1)
|
||||
|
||||
def clone_and_reset(self) -> "MooncakeKVConnectorStats":
|
||||
# Copy lists under the lock for length alignment; return a fresh
|
||||
# instance so the snapshot has its own _lock.
|
||||
with self._lock:
|
||||
snapshot_data: dict[str, list[float | int]] = {
|
||||
k: list(v) for k, v in self.data.items()
|
||||
}
|
||||
self.reset()
|
||||
return MooncakeKVConnectorStats(data=snapshot_data)
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
return (
|
||||
self.num_successful_transfers == 0
|
||||
and len(self.data["num_failed_transfers"]) == 0
|
||||
and len(self.data["num_failed_recvs"]) == 0
|
||||
and len(self.data["num_kv_expired_reqs"]) == 0
|
||||
)
|
||||
|
||||
def aggregate(self, other: KVConnectorStats) -> KVConnectorStats:
|
||||
if not other.is_empty():
|
||||
for k, v in other.data.items():
|
||||
accumulator = self.data[k]
|
||||
assert isinstance(accumulator, list)
|
||||
accumulator.extend(v)
|
||||
return self
|
||||
|
||||
def reduce(self) -> dict[str, int | float]:
|
||||
num_failed_transfers = len(self.data["num_failed_transfers"])
|
||||
num_failed_recvs = len(self.data["num_failed_recvs"])
|
||||
num_kv_expired_reqs = len(self.data["num_kv_expired_reqs"])
|
||||
|
||||
if self.num_successful_transfers == 0:
|
||||
return {
|
||||
"Num successful transfers": 0,
|
||||
"Avg xfer time (ms)": 0,
|
||||
"P90 xfer time (ms)": 0,
|
||||
"Avg MB per transfer": 0,
|
||||
"Throughput (MB/s)": 0,
|
||||
"Avg number of descriptors": 0,
|
||||
"Num failed transfers": num_failed_transfers,
|
||||
"Num failed recvs": num_failed_recvs,
|
||||
"Num KV expired reqs": num_kv_expired_reqs,
|
||||
}
|
||||
|
||||
xfer_time = np.asarray(self.data["transfer_duration"])
|
||||
mb = np.asarray(self.data["bytes_transferred"]) / 2**20
|
||||
descs = np.asarray(self.data["num_descriptors"], dtype=np.uint32)
|
||||
n = len(descs)
|
||||
assert n == self.num_successful_transfers
|
||||
|
||||
total_mb = mb.sum()
|
||||
avg_mb = total_mb / n
|
||||
total_time_seconds = xfer_time.sum()
|
||||
throughput_mb_s = (
|
||||
total_mb / total_time_seconds if total_time_seconds > 0 else 0.0
|
||||
)
|
||||
|
||||
return {
|
||||
"Num successful transfers": n,
|
||||
"Avg xfer time (ms)": round(xfer_time.mean() * 1e3, 3),
|
||||
"P90 xfer time (ms)": round(np.percentile(xfer_time, 90).item() * 1e3, 3),
|
||||
"Avg MB per transfer": round(avg_mb, 3),
|
||||
"Throughput (MB/s)": round(throughput_mb_s, 3),
|
||||
"Avg number of descriptors": round(descs.mean(), 1),
|
||||
"Num failed transfers": num_failed_transfers,
|
||||
"Num failed recvs": num_failed_recvs,
|
||||
"Num KV expired reqs": num_kv_expired_reqs,
|
||||
}
|
||||
|
||||
@property
|
||||
def num_successful_transfers(self) -> int:
|
||||
return len(self.data["transfer_duration"])
|
||||
Reference in New Issue
Block a user