Skip to content

Commit 6e440d2

Browse files
authored
StreamMailbox race fix and new pulsing backend bench (#97)
* stop tracking bench/results (covered by .gitignore) * fix rpc race * fix rpc race and add rpc docs with rpc benchmark
1 parent b996014 commit 6e440d2

37 files changed

Lines changed: 791 additions & 448 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<img src="docs/imgs/assets/logo.png" alt="DLSlime logo" width="44%">
33
</div>
44
<p align="center">
5-
<a href="https://deeplink-org.github.io/DLSlime">Docs</a> |
5+
<a href="https://deeplink-org.github.io/DLSlime"><img src="docs/imgs/assets/docs.png" width="16" height="16" style="vertical-align: middle;"> Docs </a> |
66
<a href="docs/roadmap.md"><img src="docs/imgs/assets/roadmap.svg" width="16" height="16" style="vertical-align: middle;"> Roadmap </a> |
77
<a href="https://join.slack.com/t/dlslime/shared_invite/zt-3e9zvercw-a89KI_Ig8N1UTaol_q6MXg"><img src="docs/imgs/assets/slack.svg" width="16" height="16" style="vertical-align: middle;"> Slack </a> |
88
<a href="docs/imgs/assets/wechat_qrcode.jpg"><img src="docs/imgs/assets/wechat.svg" width="16" height="16" style="vertical-align: middle;"> WeChat Group </a> |

README_zh.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<img src="docs/imgs/assets/logo.png" alt="DLSlime logo" width="44%">
33
</div>
44
<p align="center">
5-
<a href="https://deeplink-org.github.io/DLSlime">文档</a> |
5+
<a href="https://deeplink-org.github.io/DLSlime"><img src="docs/imgs/assets/docs.png" width="16" height="16" style="vertical-align: middle;"> 文档 </a> |
66
<a href="docs/roadmap.md"><img src="docs/imgs/assets/roadmap.svg" width="16" height="16" style="vertical-align: middle;"> 路线图 </a> |
77
<a href="https://join.slack.com/t/dlslime/shared_invite/zt-3e9zvercw-a89KI_Ig8N1UTaol_q6MXg"><img src="docs/imgs/assets/slack.svg" width="16" height="16" style="vertical-align: middle;"> Slack </a> |
88
<a href="docs/imgs/assets/wechat_qrcode.jpg"><img src="docs/imgs/assets/wechat.svg" width="16" height="16" style="vertical-align: middle;"> 微信群 </a> |

bench/README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ so benchmark commands, hardware notes, and result files can evolve together.
1212
| `python/endpoint_io_bench.py` | Endpoint-level I/O benchmark |
1313
| `python/endpoint_sendrecv_bench.py` | Endpoint send/recv benchmark |
1414
| `python/cache_bench.py` | DLSlimeCache benchmark |
15-
| `python/run_rpc_bench.sh` | SlimeRPC vs Ray benchmark wrapper |
16-
| `python/rpc_bench_*.py` | SlimeRPC and Ray benchmark implementations |
15+
| `python/run_rpc_bench.sh` | SlimeRPC vs Ray (optionally + Pulsing) benchmark wrapper |
16+
| `python/rpc_bench_*.py` | SlimeRPC, Ray, and Pulsing benchmark implementations |
1717
| `results/` | CSV outputs and captured worker logs |
1818

1919
## Prerequisites
@@ -112,12 +112,21 @@ dlslime-cache stop
112112
## SlimeRPC vs Ray Benchmark
113113

114114
The RPC benchmark compares SlimeRPC round-trip latency and bandwidth with a Ray
115-
actor baseline.
115+
actor baseline. A Pulsing (`@pul.remote`) actor baseline is available as an
116+
opt-in third comparator.
116117

117118
```bash
118119
bash bench/python/run_rpc_bench.sh
119120
```
120121

122+
Include the Pulsing baseline (requires `pip install pulsing`):
123+
124+
```bash
125+
bash bench/python/run_rpc_bench.sh --with-pulsing
126+
# or
127+
WITH_PULSING=1 bash bench/python/run_rpc_bench.sh
128+
```
129+
121130
With explicit parameters:
122131

123132
```bash
@@ -132,6 +141,7 @@ The script writes:
132141
```text
133142
bench/results/slime_rpc.csv
134143
bench/results/ray_rpc.csv
144+
bench/results/pulsing_rpc.csv # only when --with-pulsing is passed
135145
```
136146

137147
See [../docs/benchmark-rpc.md](../docs/benchmark-rpc.md) for the full RPC

bench/python/rpc_bench_compare.py

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#!/usr/bin/env python3
2-
"""Print a side-by-side comparison of SlimeRPC vs Ray benchmark CSVs.
2+
"""Print a side-by-side comparison of SlimeRPC, Ray (and optionally Pulsing) benchmark CSVs.
33
44
Usage:
5-
python rpc_bench_compare.py [--slime results/slime_rpc.csv] [--ray results/ray_rpc.csv]
5+
python rpc_bench_compare.py \\
6+
[--slime results/slime_rpc.csv] \\
7+
[--ray results/ray_rpc.csv] \\
8+
[--pulsing results/pulsing_rpc.csv] # optional
69
"""
710

811
import argparse
@@ -22,10 +25,15 @@ def label(size: int) -> str:
2225
def main():
2326
default_dir = os.path.join(os.path.dirname(__file__), "..", "results")
2427
parser = argparse.ArgumentParser(
25-
description="Compare SlimeRPC vs Ray benchmark results"
28+
description="Compare SlimeRPC vs Ray (and optionally Pulsing) benchmark results"
2629
)
2730
parser.add_argument("--slime", default=os.path.join(default_dir, "slime_rpc.csv"))
2831
parser.add_argument("--ray", default=os.path.join(default_dir, "ray_rpc.csv"))
32+
parser.add_argument(
33+
"--pulsing",
34+
default=None,
35+
help="Optional Pulsing CSV; if omitted, Pulsing columns are skipped.",
36+
)
2937
args = parser.parse_args()
3038

3139
for path in (args.slime, args.ray):
@@ -37,25 +45,43 @@ def main():
3745
slime = load(args.slime)
3846
ray_r = load(args.ray)
3947

40-
common = sorted(set(slime) & set(ray_r))
48+
pulsing = None
49+
if args.pulsing:
50+
if not os.path.exists(args.pulsing):
51+
print(f"Missing Pulsing results file: {args.pulsing}")
52+
print("Run rpc_bench_pulsing.py first, or omit --pulsing.")
53+
return
54+
pulsing = load(args.pulsing)
55+
56+
common = set(slime) & set(ray_r)
57+
if pulsing is not None:
58+
common &= set(pulsing)
59+
common = sorted(common)
4160
if not common:
42-
print("No overlapping sizes between the two result files.")
61+
print("No overlapping sizes between the result files.")
4362
return
4463

4564
col = 12
46-
sep = "-" * (
47-
10 + 1 + col + 1 + col + 1 + col + 1 + col + 1 + col + 1 + col + 1 + 12
48-
)
49-
5065
print(
5166
"\n┌─ Avg Latency (µs) ─────────────────────────────────────────────────────────┐"
5267
)
53-
header = (
54-
f"{'Size':<10} | "
55-
f"{'Slime avg':>{col}} | {'Slime p99':>{col}} | {'Slime BW':>{col}} | "
56-
f"{'Ray avg':>{col}} | {'Ray p99':>{col}} | {'Ray BW':>{col}} | "
57-
f"{'Speedup':>10}"
58-
)
68+
69+
if pulsing is not None:
70+
header = (
71+
f"{'Size':<10} | "
72+
f"{'Slime avg':>{col}} | {'Slime p99':>{col}} | {'Slime BW':>{col}} | "
73+
f"{'Puls avg':>{col}} | {'Puls p99':>{col}} | {'Puls BW':>{col}} | "
74+
f"{'Ray avg':>{col}} | {'Ray p99':>{col}} | {'Ray BW':>{col}} | "
75+
f"{'S/Pul':>10} | {'S/Ray':>10}"
76+
)
77+
else:
78+
header = (
79+
f"{'Size':<10} | "
80+
f"{'Slime avg':>{col}} | {'Slime p99':>{col}} | {'Slime BW':>{col}} | "
81+
f"{'Ray avg':>{col}} | {'Ray p99':>{col}} | {'Ray BW':>{col}} | "
82+
f"{'S/Ray':>10}"
83+
)
84+
sep = "-" * len(header)
5985
print(header)
6086
print(sep)
6187

@@ -66,17 +92,36 @@ def main():
6692
ray_avg = float(ray_r[size]["avg_us"])
6793
ray_p99 = float(ray_r[size]["p99_us"])
6894
ray_bw = float(ray_r[size]["bw_gbps"])
69-
speedup = ray_avg / sl_avg # >1 means SlimeRPC is faster
95+
sp_ray = ray_avg / sl_avg # >1 means SlimeRPC is faster than Ray
96+
97+
if pulsing is not None:
98+
pu_avg = float(pulsing[size]["avg_us"])
99+
pu_p99 = float(pulsing[size]["p99_us"])
100+
pu_bw = float(pulsing[size]["bw_gbps"])
101+
sp_pul = pu_avg / sl_avg # >1 means SlimeRPC is faster than Pulsing
102+
print(
103+
f"{label(size):<10} | "
104+
f"{sl_avg:>{col}.1f} | {sl_p99:>{col}.1f} | {sl_bw:>{col}.3f} | "
105+
f"{pu_avg:>{col}.1f} | {pu_p99:>{col}.1f} | {pu_bw:>{col}.3f} | "
106+
f"{ray_avg:>{col}.1f} | {ray_p99:>{col}.1f} | {ray_bw:>{col}.3f} | "
107+
f"{'%.2fx' % sp_pul:>10} | {'%.2fx' % sp_ray:>10}"
108+
)
109+
else:
110+
print(
111+
f"{label(size):<10} | "
112+
f"{sl_avg:>{col}.1f} | {sl_p99:>{col}.1f} | {sl_bw:>{col}.3f} | "
113+
f"{ray_avg:>{col}.1f} | {ray_p99:>{col}.1f} | {ray_bw:>{col}.3f} | "
114+
f"{'%.2fx' % sp_ray:>10}"
115+
)
70116

117+
print(sep)
118+
if pulsing is not None:
71119
print(
72-
f"{label(size):<10} | "
73-
f"{sl_avg:>{col}.1f} | {sl_p99:>{col}.1f} | {sl_bw:>{col}.3f} | "
74-
f"{ray_avg:>{col}.1f} | {ray_p99:>{col}.1f} | {ray_bw:>{col}.3f} | "
75-
f"{'%.2fx' % speedup:>10}"
120+
"S/Pul = Pulsing avg latency / SlimeRPC avg latency (>1 means SlimeRPC wins)"
76121
)
77-
78-
print(sep)
79-
print("Speedup = Ray avg latency / SlimeRPC avg latency (>1 means SlimeRPC wins)")
122+
print(
123+
"S/Ray = Ray avg latency / SlimeRPC avg latency (>1 means SlimeRPC wins)"
124+
)
80125
print()
81126

82127

bench/python/rpc_bench_pulsing.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python3
2+
"""Pulsing RPC benchmark — same-machine CPU bytes echo.
3+
4+
Measures round-trip latency for bytes echo via a Pulsing actor across a range
5+
of payload sizes, using identical metrics as rpc_bench_slime_driver.py and
6+
rpc_bench_ray.py so the three CSVs can be compared directly.
7+
8+
Usage:
9+
python rpc_bench_pulsing.py [--out results/pulsing_rpc.csv]
10+
"""
11+
12+
import argparse
13+
import asyncio
14+
import csv
15+
import os
16+
import time
17+
18+
import pulsing as pul
19+
20+
21+
@pul.remote
22+
class EchoActor:
23+
def echo(self, data: bytes) -> bytes:
24+
return data
25+
26+
27+
SIZES = [
28+
1 * 1024,
29+
4 * 1024,
30+
16 * 1024,
31+
64 * 1024,
32+
256 * 1024,
33+
1 * 1024 * 1024,
34+
4 * 1024 * 1024,
35+
16 * 1024 * 1024,
36+
64 * 1024 * 1024,
37+
]
38+
39+
40+
async def _benchmark(actor, data: bytes, warmup: int, iterations: int) -> dict:
41+
for _ in range(warmup):
42+
await actor.echo(data)
43+
44+
latencies_us = []
45+
for _ in range(iterations):
46+
t0 = time.perf_counter()
47+
await actor.echo(data)
48+
latencies_us.append((time.perf_counter() - t0) * 1e6)
49+
50+
latencies_us.sort()
51+
n = len(latencies_us)
52+
avg = sum(latencies_us) / n
53+
return {
54+
"avg_us": avg,
55+
"p50_us": latencies_us[n // 2],
56+
"p99_us": latencies_us[int(n * 0.99)],
57+
"bw_gbps": (2 * len(data)) / 1e9 / (avg / 1e6),
58+
}
59+
60+
61+
def _label(size: int) -> str:
62+
return f"{size // 1024}KB" if size < 1024 * 1024 else f"{size // (1024 * 1024)}MB"
63+
64+
65+
async def _run(out_path: str):
66+
await pul.init()
67+
try:
68+
actor = await EchoActor.spawn()
69+
70+
header = (
71+
f"{'Size':<10} | {'Avg (µs)':<12} | {'P50 (µs)':<12} "
72+
f"| {'P99 (µs)':<12} | {'BW (GB/s)':<10}"
73+
)
74+
print(header)
75+
print("-" * len(header))
76+
77+
records = []
78+
for size in SIZES:
79+
data = bytes(size)
80+
iters = max(50, min(500, 50_000_000 // size))
81+
r = await _benchmark(actor, data, warmup=20, iterations=iters)
82+
records.append({"size": size, **r})
83+
print(
84+
f"{_label(size):<10} | {r['avg_us']:<12.1f} | {r['p50_us']:<12.1f} "
85+
f"| {r['p99_us']:<12.1f} | {r['bw_gbps']:<10.3f}"
86+
)
87+
88+
os.makedirs(os.path.dirname(os.path.abspath(out_path)), exist_ok=True)
89+
with open(out_path, "w", newline="") as f:
90+
writer = csv.DictWriter(
91+
f, fieldnames=["size", "avg_us", "p50_us", "p99_us", "bw_gbps"]
92+
)
93+
writer.writeheader()
94+
writer.writerows(records)
95+
print(f"\nResults saved → {out_path}")
96+
finally:
97+
await pul.shutdown()
98+
99+
100+
def main():
101+
parser = argparse.ArgumentParser(description="Pulsing RPC benchmark")
102+
parser.add_argument(
103+
"--out",
104+
default=os.path.join(
105+
os.path.dirname(__file__), "..", "results", "pulsing_rpc.csv"
106+
),
107+
)
108+
args = parser.parse_args()
109+
asyncio.run(_run(args.out))
110+
111+
112+
if __name__ == "__main__":
113+
main()

bench/python/rpc_bench_slime_driver.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,8 @@ def main():
118118
driver._rpc_buffer_size = buf_bytes
119119
driver._rpc_max_inflight = max(1, int(getattr(args, "max_inflight", 4)))
120120

121-
driver.set_desired_topology(["bench-worker"])
122121
print("Waiting for worker…")
123-
driver.wait_for_peers(["bench-worker"], timeout_sec=120)
122+
driver.connect_to("bench-worker", ib_port=1, qp_num=1).wait(timeout=120)
124123
print("Connected. Starting benchmark.\n")
125124

126125
w = make_proxy(driver, "bench-worker", EchoService)

bench/python/rpc_bench_slime_worker.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,8 @@ def main():
6767
# buf_mb * SLIME_RPC_MAX_INFLIGHT bytes of CPU memory needlessly.
6868
worker._rpc_max_inflight = max(1, int(getattr(args, "max_inflight", 4)))
6969

70-
worker.set_desired_topology(["bench-driver"])
7170
print("Worker ready, waiting for driver to connect…")
72-
worker.wait_for_peers(["bench-driver"], timeout_sec=120)
71+
worker.connect_to("bench-driver", ib_port=1, qp_num=1).wait(timeout=120)
7372
print("Connected. Serving (Ctrl-C to stop).")
7473

7574
try:

0 commit comments

Comments
 (0)