Skip to content

Commit b2e60ea

Browse files
authored
Merge pull request #219 from licy666/feat/mooncake-examples-guard
feat(mooncake): 迁移 example 看护到共用引擎
2 parents 2299462 + 75b79b4 commit b2e60ea

7 files changed

Lines changed: 692 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# mooncake examples guard - project thin trigger.
2+
#
3+
# Calls the common engine .github/workflows/examples-template.yml;
4+
# this file only declares what varies per project: schedule,
5+
# concurrency, upstream repo, dispatch input forwarding, and matrix
6+
# parallelism. The guard loop (release + workflow-files monitor /
7+
# manifest-check / run-example matrix / validate-results /
8+
# save-monitor-state) lives in the engine. See examples-guards/README.md
9+
# for the design.
10+
11+
name: mooncake-examples
12+
13+
concurrency:
14+
# Three groups: schedule runs serialize (a run cancelled mid-way
15+
# loses its outcome writeback, and the outcome drives the next
16+
# cycle's retry); each PR serializes per number so a new push
17+
# cancels the stale run instead of queueing another matrix on the
18+
# NPU runners; each manual run stands alone. Group names are
19+
# repository-wide, so the PR group carries the workflow name: a bare
20+
# pr-<number> would let two projects' runs on one PR cancel each
21+
# other.
22+
group: ${{ github.event_name == 'schedule' && 'mooncake-examples-schedule' || (github.event_name == 'pull_request' && format('mooncake-examples-pr-{0}', github.event.pull_request.number)) || format('manual-{0}', github.run_id) }}
23+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
24+
25+
on:
26+
# Daily at 06:00 Beijing time (UTC 22:00). accelerate owns 02:00
27+
# Beijing (UTC 18:00); tgi owns 03:00 Beijing (UTC 19:00). The source
28+
# repo's mooncake schedule is already commented, so this daily run
29+
# does not overlap a live cron there.
30+
schedule:
31+
- cron: '0 22 * * *'
32+
workflow_dispatch:
33+
inputs:
34+
target_ref:
35+
description: Branch, tag, or SHA to test (empty = latest upstream release; falls back to main on API failure)
36+
required: false
37+
default: ''
38+
type: string
39+
# PR runs need no merge: GitHub executes the workflow files from the
40+
# PR merge ref, so a newly added trigger and engine run on the PR
41+
# that adds them. Whether a fork PR waits for a maintainer's approval
42+
# first is the repository's fork-PR policy. `pull_request`, not
43+
# `pull_request_target`: the declared read permissions are all the
44+
# engine needs. paths lists only this project's files, as the
45+
# quick-start triggers do: a PR that touches only the shared engine,
46+
# scripts or schema starts no project's matrix.
47+
pull_request:
48+
branches: [main]
49+
types: [opened, synchronize, reopened]
50+
paths:
51+
- 'examples-guards/mooncake/**'
52+
- '.github/workflows/mooncake-examples.yml'
53+
54+
# Engine jobs query the GitHub Job API (validate-results); called
55+
# workflows run with the caller's GITHUB_TOKEN permissions.
56+
permissions:
57+
actions: read
58+
contents: read
59+
60+
jobs:
61+
mooncake-examples:
62+
uses: ./.github/workflows/examples-template.yml
63+
with:
64+
# Namespaces cache keys (examples-monitor-state-mooncake_*),
65+
# artifact names and the scheduling paths under examples-guards/mooncake.
66+
project: mooncake
67+
upstream_repo: kvcache-ai/Mooncake
68+
target_ref: ${{ inputs.target_ref }}
69+
# One supported entry, and it needs two 910B cards. Serial is the
70+
# whole matrix.
71+
max_parallel: 1
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
version: 1
2+
scan:
3+
root: mooncake-transfer-engine/example
4+
include_extensions: ['.cpp', '.py', '.cu']
5+
supported:
6+
- path: mooncake-transfer-engine/example/transfer_engine_ascend_direct_perf.cpp
7+
profile: ascend-direct
8+
runner: linux-aarch64-a2-2
9+
npu_devices: '0,1'
10+
image: swr.cn-south-1.myhuaweicloud.com/ascendhub/cann:9.1.0-910b-ubuntu22.04-py3.12
11+
exec: build/mooncake-transfer-engine/example/transfer_engine_ascend_direct_perf
12+
overlay_args:
13+
- --block_iteration=1
14+
- --batch_size=2
15+
- --block_size=16384
16+
timeout_minutes: 120
17+
unsupported:
18+
# v0.3.13.post1 example/CMakeLists.txt:41 builds this only inside
19+
# if(USE_ASCEND). transfer_engine_ascend_one_sided.cpp:31 includes
20+
# "hccl.h" and :40 defaults protocol to "hccl". This guard passes
21+
# -DUSE_ASCEND_DIRECT=ON, which does not define USE_ASCEND.
22+
- mooncake-transfer-engine/example/transfer_engine_ascend_one_sided.cpp
23+
# Same gate at example/CMakeLists.txt:46. transfer_engine_ascend_perf.cpp:31
24+
# includes "hccl.h" and :40 defaults protocol to "hccl".
25+
# -DUSE_ASCEND_DIRECT=ON does not build this target.
26+
- mooncake-transfer-engine/example/transfer_engine_ascend_perf.cpp
27+
# example/CMakeLists.txt:58 builds this only inside
28+
# if(USE_ASCEND_HETEROGENEOUS). The initiator at :194 calls
29+
# installTransport("ascend"), and :166 puts "cuda:0" in the NIC matrix
30+
# beside "npu:0". This runner has no CUDA peer.
31+
- mooncake-transfer-engine/example/transfer_engine_heterogeneous_ascend_perf_initiator.cpp
32+
# transfer_engine_bench.cpp:91 defaults protocol to "rdma". The help
33+
# string is rdma|barex|tcp|efa|nvlink|musa|nvlink_intra|hip|sunrise_link
34+
# and does not name ascend or hccl. :95 defaults device_name to
35+
# "mlx5_2". The NPU allocator at :136 is under USE_UBSHMEM.
36+
- mooncake-transfer-engine/example/transfer_engine_bench.cpp
37+
# transfer_engine_bench_with_notify.cpp:77 defaults protocol to "rdma"
38+
# (rdma|tcp|nvlink|musa|hip). Under USE_CUDA, :110 calls cudaSetDevice
39+
# and :114 calls cudaMalloc.
40+
- mooncake-transfer-engine/example/transfer_engine_bench_with_notify.cpp
41+
# transfer_engine_bench_with_retry.cpp:62 defaults protocol to "rdma"
42+
# (rdma|tcp). Under USE_CUDA, :95 calls cudaSetDevice and :96 calls
43+
# cudaMalloc.
44+
- mooncake-transfer-engine/example/transfer_engine_bench_with_retry.cpp
45+
# transfer_engine_validator.cpp:73 defaults protocol to "rdma"
46+
# (rdma|tcp|nvlink|musa|maca|hip). Under USE_CUDA, :112 calls
47+
# cudaSetDevice and :116 calls cudaMalloc.
48+
- mooncake-transfer-engine/example/transfer_engine_validator.cpp
49+
# memory_pool.cpp:81 calls installTransport("rdma"). :89 registers the
50+
# buffer with location "cpu:" plus the socket id. No Ascend call.
51+
- mooncake-transfer-engine/example/memory_pool.cpp
52+
# show_link.cpp:63 calls installTransport("rdma") and prints
53+
# engine->showLinks. No device buffer and no Ascend call.
54+
- mooncake-transfer-engine/example/show_link.cpp
55+
# device_transport_example.cu:27 says "Requires 2 GPUs with P2P access".
56+
# :31 includes cuda_runtime.h and :198 calls cudaSetDevice.
57+
# example/CMakeLists.txt:82 builds it only inside
58+
# if(USE_CUDA AND BUILD_DEVICE_TRANSPORT_EXAMPLE).
59+
- mooncake-transfer-engine/example/device_transport_example.cu
60+
# nccl_device_transport_example.cu:21 includes cuda_runtime.h, :36
61+
# includes nccl_device.cuh, and :227 calls cudaSetDevice.
62+
# example/CMakeLists.txt:129 builds it only inside if(USE_NCCL_DEVICE
63+
# AND BUILD_NCCL_DEVICE_TRANSPORT_EXAMPLE).
64+
- mooncake-transfer-engine/example/nccl_device_transport_example.cu
65+
# nccl_host_transport_example.cpp:17 includes nccl.h. :160 logs
66+
# "The NCCL host example requires two GPUs". :180 calls
67+
# installTransport("nccl") and :182 registers memory as "cuda:" plus
68+
# the rank. example/CMakeLists.txt:166 builds it only inside
69+
# if(USE_NCCL_HOST AND BUILD_NCCL_HOST_TRANSPORT_EXAMPLE).
70+
- mooncake-transfer-engine/example/nccl_host_transport_example.cpp
71+
# efa_first_submit_probe.cpp:47 includes efa_transport.h and :86 calls
72+
# installTransport("efa"). example/CMakeLists.txt:32 builds it only
73+
# inside if(USE_EFA).
74+
- mooncake-transfer-engine/example/efa_first_submit_probe.cpp
75+
# efa_per_transfer_latency_bench.py:83 starts the bench with
76+
# --protocol=efa. :58 builds an ssh argv, ssh_args = ["ssh", ...], and
77+
# :173 uses it to start the target on --target_host. Not an on-node
78+
# NPU workload.
79+
- mooncake-transfer-engine/example/efa_per_transfer_latency_bench.py
80+
# batch_register_bench.py:3 titles itself "Batch Memory Registration
81+
# Benchmark for Mooncake EFA Transport". :59 defaults --protocol to
82+
# "efa".
83+
- mooncake-transfer-engine/example/batch_register_bench.py
84+
# kvcache_prefix_bench.py:3 titles itself "KV Cache Prefix Transfer
85+
# Benchmark for Mooncake EFA Transport". :62 defaults --protocol to
86+
# "efa". :166 allocates with torch.zeros(..., device=f"cuda:{gpu_id}").
87+
- mooncake-transfer-engine/example/kvcache_prefix_bench.py
88+
# bootstrap_server.py:29 routes every method on /metadata into an
89+
# in-memory dict via aiohttp. No NPU or transfer call.
90+
- mooncake-transfer-engine/example/http-metadata-server-python/bootstrap_server.py
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#!/usr/bin/env python3
2+
"""Make /etc/hccn.conf a regular file of this container's device NIC IPs.
3+
4+
Ascend Direct reads address_<phyId>=<ip> from that path. The source
5+
workflow bind-mounted the host file. This engine does not pass container
6+
volumes, and the Hong Kong ARC runners ignore Docker --volume, so the
7+
mount is not here. Recover the IPs already visible in the job: flatten a
8+
directory-shaped mount, copy a driver copy, or query hccn_tool. Do not
9+
invent addresses from host NICs.
10+
11+
The engine then sets ASCEND_RT_VISIBLE_DEVICES from the manifest, which
12+
is 0,1. That list is not the physical id on every 2-card pod. Run
13+
38249171972 queried -i 0, got "invalid device id", wrote the one id that
14+
answered, and the initiator aborted because ADXL asked for phy id 3.
15+
Probe with that mask unset and write every IP the tool returns.
16+
Leave ASCEND_RT_VISIBLE_DEVICES alone: the example calls
17+
aclrtSetDevice with logic ids 0 and 1. Rewriting the mask to physical
18+
ids 1,2 made that call fail with "Failed to set device ACL" in run
19+
38375629967.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import os
25+
import re
26+
import shutil
27+
import subprocess
28+
29+
HCCN_CONF = '/etc/hccn.conf'
30+
DRIVER_HCCN_CONF = '/usr/local/Ascend/driver/tools/hccn.conf'
31+
CARDS_NEEDED = 2
32+
PROBE_LIMIT = 16
33+
34+
35+
def _phy_ids_from_npu_smi() -> list[int]:
36+
info = subprocess.run(
37+
['npu-smi', 'info'],
38+
capture_output=True,
39+
text=True,
40+
check=False,
41+
)
42+
found: list[int] = []
43+
for line in info.stdout.splitlines():
44+
match = re.search(r'\|\s+(\d+)\s+910', line)
45+
if match is None:
46+
continue
47+
phy = int(match.group(1))
48+
if phy not in found:
49+
found.append(phy)
50+
return found
51+
52+
53+
def _parse_hccn_ip(text: str) -> str | None:
54+
"""Parse `hccn_tool -i N -ip -g` output (`ipaddr:1.2.3.4`)."""
55+
for line in text.splitlines():
56+
if 'ipaddr' not in line.lower():
57+
continue
58+
_, _, value = line.partition(':')
59+
ip = value.strip()
60+
if ip:
61+
return ip
62+
return None
63+
64+
65+
def _flatten_dir_shaped_hccn() -> bool:
66+
"""A file mount that ARC created with mkdir -p lands as a directory."""
67+
nested = os.path.join(HCCN_CONF, 'hccn.conf')
68+
if not (os.path.isdir(HCCN_CONF) and os.path.isfile(nested)):
69+
return False
70+
with open(nested, 'rb') as handle:
71+
data = handle.read()
72+
shutil.rmtree(HCCN_CONF)
73+
with open(HCCN_CONF, 'wb') as handle:
74+
handle.write(data)
75+
print(f'setup: flattened directory-shaped {HCCN_CONF}', flush=True)
76+
return True
77+
78+
79+
def _copy_driver_conf() -> bool:
80+
if not os.path.isfile(DRIVER_HCCN_CONF):
81+
return False
82+
shutil.copyfile(DRIVER_HCCN_CONF, HCCN_CONF)
83+
print(f'setup: copied {DRIVER_HCCN_CONF} -> {HCCN_CONF}', flush=True)
84+
return True
85+
86+
87+
def _probe_env() -> dict[str, str]:
88+
env = os.environ.copy()
89+
env.pop('ASCEND_RT_VISIBLE_DEVICES', None)
90+
env.pop('ASCEND_VISIBLE_DEVICES', None)
91+
return env
92+
93+
94+
def _query_ip(tool: str, phy: int, env: dict[str, str]) -> str | None:
95+
query = subprocess.run(
96+
[tool, '-i', str(phy), '-ip', '-g'],
97+
capture_output=True,
98+
text=True,
99+
check=False,
100+
env=env,
101+
)
102+
return _parse_hccn_ip(query.stdout) or _parse_hccn_ip(query.stderr)
103+
104+
105+
def _write_probed(tool: str) -> None:
106+
env = _probe_env()
107+
smi_ids = _phy_ids_from_npu_smi()
108+
probe_ids: list[int] = []
109+
for phy in [*smi_ids, *range(PROBE_LIMIT)]:
110+
if phy not in probe_ids:
111+
probe_ids.append(phy)
112+
resolved: dict[int, str] = {}
113+
for phy in probe_ids:
114+
ip = _query_ip(tool, phy, env)
115+
if ip:
116+
resolved[phy] = ip
117+
if len(resolved) < CARDS_NEEDED:
118+
found = ','.join(str(phy) for phy in sorted(resolved)) or 'none'
119+
raise SystemExit(
120+
f'setup: hccn_tool resolved {len(resolved)} device IP(s) '
121+
f'[{found}], need {CARDS_NEEDED}')
122+
with open(HCCN_CONF, 'w', encoding='utf-8') as handle:
123+
for phy in sorted(resolved):
124+
handle.write(f'address_{phy}={resolved[phy]}\n')
125+
ids = ','.join(str(phy) for phy in sorted(resolved))
126+
print(
127+
f'setup: wrote {HCCN_CONF} from hccn_tool '
128+
f'({len(resolved)} address lines: {ids})',
129+
flush=True,
130+
)
131+
132+
133+
def ensure() -> None:
134+
if os.path.isfile(HCCN_CONF):
135+
print(f'setup: {HCCN_CONF} already present', flush=True)
136+
elif _flatten_dir_shaped_hccn():
137+
pass
138+
elif _copy_driver_conf():
139+
pass
140+
else:
141+
# setup_example.sh puts driver/tools on PATH before calling this.
142+
tool = shutil.which('hccn_tool')
143+
if tool is None:
144+
raise SystemExit(
145+
'setup: no hccn_tool; cannot materialize /etc/hccn.conf')
146+
_write_probed(tool)
147+
148+
149+
if __name__ == '__main__':
150+
ensure()
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python3
2+
"""Print OVERLAY_ARGS as one argv token per line.
3+
4+
The manifest stores overlay_args as a JSON array of CLI strings. The
5+
engine forwards that array in OVERLAY_ARGS. Empty, null, and "" mean
6+
no extra args. Each string is split with POSIX shell rules so a quoted
7+
token stays one argv element.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import json
13+
import os
14+
import shlex
15+
16+
17+
def main() -> None:
18+
raw = os.environ.get('OVERLAY_ARGS', '').strip()
19+
if not raw or raw in ('null', '""'):
20+
return
21+
try:
22+
items = json.loads(raw)
23+
except json.JSONDecodeError as exc:
24+
raise SystemExit(f'OVERLAY_ARGS is not valid JSON: {exc}') from exc
25+
if not isinstance(items, list):
26+
raise SystemExit(
27+
f'OVERLAY_ARGS must be a JSON array, got {type(items).__name__}')
28+
for item in items:
29+
if not isinstance(item, str):
30+
raise SystemExit(
31+
'OVERLAY_ARGS items must be strings, '
32+
f'got {type(item).__name__}')
33+
for token in shlex.split(os.path.expandvars(item), posix=True):
34+
if '\n' in token or '\r' in token:
35+
raise SystemExit('OVERLAY_ARGS token contains a newline')
36+
print(token)
37+
38+
39+
if __name__ == '__main__':
40+
main()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env python3
2+
"""Print the last `listening on host:port` line from a target log.
3+
4+
Transfer Engine logs `Transfer Engine RPC using <protocol>, listening on
5+
<host>:<port>` once the target is ready. The initiator needs that
6+
endpoint as --segment_id. Exit 1 when the log does not have one yet.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import re
12+
import sys
13+
from pathlib import Path
14+
15+
16+
def main() -> None:
17+
if len(sys.argv) != 2:
18+
raise SystemExit(f'usage: {sys.argv[0]} <log>')
19+
text = Path(sys.argv[1]).read_text(encoding='utf-8', errors='replace')
20+
matches = re.findall(r'listening on (\S+:\d+)', text)
21+
if not matches:
22+
raise SystemExit(1)
23+
print(matches[-1])
24+
25+
26+
if __name__ == '__main__':
27+
main()

0 commit comments

Comments
 (0)