Read NVLink and GPU-to-adapter distance from nvidia-smi topo

The release binary is built without cgo, so without NVML, and NVML was
the only source of NVLink peers: on every NVLink machine the portable
agent reported none, and tensor-parallel groups were chosen by GPU index.
Discovery now reads `nvidia-smi topo -m` after either path, for the
NVLink peers (kept from NVML where it gave them), each card's NUMA node,
and how far each network adapter is from it - PIX, PXB, PHB, NODE or
SYS - which travels to the control plane as nic_paths for placement to
use. Both the NICn-with-legend layout of newer drivers and the
adapter-named columns of older ones are read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-09-29 00:09:26 +02:00
1 parent 22bb098247
commit bf2d171e89
8 files changed
+363 -30

No files matched your search

+42 -26
View File
@@ -933,8 +933,11 @@ type GPU struct {
// Zero when the driver is too old to report it, which is not the same as
// idle: the metric is simply absent rather than wrong.
UtilizationPercent int32 `protobuf:"varint,15,opt,name=utilization_percent,json=utilizationPercent,proto3" json:"utilization_percent,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// nic_paths is how far each network adapter is from this card, by HCA
// name: PIX, PXB, PHB, NODE or SYS, from nvidia-smi topo.
NicPaths map[string]string `protobuf:"bytes,16,rep,name=nic_paths,json=nicPaths,proto3" json:"nic_paths,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GPU) Reset() {
@@ -1072,6 +1075,13 @@ func (x *GPU) GetUtilizationPercent() int32 {
return 0
}
func (x *GPU) GetNicPaths() map[string]string {
if x != nil {
return x.NicPaths
}
return nil
}
type FabricAttachment struct {
state protoimpl.MessageState `protogen:"open.v1"`
// fabric is the operator-assigned name. It is not derived from the GID
@@ -2604,7 +2614,7 @@ const file_llmgw_v1_agent_proto_rawDesc = "" +
"\vleader_port\x18\x0e \x01(\x05R\n" +
"leaderPort\x12\x1d\n" +
"\n" +
"profile_fp\x18\x0f \x01(\tR\tprofileFp\"\xf9\x03\n" +
"profile_fp\x18\x0f \x01(\tR\tprofileFp\"\xf0\x04\n" +
"\x03GPU\x12\x14\n" +
"\x05index\x18\x01 \x01(\x05R\x05index\x12\x12\n" +
"\x04uuid\x18\x02 \x01(\tR\x04uuid\x12\x12\n" +
@@ -2623,7 +2633,11 @@ const file_llmgw_v1_agent_proto_rawDesc = "" +
"\rtemperature_c\x18\r \x01(\x05R\ftemperatureC\x12\x1d\n" +
"\n" +
"ecc_errors\x18\x0e \x01(\x05R\teccErrors\x12/\n" +
"\x13utilization_percent\x18\x0f \x01(\x05R\x12utilizationPercent\"[\n" +
"\x13utilization_percent\x18\x0f \x01(\x05R\x12utilizationPercent\x128\n" +
"\tnic_paths\x18\x10 \x03(\v2\x1b.llmgw.v1.GPU.NicPathsEntryR\bnicPaths\x1a;\n" +
"\rNicPathsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"[\n" +
"\x10FabricAttachment\x12\x16\n" +
"\x06fabric\x18\x01 \x01(\tR\x06fabric\x12\x12\n" +
"\x04hcas\x18\x02 \x03(\tR\x04hcas\x12\x1b\n" +
@@ -2783,7 +2797,7 @@ func file_llmgw_v1_agent_proto_rawDescGZIP() []byte {
}
var file_llmgw_v1_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_llmgw_v1_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
var file_llmgw_v1_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
var file_llmgw_v1_agent_proto_goTypes = []any{
(ShardStatus_State)(0), // 0: llmgw.v1.ShardStatus.State
(*AgentMessage)(nil), // 1: llmgw.v1.AgentMessage
@@ -2813,8 +2827,9 @@ var file_llmgw_v1_agent_proto_goTypes = []any{
(*LogsReply)(nil), // 25: llmgw.v1.LogsReply
nil, // 26: llmgw.v1.Register.LabelsEntry
nil, // 27: llmgw.v1.Register.EngineVersionsEntry
nil, // 28: llmgw.v1.Heartbeat.EngineVersionsEntry
nil, // 29: llmgw.v1.EngineProfile.EnvEntry
nil, // 28: llmgw.v1.GPU.NicPathsEntry
nil, // 29: llmgw.v1.Heartbeat.EngineVersionsEntry
nil, // 30: llmgw.v1.EngineProfile.EnvEntry
}
var file_llmgw_v1_agent_proto_depIdxs = []int32{
3, // 0: llmgw.v1.AgentMessage.register:type_name -> llmgw.v1.Register
@@ -2837,24 +2852,25 @@ var file_llmgw_v1_agent_proto_depIdxs = []int32{
5, // 17: llmgw.v1.Register.storage:type_name -> llmgw.v1.Storage
4, // 18: llmgw.v1.Register.host:type_name -> llmgw.v1.Host
0, // 19: llmgw.v1.RunningShard.state:type_name -> llmgw.v1.ShardStatus.State
11, // 20: llmgw.v1.NodeLoad.shards:type_name -> llmgw.v1.ShardLoad
5, // 21: llmgw.v1.Heartbeat.storage:type_name -> llmgw.v1.Storage
9, // 22: llmgw.v1.Heartbeat.weights:type_name -> llmgw.v1.LocalWeights
28, // 23: llmgw.v1.Heartbeat.engine_versions:type_name -> llmgw.v1.Heartbeat.EngineVersionsEntry
15, // 24: llmgw.v1.Heartbeat.provision:type_name -> llmgw.v1.ProvisionStatus
10, // 25: llmgw.v1.Heartbeat.load:type_name -> llmgw.v1.NodeLoad
14, // 26: llmgw.v1.Provision.engines:type_name -> llmgw.v1.EngineWant
0, // 27: llmgw.v1.ShardStatus.state:type_name -> llmgw.v1.ShardStatus.State
17, // 28: llmgw.v1.ShardStatus.fetch:type_name -> llmgw.v1.FetchProgress
19, // 29: llmgw.v1.StartShard.profile:type_name -> llmgw.v1.EngineProfile
29, // 30: llmgw.v1.EngineProfile.env:type_name -> llmgw.v1.EngineProfile.EnvEntry
1, // 31: llmgw.v1.AgentService.Connect:input_type -> llmgw.v1.AgentMessage
2, // 32: llmgw.v1.AgentService.Connect:output_type -> llmgw.v1.ControlMessage
32, // [32:33] is the sub-list for method output_type
31, // [31:32] is the sub-list for method input_type
31, // [31:31] is the sub-list for extension type_name
31, // [31:31] is the sub-list for extension extendee
0, // [0:31] is the sub-list for field type_name
28, // 20: llmgw.v1.GPU.nic_paths:type_name -> llmgw.v1.GPU.NicPathsEntry
11, // 21: llmgw.v1.NodeLoad.shards:type_name -> llmgw.v1.ShardLoad
5, // 22: llmgw.v1.Heartbeat.storage:type_name -> llmgw.v1.Storage
9, // 23: llmgw.v1.Heartbeat.weights:type_name -> llmgw.v1.LocalWeights
29, // 24: llmgw.v1.Heartbeat.engine_versions:type_name -> llmgw.v1.Heartbeat.EngineVersionsEntry
15, // 25: llmgw.v1.Heartbeat.provision:type_name -> llmgw.v1.ProvisionStatus
10, // 26: llmgw.v1.Heartbeat.load:type_name -> llmgw.v1.NodeLoad
14, // 27: llmgw.v1.Provision.engines:type_name -> llmgw.v1.EngineWant
0, // 28: llmgw.v1.ShardStatus.state:type_name -> llmgw.v1.ShardStatus.State
17, // 29: llmgw.v1.ShardStatus.fetch:type_name -> llmgw.v1.FetchProgress
19, // 30: llmgw.v1.StartShard.profile:type_name -> llmgw.v1.EngineProfile
30, // 31: llmgw.v1.EngineProfile.env:type_name -> llmgw.v1.EngineProfile.EnvEntry
1, // 32: llmgw.v1.AgentService.Connect:input_type -> llmgw.v1.AgentMessage
2, // 33: llmgw.v1.AgentService.Connect:output_type -> llmgw.v1.ControlMessage
33, // [33:34] is the sub-list for method output_type
32, // [32:33] is the sub-list for method input_type
32, // [32:32] is the sub-list for extension type_name
32, // [32:32] is the sub-list for extension extendee
0, // [0:32] is the sub-list for field type_name
}
func init() { file_llmgw_v1_agent_proto_init() }
@@ -2883,7 +2899,7 @@ func file_llmgw_v1_agent_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_llmgw_v1_agent_proto_rawDesc), len(file_llmgw_v1_agent_proto_rawDesc)),
NumEnums: 1,
NumMessages: 29,
NumMessages: 30,
NumExtensions: 0,
NumServices: 1,
},
+3
View File
@@ -207,6 +207,9 @@ message GPU {
// Zero when the driver is too old to report it, which is not the same as
// idle: the metric is simply absent rather than wrong.
int32 utilization_percent = 15;
// nic_paths is how far each network adapter is from this card, by HCA
// name: PIX, PXB, PHB, NODE or SYS, from nvidia-smi topo.
map<string, string> nic_paths = 16;
}
message FabricAttachment {