Mooncake Transfer Engine Benchmark Tool (tebench) Guide#
tebench is an end-to-end benchmarking tool for the Mooncake Transfer Engine.
It evaluates bandwidth and latency across different (block_size, batch_size, concurrency) combinations, and supports both the classic TE backend and the new TENT backend.
1. Build and Deployment#
1.1 Prerequisites#
A C++ toolchain capable of building the project
Optional: GPU(s) and corresponding transport stacks (RDMA, shared memory, io_uring, etc.)
Exact dependencies (CUDA / OFED / io_uring / etc.) depend on the transport backend being used.
1.2 Build with TENT Enabled#
Typical out-of-tree build:
mkdir -p build
cd build
cmake .. -DUSE_TENT=ON
cmake --build . -j
2. Execution Model#
tebench runs in two roles:
Target (receiver): creates a memory segment and waits for connections
Initiator (sender): connects to a target and executes benchmark cases
Role Selection#
If
--target_seg_nameis empty → Target modeIf
--target_seg_nameis set → Initiator mode
Backend Selection#
--backend=classic→ classic TE implementation--backend=tent→ TENT implementation
3. Quick Start#
3.1 Start Target (First)#
On the target machine:
./tebench \
--seg_type=DRAM \
--backend=tent
The program will print a command containing a generated segment name:
To start initiators, run
./tebench --target_seg_name=<SEG_NAME> --seg_type=DRAM --backend=tent
Press Ctrl-C to terminate
Copy the printed --target_seg_name to the initiator side.
3.2 Start Initiator#
On the initiator machine:
./tebench \
--target_seg_name=<SEG_NAME_FROM_TARGET> \
--seg_type=DRAM \
--backend=tent \
--op_type=read \
--duration=5
3.3 Request Pacing#
Use --request_interval_us=<N> to add a per-thread delay before each
transfer batch. The value is in microseconds; 0 disables pacing.
When pacing is enabled, Avg Lat (us) includes the pacing gap because it is
computed from wall-clock runtime, while Avg Tx (us) and Tx percentiles only
measure transfer execution time.
4. Output Metrics#
Each output row corresponds to one benchmark configuration.
Column |
Description |
|---|---|
|
Block size per request (bytes) |
|
Number of requests per submission |
|
Throughput (total bytes / total time) |
|
Average per-transfer instantaneous bandwidth |
|
Average wall-clock per-operation latency, including pacing or scheduling gaps (scaled by thread count) |
|
Average per-transfer execution time, excluding gaps |
|
P99 transfer latency |
|
P999 transfer latency |
A short (~1 second) warmup phase is executed before measurements begin.
4.1 QoS Metrics Baseline#
Use --qos_classes to partition a fixed number of worker threads into QoS
classes:
name:threads:slo_us:weight[:isolated_gbps],...
For readability, the same contract can be supplied as JSON with
--qos_classes_json:
[
{"name": "foreground", "threads": 4, "slo_us": 1000, "weight": 4, "isolated_gbps": 12.5},
{"name": "checkpoint", "threads": 12, "slo_us": 0, "weight": 1, "isolated_gbps": 10.0}
]
Use only one of --qos_classes and --qos_classes_json.
For example, the following closed-loop mixed workload assigns four workers to an SLO-constrained foreground class and twelve workers to a best-effort checkpoint class:
./tebench \
--target_seg_name=<SEG> \
--backend=tent \
--start_num_threads=16 \
--max_num_threads=16 \
--qos_classes=foreground:4:1000:4:12.5,checkpoint:12:0:1:10.0 \
--qos_link_capacity_gbps=25 \
--qos_output_jsonl=qos-results.jsonl
The class thread counts must add up to the fixed start_num_threads value.
An slo_us of zero marks a best-effort class. The SLO is a reporting threshold:
QoS baseline mode measures whether each completed transfer meets it, without
changing request scheduling policy on either backend.
The human-readable summary and the optional versioned JSONL record report:
Metric |
Definition |
|---|---|
|
Fraction of completed batches whose measured end-to-end transfer time is at most |
|
P99 end-to-end batch transfer latency for the class |
|
Class throughput multiplied by SLO attainment; best-effort classes use attainment 1 |
|
Sum of |
|
Jain index over per-class |
|
|
|
Aggregate measured throughput divided by |
Isolation leakage requires a matching class-only baseline, supplied as the
optional fifth class field. Total utilization requires
--qos_link_capacity_gbps. Missing baselines are emitted as N/A in text and
null in JSON rather than being inferred from the mixed run. Run isolated and
mixed cases with the same block size, batch size, transport, memory type, and
host pair. JSONL records retain isolated_throughput_gbps and
link_capacity_gbps alongside the derived values so every metric can be
recomputed from one record.
4.2 Mixed Foreground/Background Traffic#
The QoS class flags above use one block size, batch size, and request intent for
all workers. Use --workload_classes_json when classes must generate different
transfer shapes concurrently, for example latency-sensitive reads competing
with a bulk KV migration:
./tebench \
--target_seg_name=<SEG> \
--backend=tent \
--start_num_threads=8 \
--max_num_threads=8 \
--workload_classes_json='[
{"name":"foreground","threads":2,"block_size":4096,"batch_size":1,
"intent_type":"foreground_get","deadline_us":250,"slo_us":300,"weight":4},
{"name":"migration","threads":6,"block_size":4194304,"batch_size":2,
"intent_type":"migration","slo_us":0,"weight":1}
]' \
--qos_link_capacity_gbps=25 \
--qos_output_jsonl=mixed-traffic.jsonl
This mode runs one fixed workload instead of sweeping the global block and
batch size flags. Class thread counts must add up to the fixed benchmark thread
count. deadline_us is optional and controls TENT deadline tagging;
slo_us remains the independent reporting threshold. Per-class
transferred_bytes is included in text and JSONL output so throughput is
computed from each class’s actual transfer size.
--workload_classes_json is mutually exclusive with --qos_classes,
--qos_classes_json, and the global --tent_intent_type. Non-default
per-class intents and deadlines require the TENT backend.
4.3 Per-Target Metrics#
Multi-target runs print one [target-summary] line per target. Use
--result_output_jsonl=<path> to also append a schema-versioned JSON record for
each benchmark configuration. The record keeps the aggregate operation, byte,
and throughput totals plus each target’s segment name, assigned thread count,
completed operations, transferred bytes, throughput, and latency distribution.
The aggregate throughput uses the pooled average worker duration, matching the
existing BW (GB/s) table calculation.
Targets with no assigned worker are retained with zero-valued metrics. This
makes an under-provisioned run (threads < targets) visible instead of silently
dropping targets from the result.
5. Runtime Configuration#
This section summarizes the key runtime options that control workload behavior, resource usage, and backend selection.
5.1 Workload Mode (--op_type)#
Controls the transfer pattern executed by the initiator:
read— repeated READ transferswrite— repeated WRITE transfersmix— alternating WRITE followed by READ (both recorded)
Example:
--op_type=write
5.2 Data Consistency Check (--check_consistency)#
When enabled, the benchmark validates correctness in mix mode:
Source buffers are filled with a known pattern before WRITE
Data is READ back and verified on the initiator
Example:
./tebench --target_seg_name=<SEG> --op_type=mix --check_consistency=true
Consistency checking introduces CPU-side overhead and should be disabled for pure performance measurements.
5.3 Notification Feature (--notifi)#
When enabled, the benchmark sends a notification message along with each transfer batch:
The notification contains the
target_addras the message payloadThe peer can verify the notification was received correctly by checking the message
Useful for testing notification delivery and end-to-end communication
Example:
./tebench --target_seg_name=<SEG> --notifi=true
This feature is primarily for testing notification mechanisms. The notification message contains the target address for verification purposes.
5.4 Segment, Concurrency, and Memory Layout#
Segment and role
--seg_name: local segment name (typically left empty)--seg_type:DRAM | VRAM(default:DRAM)--seg_type_mix: comma-separated segment types for mixed DRAM+VRAM runs, e.g.dram,vram. When set, the target registers buffers of each listed type in one segment, and initiator threads round-robin across them so a single tebench process drives traffic over multiple memory types (and thus multiple transports — SHM for DRAM, NVLink for VRAM) concurrently. Empty falls back to--seg_type(single type, existing behavior). See Section 5.8 for usage and the multi-transport configuration it requires.--target_seg_name: target segment name (empty → Target mode). A comma-separated list enables multi-target initiator mode, and worker threads are distributed across all listed target segments.
Scan ranges
--total_buffer_size: total buffer limit (bytes)--start_block_size,--max_block_size: block size sweep (powers of two)--start_batch_size,--max_batch_size: batch size sweep (powers of two)--start_num_threads,--max_num_threads: thread sweep (powers of two)--duration: measurement time per case (seconds)
A test case is skipped when:
block_size × batch_size × num_threads > total_buffer_size
Multi-target initiator
Use a comma-separated --target_seg_name value when one initiator process
should send traffic to multiple target segments:
./tebench \
--backend=tent \
--metadata_type=p2p \
--target_seg_name=<SEG_A>,<SEG_B>,<SEG_C> \
--op_type=read \
--start_num_threads=3 \
--max_num_threads=3
Thread i selects target i % target_count. Within the selected target, the
local target-thread index is i / target_count, so increasing the thread count
spreads traffic across targets before advancing to the next buffer slot inside
each target. --target_gpu_id shifts the per-target buffer slot and does not
change the target selection order.
For multi-node benchmarks, tebench intentionally stays at the endpoint level:
each process publishes its own segment, and an external launcher decides which
target segment list each initiator receives. This keeps M-to-N, fan-out,
incast, and all-to-all topologies as different launch configurations over the
same comma-separated --target_seg_name primitive.
For multiple initiator processes sharing the same target segment, use
--target_offset and --target_range_size to partition the remote address
space. The range size is relative to each initiator; tebench also validates that
target_offset + relative_offset + transfer_size stays inside the actual target
buffer.
# Initiator 0 uses [0, 512MiB)
./tebench --target_seg_name=<SEG> --target_offset=0 --target_range_size=536870912
# Initiator 1 uses [512MiB, 1GiB)
./tebench --target_seg_name=<SEG> --target_offset=536870912 --target_range_size=536870912
For read-only verification with multiple readers, first write deterministic data
using --op_type=write_seed, then run readers with --op_type=read_verify
against the same target range. read_verify performs pure READs and validates
the local buffer without modifying the remote data. Do not combine these modes
with --check_consistency; --check_consistency remains the existing
WRITE→READ self-check mode.
5.5 GPU Affinity#
--local_gpu_id: initiator base GPU ID--target_gpu_id: target base GPU ID
Each worker thread uses:
gpu_id + thread_id
5.6 Backend, Transport, and Metadata#
Backend
--backend:classic | tent(default:tent)
Transport (TENT only)
--xport_type:rdma | shm | mnnvl | gds | iouring. Selects a single transport to enable (all others are disabled). Empty means no transport is explicitly enabled or disabled by tebench — the engine reads the transport enable list from theMC_TENT_CONFconfig file (see Section 5.8 for multi-transport scenarios).--tent_intent_type: attach a standard transfer intent to every request, such asforeground_get,background_prefetch, orcheckpoint. This is useful for validating intent-specific transport and QoS policy selection.
Metadata service
--metadata_type:p2p | etcd | redis | http(default:p2p)--metadata_url_list: comma-separated URLs (ignored inp2pmode)
5.7 QoS Reporting#
--qos_classes: class/thread/SLO/weight contract described in Section 4.1--workload_classes_json: class-specific transfer shape, intent, deadline, and QoS contract described in Section 4.2--qos_link_capacity_gbps: measured usable link capacity in decimal GB/s--qos_output_jsonl: append one schema-versioned JSON object per benchmark configuration--result_output_jsonl: append aggregate and per-target metrics for each benchmark configuration
QoS mode intentionally requires a fixed thread count. Sweep offered load by running explicit cases with different class thread allocations so every output record has an unambiguous workload contract.
5.8 Mixed DRAM+VRAM and Multi-Transport#
By default, tebench allocates buffers of a single --seg_type and enables a
single --xport_type per run. To exercise a mixed-transport workload where
the engine’s transport selector naturally picks SHM for DRAM→DRAM transfers
and NVLink for VRAM→VRAM transfers within one process, combine
--seg_type_mix with the MC_TENT_CONF environment variable:
Enable multiple transports via
MC_TENT_CONF— point it at a JSON config file (or inline JSON string) that enables the transports you want active:{ "transports": { "shm": {"enable": true}, "nvlink": {"enable": true} } }
Leave
--xport_typeempty so tebench does not override the config’s enable list.MC_TENT_CONFis read by the engine’sConfigHelperand applies to both target and initiator.Register a mixed DRAM+VRAM segment via
--seg_type_mix:# Target: mixed DRAM+VRAM segment, all GPUs MC_TENT_CONF=/path/to/config.json ./tebench --backend=tent \ --metadata_type=p2p --seg_name=tgtmix --seg_type_mix=dram,vram \ --local_gpu_id=-1 & # Initiator: mixed DRAM+VRAM, 8 threads (4 DRAM→SHM, 4 VRAM→NVLink) MC_TENT_CONF=/path/to/config.json ./tebench --backend=tent \ --metadata_type=p2p --target_seg_name=<SEG> --seg_type_mix=dram,vram \ --local_gpu_id=-1 --op_type=write --duration=30 \ --start_num_threads=8 --max_num_threads=8
Initiator threads round-robin across the listed seg_types by
thread_id(even→DRAM, odd→VRAM). The engine selects transport based on the target buffer’s location — SHM forcpu:Nbuffers, NVLink forcuda:Nbuffers — so/metricswill show bothtransport="shm"andtransport="nvlink"labels with non-zero counts from a single metrics endpoint.--local_gpu_id=-1is recommended for VRAM so each worker thread gets its own GPU buffer. With the default--local_gpu_id=0, all VRAM threads share one GPU buffer and contend, which underrepresents NVLink throughput.
Mixed mode requires --start_num_threads ≥ 2 (at least one thread per
seg_type) and a build with USE_CUDA=ON (for VRAM/NVLink support). When
--seg_type_mix is empty, tebench falls back to --seg_type (single type)
and --xport_type (single transport) — the existing behavior.