Pull Request已成功合入, 合并人@wangyang
(感谢 灏悦心陈 的贡献)变更摘要
本 PR 为 MindIE Motor 引入「独立部署(Standalone)Coordinator」能力:Coordinator 可在不依赖 Controller 控制面的场景下独立运行,通过新增一套仅含路由信息的「外部部署器(External Deployer)」实例事件协议(ExternalInsEventMsg),将最小拓扑信息转换为内部 InsEventMsg 后复用既有纳管与调度流程;同时补充 KV conductor 未配置时的跳过逻辑、Controller 配置惰性加载与告警上报跳过、原生引擎模型名解析,以及配套单元测试和端到端冒烟测试。
主要改动
-
新增外部部署器事件协议(
motor/common/resources/http_msg_spec.py):新增ExternalEndpoint、ExternalInstance、ExternalInsEventMsg,仅携带id/role/endpoints(host:port)等路由信息;to_internal()将其转换为内部InsEventMsg(含job_name、parallel_config、端点按 IP 归组等),并校验事件类型(仅支持 set/add/del)、engine_type(仅 vllm)、实例/端点去重及端口范围。 -
管理面接入独立事件解析(
motor/coordinator/api_server/management_server.py):新增_is_standalone_instance_refresh()区分外部部署器与 Controller 两种负载形状,_parse_instance_event()据此选择InsEventMsg或ExternalInsEventMsg解析;省略model_name时由_resolve_external_model_name()通过NativeEngineApiClient.query_model_ids()查询原生引擎/v1/models(经asyncio.to_thread卸载阻塞 I/O)。 -
Controller 解耦与告警跳过(
motor/coordinator/api_client/controller_api_client.py、motor/config/config_utils.py):ControllerConfig/CoordinatorConfig改为惰性加载;新增has_motor_controller_config()判断用户配置是否包含motor_controller_config,report_alarms()在独立部署模式下跳过告警上报且不再加载 Controller 配置。 -
KV conductor 空配置跳过(
motor/coordinator/api_client/conductor_api_client.py):register_kv_instance()与unregister_kv_instance()在conductor_service为空时跳过 KV conductor 实例注册/注销流程。 -
测试覆盖(
tests/):新增tests/e2e/test_standalone_coordinator_smoke.py端到端冒烟测试(环境变量开启,覆盖 SET/ADD/DEL 成员事件、推理连通与重启回放),并补充外部拓扑协议校验、独立 Coordinator 配置加载、report_alarms跳过及 KV 注册相关单元测试。


1. 合入背景
Fixes #546
2. 修改内容
本 PR 为 MindIE Motor 引入"独立部署(Standalone)Coordinator"能力:通过新增 controller_integration_enabled 配置项,使 Coordinator 可在不依赖 Controller 控制面的场景下独立运行,并新增一套仅含路由信息的"外部部署器(External Deployer)"实例事件协议,将最小拓扑信息转换为内部 InsEventMsg 后复用既有纳管与调度流程。同时补充了 KV conductor 在未配置时的跳过逻辑、裸机部署的配置与启停脚本示例,以及配套的单元测试和端到端冒烟测试。
3. 资料变更
涉及
MindIE Motor 引入"独立部署(Standalone)Coordinator"能力
4. 接口变更
不涉及
5. 测试结果
用例 C1 文档 SET 1P1D + 模型名自动发现 + readiness
结果 通过
时间 08:47:00
步骤:POST /instances/refresh body=topology_set_1p1d.json(无 model_name)
HTTP 响应 resp_set_1p1d.json
{"status":"success","message":"Instance refresh completed",
"data":{"event_type":"set","instance_count":2,
"timestamp":"2026-09-09T00:47:00.522166+00:00"}}
GET /readiness readiness_after_set_1p1d.json
{"status":"ok","message":"Coordinator is ok","ready":true}
GET /instances instances_after_set_1p1d.json
count=2
id=1 prefill model_name=qwen3-8b job_name=external-prefill-1 endpoint id=0 port=8100
id=3 decode model_name=qwen3-8b job_name=external-decode-3 endpoint id=0 port=8200
Coordinator 日志(node/node-37-110.log / coordinator.log)
08:47:00 Refresh instances: event_type=EventType.SET, count=2, instance_ids=[(1, 'prefill'), (3, 'decode')]
08:47:00 Added instance ID 1 (role: prefill, job_name: external-prefill-1) with 1 endpoints to available pool successfully
08:47:00 Added instance ID 3 (role: decode, job_name: external-decode-3) with 1 endpoints to available pool successfully
08:47:00 HBM DP registered (ZMQ+HTTP): instance=vllm-prefill-1 dp=0 replay=tcp://127.0.0.1:6677
08:47:00 Refresh instances done: E=0, P=1, D=1, U=0
判定:省略 model_name 仍按精简协议解析;模型从引擎发现为 qwen3-8b;P/D 齐后 ready=true。
用例 C2 ADD P2 不启动进程 / DEL P2 不停止进程
结果 通过
时间 08:47:06
ADD 响应 resp_add_p2.json
{"status":"success","data":{"event_type":"add","instance_count":1,
"timestamp":"2026-09-09T00:47:06.115084+00:00"}}
ADD 后实例表 instances_after_add_p2.json count=3
新增 id=2 prefill job_name=external-prefill-2 endpoint id=0 port=8110
DEL 响应 resp_del_p2.json
{"status":"success","data":{"event_type":"del","instance_count":1,
"timestamp":"2026-09-09T00:47:06.318868+00:00"}}
DEL 后实例表 instances_after_del_p2.json count=2(仅 P1+D)
进程核验(当时现场)
p2_pid_before=39516
p2_pid_after_add=39516 same=1
p2_pid_after_del=39516 health=200 still_alive=1
日志
08:47:06 Refresh instances: event_type=EventType.ADD, count=1, instance_ids=[(2, 'prefill')]
08:47:06 Added instance ID 2 ... Refresh instances done: E=0, P=2, D=1, U=0
08:47:06 Refresh instances: event_type=EventType.DEL, count=1, instance_ids=[(2, 'prefill')]
08:47:06 Refresh instances done: E=0, P=1, D=1, U=0
判定:add/del 只改 Coordinator 纳管表,不启停 P2 进程。
用例 C3 Coordinator 重启后必须重放 SET,重放后推理恢复
结果 通过
时间 08:55:58 ~ 08:56:16
重启后、重放前 readiness_before_replay.json
{"status":"ok","message":"Coordinator is ok","ready":false}
重放 SET resp_replay_set.json
{"status":"success","data":{"event_type":"set","instance_count":3,
"timestamp":"2026-09-09T00:56:06.200305+00:00"}}
重放后 readiness_after_replay.json
{"status":"ok","message":"Coordinator is ok","ready":true}
推理 infer_after_replay.json
id=cmpl-178891537681151000006c969451
text=" I'm a new user, so I'm not sure what to do. I"
usage prompt_tokens=6 total_tokens=22 completion_tokens=16
coordinator_restart.log
08:56:04 Uvicorn running on http://0.0.0.0:19001
08:56:06 Refresh instances: event_type=EventType.SET, count=3,
instance_ids=[(1, 'prefill'), (2, 'prefill'), (3, 'decode')]
08:56:06 Added instance ID 1/2/3 ... to available pool successfully
08:56:06 HBM DP registered: vllm-prefill-1 dp=0 ; vllm-prefill-2 dp=0
08:56:06 Refresh instances done: E=0, P=2, D=1, U=0
08:56:06 AUDIT event_type=instance_refresh result=success method=POST path=/instances/refresh
08:56:06 "POST /instances/refresh HTTP/1.1" 200 OK
08:56:06 [Readiness] Coordinator is ready. result=ok_standby instances_status=required_met
08:56:16 Worker 0: metaserver port enabled: 19100
08:56:16 scheduled role=decode instance=3 endpoint=0
08:56:16 scheduled role=prefill instance=1 endpoint=0
req_id=178891537681151000006c969451 policy=kv_cache_affinity matched=0 proposed=1-0
HTTPClientPool address=127.0.0.1:8100 以及 127.0.0.1:8200
判定:重启丢内存拓扑,ready=false;外部重放完整 set 后 ready=true,Layerwise 推理恢复(含 metaserver 19100)。
用例 C4 Layerwise 1P1D 推理(concurrent_engine_sync)
结果 通过
时间 08:53:22
SET 响应 resp_set_1p1d_concurrent.json
{"status":"success","data":{"event_type":"set","instance_count":2}}
推理 POST :19000/v1/completions infer_1p1d_concurrent.json
id=cmpl-17889152027183390000b61d2582
model=qwen3-8b
text=" I'm a new user, so I'm not sure what to do. I"
usage prompt_tokens=6 total_tokens=22 completion_tokens=16
P1 /health 仍为 200
日志
08:53:22 Refresh instances done: E=0, P=1, D=1, U=0
08:53:22 CircuitBreaker cleared all: count=1
08:53:22 scheduled role=prefill req_id=17889152027183390000b61d2582
instance=1 endpoint=0 policy=kv_cache_affinity matched=0 proposed=1-0
判定:精简 SET + concurrent 后,Coordinator 作为推理入口打通 1P1D。
用例 C5 文档 SET 2P1D,endpoint.id 全部为 0
结果 通过
时间 08:53:42
SET 响应 resp_set_2p1d_concurrent.json instance_count=3
GET /instances instances_2p1d.json
id=1 prefill endpoint id=0 port=8100
id=2 prefill endpoint id=0 port=8110
id=3 decode endpoint id=0 port=8200
日志
08:53:42 Refresh instances: event_type=EventType.SET, count=3,
instance_ids=[(1, 'prefill'), (2, 'prefill'), (3, 'decode')]
08:53:42 Added instance ID 2 (role: prefill, job_name: external-prefill-2)
08:53:42 HBM DP registered: instance=vllm-prefill-2 dp=0 replay=tcp://127.0.0.1:6677
08:53:42 Refresh instances done: E=0, P=2, D=1, U=0
判定:与昨晚错误 JSON(P2 endpoint.id=1)不同,本次两边都是 id=0、dp=0。
用例 C6 P1 KV 亲和:热请求 matched=640,查找 key 为 endpoint.id=0
结果 通过
时间 08:53:54 ~ 08:53:56
请求 affinity_request.json 约 666 prompt tokens,block=128,期望命中 5 块=640。
冷请求 p1_warm.json
id=chatcmpl-17889152340817130000091faa74 usage prompt_tokens=666
热请求 p1_hot.json
id=chatcmpl-178891523688943700001fe22583 usage prompt_tokens=666 completion_tokens=8
日志
08:53:54 scheduled ... instance=1 endpoint=0 policy=load_balance matched=None
req_id=17889152340817130000091faa74 committed=666.0
08:53:56 scheduled ... instance=1 endpoint=0 policy=kv_cache_affinity matched=640
req_id=178891523688943700001fe22583 committed=26.0 proposed=1-0
判定:文档 id=0 与引擎 DP0、调度 DP["0"] 对齐。昨晚 id=1 → DP["1"] → matched=0 的问题本次未复现。
用例 C7 P2 独有前缀亲和:订 5578 后 matched=384,endpoint=0
结果 通过
时间 08:55:35 ~ 08:55:37
手工订 conductor(SET 协议带不了 per-instance ZMQ 口)
POST :19333/register resp_p2_register.json {"status":"ok"}
instance_id=vllm-prefill-2 dp_rank=0 npu=tcp://127.0.0.1:5578 replay=6678
独有前缀 p2_unique_hot.json
id=chatcmpl-17889153379236990000289e62a6
usage prompt_tokens=424 total_tokens=432
日志
08:55:35 scheduled instance=2 endpoint=0 policy=load_balance matched=None committed=424.0
req_id=178891533516689100006adc96b5
08:55:37 scheduled instance=2 endpoint=0 policy=kv_cache_affinity matched=384
req_id=17889153379236990000289e62a6 proposed=2-0
判定:查找 key 仍是 "0"(不是 "1"),P2 自己的事件能计分。
附:自动注册时 P1/P2 都订 replay=tcp://127.0.0.1:6677(5577+id,id 都是 0)。
P2 真实口是 5578。这是同机多 P 的 conductor 端口偏移限制,不是 SET id 写错。
用例 C8 liu意见2:省略 model_name 的精简 SET
结果 通过
时间 09:41:16
liu_omit_model.json
{"status":"success","data":{"event_type":"set","instance_count":2}} http=200
liu_instances.json
id=1 prefill model_name=qwen3-8b job_name=external-prefill-1 endpoint id=0:8100
id=3 decode model_name=qwen3-8b job_name=external-decode-3 endpoint id=0:8200
用例 C9 liu意见3:拒绝 decode_colocation,放行 concurrent_engine_sync
结果 通过
时间 09:41:17
liu_reject_colocation.out HTTP 400
External Deployer dispatch_capabilities must be one of:
prefill_handoff_decode, concurrent_engine_sync
input_value='decode_colocation'
liu_allow_concurrent.out HTTP 200
{"status":"success","data":{"event_type":"set","instance_count":2}}
用例 C10 liu意见1:无配置 / 无 Controller 段时不按有 Controller 处理
结果 通过
时间 09:42:27 ~ 09:43:08
运行时探测 has_motor_controller_config()(liu_c1/check_has_ctrl.py)
missing_file => False
unset USER_CONFIG_PATH => False
配置存在但无 motor_controller_config => False
配置含 motor_controller_config => True
无配置启动日志 liu_c1/missing_cfg.log
Configuration file does not exist, using default configuration
全程无 127.0.0.1:1026 / ControllerConfig / report_alarm
(本机 1025 已被占用,随后 Aborting,属端口冲突,不是告警误打)
独立端口 19120/19121 启动 liu_c1/standalone_only.log
无 127.0.0.1:1026、无 report_alarm
6. CheckList
[ ] 代码注释完备
[ ] 正确记录维测日志
[ ] 是否有UT用例
[ ] 若涉及多线程场景,考虑了并发场景,不存在死锁问题