From dff988f744f9728cd6e3046af8c25550194b11ce Mon Sep 17 00:00:00 2001 From: Alexander Maslennikov Date: Mon, 17 Aug 2026 10:19:56 +0200 Subject: [PATCH] fix: normalize generated group identities Use one vendor-free 30-character identity for source groups and their machine labels. Balance long machine and GPU prefixes, retain a compact deterministic hash, and preserve the existing GPU label value. Signed-off-by: Alexander Maslennikov --- README.md | 16 +- docs/advanced/generation.md | 2 +- docs/reference/configuration.md | 8 +- docs/user/discovery.md | 18 ++- docs/user/heterogeneous-clusters.md | 17 ++- docs/user/profiles.md | 4 +- pkg/config/config.go | 138 ++++++++++++------ pkg/config/config_test.go | 69 ++++++++- .../discovery/discover.go | 32 ++-- pkg/networkoperatorplugin/render_plan.go | 4 +- .../spectrumx_railpool_test.go | 4 +- pkg/networkoperatorplugin/templates.go | 6 +- pkg/networkoperatorplugin/templates_test.go | 39 ++--- skills/k8s-launch-kit-config/SKILL.md | 6 +- .../references/config-reference.md | 2 +- skills/k8s-launch-kit-discover/SKILL.md | 28 ++-- skills/k8s-launch-kit-generate/SKILL.md | 11 +- skills/k8s-network-engineer/SKILL.md | 2 +- .../references/config-schema.md | 2 +- 19 files changed, 270 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index 97308f0d..8303cb5f 100644 --- a/README.md +++ b/README.md @@ -1388,15 +1388,21 @@ During discovery, each node group's `machineType` and `gpuType` are populated fr Values are sanitized to match the GPU operator label format (spaces replaced with dashes). If either probe fails (e.g., `nvidia-smi` not installed, DMI not readable), the corresponding field is left empty and discovery continues without error. When both values are available, discovery derives the group `identifier` from -`-`. Generated identifiers are limited to 40 bytes; long -values keep a readable prefix plus an 8-character deterministic hash. This -keeps resource names and label values that append the identifier below their -Kubernetes size limits without making similar hardware identities collide. +`-`, lowercases it, and removes complete `NVIDIA` +segments. Common machine segments are shortened in the same pass: +`ThinkSystem` becomes `ts` and `PowerEdge` becomes `pe`. The +`nvidia.kubernetes-launch-kit.machine` node label uses that same identifier; +the separate `.gpu` label retains the discovered GPU value. +Generated identifiers are limited to 30 bytes. Long identities keep balanced +machine/GPU prefixes plus a 6-character deterministic hash, with unused prefix +space reassigned when one component is short. This keeps resource names and +label values that append the identifier below their Kubernetes size limits +without making similar hardware identities collide. Example of discovered hardware types in the config: ```yaml clusterConfig: -- identifier: group-0 +- identifier: ts-sr680a-v3-h100-nvl machineType: ThinkSystem-SR680a-V3 gpuType: NVIDIA-H100-NVL workerNodes: diff --git a/docs/advanced/generation.md b/docs/advanced/generation.md index 75ffee17..19dac704 100644 --- a/docs/advanced/generation.md +++ b/docs/advanced/generation.md @@ -98,7 +98,7 @@ l8k generate \ # An explicit source-group subset l8k generate \ --user-config ./cluster-config.yaml \ - --groups poweredge-xe9680-h200,thinksystem-sr680a-v3-h200 \ + --groups pe-xe9680-h200,ts-sr680a-v3-h200 \ --save-deployment-files ./deployment-stage ``` diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 16773259..1917b99f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -298,7 +298,7 @@ Each `clusterConfig` entry represents one source group: ```yaml clusterConfig: - - identifier: poweredge-xe9680-h200 + - identifier: pe-xe9680-h200 machineType: PowerEdge-XE9680 gpuType: NVIDIA-H200 capabilities: @@ -307,7 +307,7 @@ clusterConfig: rdma: true ib: false nodeSelector: - nvidia.kubernetes-launch-kit.machine: PowerEdge-XE9680-NVIDIA-H200 + nvidia.kubernetes-launch-kit.machine: pe-xe9680-h200 pfs: - deviceID: a2dc pciAddress: 0000:1a:00.0 @@ -321,14 +321,14 @@ Fresh discovery may merge compatible source groups during generation, but it kee | Field | Meaning | | --- | --- | -| `identifier` | Lowercase resource-name form of machine/GPU identity, bounded to 40 bytes with a deterministic hash suffix when needed, or `group-N` fallback. | +| `identifier` | Lowercase resource-name form of machine/GPU identity with complete `NVIDIA` segments removed and common machine segments shortened (`ThinkSystem` → `ts`, `PowerEdge` → `pe`), bounded to 30 bytes with balanced machine/GPU prefixes and a 6-character deterministic hash when needed, or `group-N` fallback. The Launch Kit machine node label uses the same value. | | `machineType` / `gpuType` | Discovered hardware identity. | | `linkType` | Per-group `Ethernet` or `InfiniBand` result when fabric probes agree. | | `presetApplied` | An exact topology preset was applied. | | `presetDeviation` | PF count, PCI address, or device ID drift from a matched preset. | | `capabilities.nodes` | Group-level SR-IOV, RDMA, and InfiniBand capability flags. | | `workerNodes` | Kubernetes node names in the source group. | -| `nodeSelector` | Deployment selector, normally the Launch Kit machine label. | +| `nodeSelector` | Deployment selector, normally the Launch Kit machine label whose value matches `identifier`. | | `storageModules` / `thirdPartyRDMAModules` | Optional site-supplied dependent module lists. | | `pfs` | Physical-function inventory. | diff --git a/docs/user/discovery.md b/docs/user/discovery.md index c98b9a9d..61356674 100644 --- a/docs/user/discovery.md +++ b/docs/user/discovery.md @@ -90,16 +90,18 @@ Discovery also writes up to two Launch Kit-owned labels: | Label | Purpose | | --- | --- | -| `nvidia.kubernetes-launch-kit.machine: -` | Selects one source hardware group. | +| `nvidia.kubernetes-launch-kit.machine: ` | Selects one source hardware group using the same value persisted in `clusterConfig[].identifier`. | | `nvidia.kubernetes-launch-kit.gpu: ` | Selects compatible source groups that share a GPU type. | -Label values preserve the discovered case and normalize spaces for Kubernetes. -The combined machine/GPU label and its lowercase group `identifier` are bounded -to 40 bytes; long values keep a readable prefix plus an 8-character -deterministic hash. GPU-only labels use the Kubernetes 63-byte limit and the -same shortening rule. If the machine or GPU type cannot be resolved, discovery -uses a fallback `group-N` identifier and writes only the labels it can -construct. +The machine label and group `identifier` share one lowercase, vendor-free value +bounded to 30 bytes. The same normalization shortens common machine segments: +`ThinkSystem` becomes `ts` and `PowerEdge` becomes `pe`. Long identities keep +balanced machine/GPU prefixes plus a 6-character deterministic hash; unused +prefix space moves to the longer component. GPU-only labels preserve the +discovered case and `NVIDIA` segment, using the Kubernetes 63-byte limit and the +existing 8-character shortening hash. If the machine or GPU type cannot be +resolved, discovery uses a fallback `group-N` identifier and writes only the +labels it can construct. See [Heterogeneous Clusters](heterogeneous-clusters.md) for group merging and targeted generation. diff --git a/docs/user/heterogeneous-clusters.md b/docs/user/heterogeneous-clusters.md index 11cd7aaf..cf8b3cfb 100644 --- a/docs/user/heterogeneous-clusters.md +++ b/docs/user/heterogeneous-clusters.md @@ -16,10 +16,15 @@ Each `clusterConfig` entry is a source group with its own: - East-west PF inventory, rail assignments, and capabilities. - Storage and third-party RDMA kernel modules. -Fresh discovery derives `identifier` from the machine/GPU identity and bounds -it to 40 bytes with a deterministic hash suffix when the natural value is -longer. Use the persisted identifier shown in `cluster-config.yaml` with -`--groups`. +Fresh discovery derives `identifier` from the machine/GPU identity, lowercases +it, and removes complete `NVIDIA` segments. For example, +`DGX-B200-NVIDIA-H200` becomes `dgx-b200-h200`; the Launch Kit machine node +label uses the same value. Common machine segments are shortened in the same +pass (`ThinkSystem` → `ts`, `PowerEdge` → `pe`). Identifiers are bounded to 30 +bytes. Long values use balanced machine/GPU prefixes plus a 6-character +deterministic hash, with unused prefix space reassigned to the longer component. +The separate GPU label still retains its discovered value such as `NVIDIA-H200`. +Use the persisted identifier shown in `cluster-config.yaml` with `--groups`. Inspect the available groups before filtering: @@ -67,7 +72,7 @@ Use `--groups` for an exact set of source identifiers. Identifier matching is ca ```bash l8k generate \ --user-config ./cluster-config.yaml \ - --groups poweredge-xe9680-h200,thinksystem-sr680a-v3-h200 \ + --groups pe-xe9680-h200,ts-sr680a-v3-h200 \ --save-deployment-files ./deployment ``` @@ -165,7 +170,7 @@ l8k generate \ # Stage two of the three source groups l8k generate \ --user-config ./cluster-config.yaml \ - --groups dgx-b200-nvidia-h200,thinksystem-sr680a-v3-nvidia-h200 \ + --groups dgx-b200-h200,ts-sr680a-v3-h200 \ --save-deployment-files ./deployment-stage1 ``` diff --git a/docs/user/profiles.md b/docs/user/profiles.md index fdc932c2..62f7c380 100644 --- a/docs/user/profiles.md +++ b/docs/user/profiles.md @@ -82,13 +82,13 @@ Discovery writes l8k-owned node labels: | Label | Meaning | | --- | --- | -| `nvidia.kubernetes-launch-kit.machine` | One source group, value `-` | +| `nvidia.kubernetes-launch-kit.machine` | One source group, value matches the generated `clusterConfig[].identifier` | | `nvidia.kubernetes-launch-kit.gpu` | All source groups sharing the same GPU type | Use `--groups` when named source groups require different outputs: ```bash -l8k generate --groups poweredge-xe9680-h200,thinksystem-sr680a-v3-h200 +l8k generate --groups pe-xe9680-h200,ts-sr680a-v3-h200 ``` Use `--gpu-type` when all groups with the same GPU type can share a generated bundle: diff --git a/pkg/config/config.go b/pkg/config/config.go index 73c0606e..a09f5543 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -81,12 +81,10 @@ func DefaultLaunchKitConfig() (*LaunchKitConfig, error) { return &cfg, nil } -// MachineLabelKey is the node label `l8k discover` writes onto every -// node whose group has both machineType and gpuType resolved. The value -// is the literal `-` (e.g. `DGX-B200-NVIDIA-H100-NVL`) -// — upstream discovery already trims whitespace and converts spaces to -// hyphens to match GPU operator label format. Per-source-group -// `NodeSelector` keys on this label. +// MachineLabelKey is the node label `l8k discover` writes onto every node +// whose group has both machineType and gpuType resolved. Its value is the +// group's generated identifier, so per-source-group NodeSelectors and +// generated names use the same stable identity. const MachineLabelKey = "nvidia.kubernetes-launch-kit.machine" // GPULabelKey is the node label `l8k discover` writes onto every node @@ -101,7 +99,13 @@ const ( MaxLabelValueLength = 63 // MaxGeneratedIdentifierLength leaves room for prefixes added to generated // resource names and label values while retaining a collision-resistant hash. - MaxGeneratedIdentifierLength = 40 + MaxGeneratedIdentifierLength = 30 + // generatedIdentifierHashLength is the number of FNV-32a hex digits kept + // in shortened generated identities. The leading dash is additional. + generatedIdentifierHashLength = 6 + // labelValueHashLength retains the existing hash width for GPU label values, + // which continue to use Kubernetes' full label-value budget. + labelValueHashLength = 8 ) const ( @@ -116,23 +120,52 @@ const ( SpectrumXIPVersionIPv6 = "ipv6" ) -// MachineLabelValue returns the per-source-group machine label value: -// `-` literal when it fits the 40-byte generated-group -// identity limit, or a deterministic shortened form for long names -// (truncated prefix + 8-hex FNV-32a suffix). Returns the empty string -// only when either input is empty. -// -// The shortened form looks like -// `HPE-ProLiant-Compute-DL380-Gen1-0885f134` -// and is reproducible: identical inputs always produce the same -// value, so the label discover writes onto nodes always matches what -// `MachineLabelValue` returns at filter time. -func MachineLabelValue(machineType, gpuType string) string { +// GeneratedGroupIdentifier returns the shared identifier used by a source +// group and its MachineLabelKey node label. Normalization removes complete +// "nvidia" segments and shortens common machine segments ("thinksystem" to +// "ts", "poweredge" to "pe"). Long identities retain balanced prefixes from +// machineType and gpuType plus a deterministic 6-hex FNV-32a hash. If one +// component is shorter than half of the readable budget, the other component +// receives the unused space. +func GeneratedGroupIdentifier(machineType, gpuType string) string { if machineType == "" || gpuType == "" { return "" } - raw := machineType + "-" + gpuType - return truncateWithHash(raw, MaxGeneratedIdentifierLength) + + machine := normalizeIdentifier(machineType) + gpu := normalizeIdentifier(gpuType) + if machine == "" { + return truncateIdentifierWithHash(gpu) + } + if gpu == "" { + return truncateIdentifierWithHash(machine) + } + + raw := machine + "-" + gpu + if len(raw) <= MaxGeneratedIdentifierLength { + return raw + } + + suffix := hashSuffix(raw, generatedIdentifierHashLength) + readableBudget := MaxGeneratedIdentifierLength - len(suffix) - 1 + machineBudget := readableBudget / 2 + gpuBudget := readableBudget - machineBudget + if len(machine) < machineBudget { + machineBudget = len(machine) + gpuBudget = readableBudget - machineBudget + } else if len(gpu) < gpuBudget { + gpuBudget = len(gpu) + machineBudget = readableBudget - gpuBudget + } + + return machine[:machineBudget] + "-" + gpu[:gpuBudget] + suffix +} + +// MachineLabelValue returns the generated source-group identifier used as the +// MachineLabelKey node label. It is kept as a named helper for callers that +// construct selector values outside discovery. +func MachineLabelValue(machineType, gpuType string) string { + return GeneratedGroupIdentifier(machineType, gpuType) } // GPULabelValue returns the gpu label value, applying the same @@ -142,36 +175,60 @@ func GPULabelValue(gpuType string) string { if gpuType == "" { return "" } - return truncateWithHash(gpuType, MaxLabelValueLength) + return truncateWithHash(gpuType, MaxLabelValueLength, labelValueHashLength) } // truncateWithHash returns s unchanged when it fits maxLen bytes. Longer -// values retain a readable prefix and an 8-hex FNV-32a hash of the full input. -func truncateWithHash(s string, maxLen int) string { +// values retain a readable prefix and the requested number of FNV-32a hash +// digits from the full input. +func truncateWithHash(s string, maxLen, hashLength int) string { if len(s) <= maxLen { return s } - h := fnv.New32a() - _, _ = h.Write([]byte(s)) - suffix := fmt.Sprintf("-%08x", h.Sum32()) + suffix := hashSuffix(s, hashLength) prefixBudget := maxLen - len(suffix) prefix := strings.TrimRight(s[:prefixBudget], "-_.") return prefix + suffix } +func hashSuffix(s string, hashLength int) string { + h := fnv.New32a() + _, _ = h.Write([]byte(s)) + fullHash := fmt.Sprintf("%08x", h.Sum32()) + return "-" + fullHash[:hashLength] +} + // SanitizeIdentifier converts a product-type or label-value string into a -// valid K8s name component: lowercases the input and replaces spaces with -// hyphens, then deterministically bounds the result to 40 bytes. The shorter -// limit leaves room for prefixes wherever the identifier is appended to a -// generated resource name or label value. Used by discovery to derive -// `ClusterConfig.Identifier` from the machine label, and by the renderer when -// an identifier needs to land in a resource name. Both call sites must agree -// on the rule so a config produced by discovery renders the same names -// downstream — single function here guarantees that. +// valid K8s name component: lowercases the input, replaces spaces with +// hyphens, removes complete "nvidia" segments, and applies common segment +// shortenings before deterministically bounding the result to 30 bytes with a +// 6-hex hash when needed. Used for single-component generated identities such +// as auto-merged GPU groups. func SanitizeIdentifier(s string) string { + return truncateIdentifierWithHash(normalizeIdentifier(s)) +} + +func normalizeIdentifier(s string) string { s = strings.ToLower(s) s = strings.ReplaceAll(s, " ", "-") - return truncateWithHash(s, MaxGeneratedIdentifierLength) + segments := strings.Split(s, "-") + filtered := segments[:0] + for _, segment := range segments { + switch segment { + case "nvidia": + continue + case "thinksystem": + segment = "ts" + case "poweredge": + segment = "pe" + } + filtered = append(filtered, segment) + } + return strings.Join(filtered, "-") +} + +func truncateIdentifierWithHash(s string) string { + return truncateWithHash(s, MaxGeneratedIdentifierLength, generatedIdentifierHashLength) } // LaunchKitConfig represents the l8k-config.yaml structure @@ -687,11 +744,10 @@ type ClusterConfig struct { // same kubelet resource. In Mode A this equals Identifier; in Mode B's // per-source render units it differs. MergedIdentifier string `yaml:"-"` - // SourceMachineLabels lists the machine-label values - // (`-`) of every source group represented by the - // merged bucket. Populated only when this is a merged render group and - // the filtered set is a strict subset of its (gpuType, railCount) - // bucket — used by Scope-Aggregate templates to emit a + // SourceMachineLabels lists the generated identifier/machine-label values + // of every source group represented by the merged bucket. Populated only + // when this is a merged render group and the filtered set is a strict subset + // of its (gpuType, railCount) bucket; Scope-Aggregate templates use it to emit a // `matchExpressions In: [...]` selector. Empty in Mode A and for // per-source render units. SourceMachineLabels []string `yaml:"-"` diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 36a47334..91c0799e 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -33,24 +33,79 @@ func TestGeneratedGroupIdentityIsBounded(t *testing.T) { gpuType = "NVIDIA-AX800-Converged-Accelerator" ) - machineLabel := MachineLabelValue(machineType, gpuType) - assert.Equal(t, "HPE-ProLiant-Compute-DL380-Gen1-0885f134", machineLabel) - assert.LessOrEqual(t, len(machineLabel), MaxGeneratedIdentifierLength) - - identifier := SanitizeIdentifier(machineLabel) - assert.Equal(t, "hpe-proliant-compute-dl380-gen1-0885f134", identifier) + identifier := GeneratedGroupIdentifier(machineType, gpuType) + assert.Equal(t, "hpe-prolian-ax800-conve-17fb89", identifier) assert.LessOrEqual(t, len(identifier), MaxGeneratedIdentifierLength) + assert.Equal(t, identifier, MachineLabelValue(machineType, gpuType), + "the machine node label must match the generated identifier") - otherIdentifier := SanitizeIdentifier(MachineLabelValue(machineType, gpuType+"-Variant")) + otherIdentifier := GeneratedGroupIdentifier(machineType, gpuType+"-Variant") assert.NotEqual(t, identifier, otherIdentifier, "long identities with a shared prefix must retain distinct hash suffixes") } +func TestGeneratedGroupIdentifierUsesBalancedComponents(t *testing.T) { + identifier := GeneratedGroupIdentifier( + "ThinkSystem-SR675-V3", + "NVIDIA-RTX-PRO-6000-Blackwell-Server-Edition", + ) + + assert.Equal(t, "ts-sr675-v3-rtx-pro-600-46c7a9", identifier) + assert.Len(t, identifier, MaxGeneratedIdentifierLength) + assert.NotContains(t, identifier, "nvidia") + assert.Equal(t, identifier, MachineLabelValue( + "ThinkSystem-SR675-V3", + "NVIDIA-RTX-PRO-6000-Blackwell-Server-Edition", + )) +} + +func TestGeneratedGroupIdentifierReallocatesUnusedComponentBudget(t *testing.T) { + t.Run("short machine type", func(t *testing.T) { + identifier := GeneratedGroupIdentifier( + "DGX", + "NVIDIA-RTX-PRO-6000-Blackwell-Server-Edition", + ) + + assert.Equal(t, "dgx-rtx-pro-6000-blackw-19773c", identifier) + assert.Len(t, identifier, MaxGeneratedIdentifierLength) + }) + + t.Run("short GPU type", func(t *testing.T) { + identifier := GeneratedGroupIdentifier("VeryLongMachineType-Generation", "H200") + + assert.Equal(t, "verylongmachinetyp-h200-8861d3", identifier) + assert.Len(t, identifier, MaxGeneratedIdentifierLength) + }) +} + +func TestGeneratedGroupIdentityDropsNVIDIASegment(t *testing.T) { + identifier := GeneratedGroupIdentifier("DGX-B200", "NVIDIA-H200") + + assert.Equal(t, "dgx-b200-h200", identifier) + assert.Equal(t, identifier, MachineLabelValue("DGX-B200", "NVIDIA-H200")) + assert.Equal(t, identifier, GeneratedGroupIdentifier("DGX-B200", "H200"), + "a complete NVIDIA segment is intentionally non-identifying") + assert.Equal(t, "NVIDIA-H200", GPULabelValue("NVIDIA-H200"), + "the GPU node label must retain its existing value") +} + +func TestGeneratedGroupIdentityUsesCommonShortenings(t *testing.T) { + assert.Equal(t, "ts-sr680a-v3-h100-nvl", + GeneratedGroupIdentifier("ThinkSystem-SR680a-V3", "NVIDIA-H100-NVL")) + assert.Equal(t, "pe-xe9680-h200", + GeneratedGroupIdentifier("PowerEdge-XE9680", "NVIDIA-H200")) + assert.Equal(t, "nvidian-tsx-h200", + GeneratedGroupIdentifier("NVIDIAN-TSX", "NVIDIA-H200"), + "only complete known segments should be shortened") +} + func TestSanitizeIdentifierBoundsLongValues(t *testing.T) { input := "hpe-proliant-compute-dl380-gen12-nvidia-ax800-converge-0885f134" got := SanitizeIdentifier(input) assert.Len(t, got, MaxGeneratedIdentifierLength) + assert.Equal(t, "hpe-proliant-compute-dl-acdfaf", got) + assert.NotContains(t, got, "nvidia") assert.Equal(t, got, SanitizeIdentifier(input), "shortening must be deterministic") } diff --git a/pkg/networkoperatorplugin/discovery/discover.go b/pkg/networkoperatorplugin/discovery/discover.go index b4de7138..caee07a3 100644 --- a/pkg/networkoperatorplugin/discovery/discover.go +++ b/pkg/networkoperatorplugin/discovery/discover.go @@ -352,8 +352,8 @@ func DiscoverClusterConfig(ctx context.Context, c client.Client, restConfig *res // from the per-group hardware probes), assign each group a stable // machine label. This replaces the differential-label nodeSelector // algorithm: every node in the group is patched with - // `nvidia.kubernetes-launch-kit.machine: -` and - // the group's Identifier + NodeSelector are aligned with that value. + // `nvidia.kubernetes-launch-kit.machine: ` and the + // group's Identifier + NodeSelector are aligned with that value. applyMachineLabelToGroups(ctx, c, cfg.ClusterConfig) // Persist the automatic GPUDirect decision in the generated config. The @@ -485,18 +485,16 @@ func resolvePresetCatalog(opts Options) (*presets.Catalog, error) { // applyMachineLabelToGroups walks each group and writes two l8k-specific // labels onto every node in the group: // -// - MachineLabelKey = `-` literal — per-source-group -// identifier, written when both fields are resolved. +// - MachineLabelKey = the generated per-source-group identifier, written +// when both machineType and gpuType are resolved. // - GPULabelKey = `` literal — written when gpuType is resolved. // Used as the merged-group NodeSelector when source groups span // machineTypes but share a GPU type. // -// Long label values are shortened with a deterministic hash. Machine labels -// and generated group identifiers are bounded to 40 bytes so identifiers can -// be safely appended to other generated values; GPU labels use Kubernetes' -// 63-byte limit. Group `Identifier` follows the resource-name convention -// (lowercase via `SanitizeIdentifier`); label values keep their original case -// to match `nvidia.com/gpu.product`-style values. +// Machine labels and generated group identifiers share the same lowercase, +// vendor-free value, bounded to 30 bytes with balanced machine/GPU prefixes +// and a deterministic hash. GPU labels retain their original case and vendor +// segment and continue to use Kubernetes' 63-byte label-value limit. // // Groups whose machine label can't be computed (one input missing) keep // their fallback identifier ("group-N") and an empty NodeSelector. The @@ -505,10 +503,10 @@ func resolvePresetCatalog(opts Options) (*presets.Catalog, error) { func applyMachineLabelToGroups(ctx context.Context, c client.Client, groups []config.ClusterConfig) { for i := range groups { g := &groups[i] - machineLabel := config.MachineLabelValue(g.MachineType, g.GPUType) + groupIdentifier := config.GeneratedGroupIdentifier(g.MachineType, g.GPUType) gpuLabel := config.GPULabelValue(g.GPUType) - if machineLabel == "" { + if groupIdentifier == "" { log.Log.V(1).Info("Skipping machine label: machineType/gpuType unresolved", "group", g.Identifier, "machineType", g.MachineType, @@ -518,15 +516,15 @@ func applyMachineLabelToGroups(ctx context.Context, c client.Client, groups []co "originalIdentifier", g.Identifier, "machineType", g.MachineType, "gpuType", g.GPUType, - "labelValue", machineLabel, + "labelValue", groupIdentifier, "nodes", len(g.WorkerNodes)) - g.Identifier = config.SanitizeIdentifier(machineLabel) - g.NodeSelector = map[string]string{config.MachineLabelKey: machineLabel} + g.Identifier = groupIdentifier + g.NodeSelector = map[string]string{config.MachineLabelKey: groupIdentifier} } labels := map[string]string{} - if machineLabel != "" { - labels[config.MachineLabelKey] = machineLabel + if groupIdentifier != "" { + labels[config.MachineLabelKey] = groupIdentifier } if gpuLabel != "" { labels[config.GPULabelKey] = gpuLabel diff --git a/pkg/networkoperatorplugin/render_plan.go b/pkg/networkoperatorplugin/render_plan.go index 025edd26..10c88fd8 100644 --- a/pkg/networkoperatorplugin/render_plan.go +++ b/pkg/networkoperatorplugin/render_plan.go @@ -93,7 +93,7 @@ func planRender(originalGroups, filteredGroups []config.ClusterConfig) ([]Render // from there here keeps the aggregate IPPool's `In` list // in lockstep with the per-source NodePolicy selectors. // - // Falls back to `MachineLabelValue(machineType, gpuType)` + // Falls back to `GeneratedGroupIdentifier(machineType, gpuType)` // when the source has no machine label set (legacy configs // from before Unit 6, or groups whose machineType/gpuType // resolution failed at discover time). Empty means the @@ -103,7 +103,7 @@ func planRender(originalGroups, filteredGroups []config.ClusterConfig) ([]Render for _, src := range filteredSources { label := src.NodeSelector[config.MachineLabelKey] if label == "" { - label = config.MachineLabelValue(src.MachineType, src.GPUType) + label = config.GeneratedGroupIdentifier(src.MachineType, src.GPUType) } if label != "" { labels = append(labels, label) diff --git a/pkg/networkoperatorplugin/spectrumx_railpool_test.go b/pkg/networkoperatorplugin/spectrumx_railpool_test.go index 9b488392..a281326b 100644 --- a/pkg/networkoperatorplugin/spectrumx_railpool_test.go +++ b/pkg/networkoperatorplugin/spectrumx_railpool_test.go @@ -36,10 +36,10 @@ func TestSpectrumXRailPoolTemplatesStaySchemaCompatibleAndBounded(t *testing.T) for _, test := range tests { t.Run(test.name, func(t *testing.T) { - identifier := config.SanitizeIdentifier(config.MachineLabelValue( + identifier := config.GeneratedGroupIdentifier( "HPE-ProLiant-Compute-DL380-Gen12", "NVIDIA-AX800-Converged-Accelerator", - )) + ) require.LessOrEqual(t, len(identifier), config.MaxGeneratedIdentifierLength) cfg := &config.LaunchKitConfig{ diff --git a/pkg/networkoperatorplugin/templates.go b/pkg/networkoperatorplugin/templates.go index e331cff9..88d62d5a 100644 --- a/pkg/networkoperatorplugin/templates.go +++ b/pkg/networkoperatorplugin/templates.go @@ -94,8 +94,8 @@ var templateFuncs = template.FuncMap{ // typically 63 minus the length of the prefix the suffix is appended to. // Empty input returns "". Inputs whose natural "-" form fits within maxLen // are used verbatim. Longer inputs are truncated and combined with an FNV-32a - // hash of the original (same algorithm as MachineLabelValue), so the result - // is deterministic across calls but never breaches maxLen. + // hash of the original (the same FNV-32a basis used by generated identities), + // so the result is deterministic across calls but never breaches maxLen. "boundedSuffix": boundedSuffix, // xPlane has its own repository and version in Network Operator's release // metadata. Resolve them from the selected release catalog entry, while @@ -1096,7 +1096,7 @@ func isSpectrumX(cfg *config.LaunchKitConfig) bool { // boundedSuffix returns "-" bounded to at most maxLen chars (including // the leading "-"). When the natural "-" form fits, it is returned as-is; // otherwise the prefix is truncated and an 8-hex FNV-32a hash of the original -// is appended, matching MachineLabelValue's algorithm. Empty input returns "". +// is appended. Empty input returns "". // // Used by templates that compose label values from ClusterConfig.Identifier: // the rendered "" combination must respect k8s' diff --git a/pkg/networkoperatorplugin/templates_test.go b/pkg/networkoperatorplugin/templates_test.go index aacd3ac5..54e1ba8d 100644 --- a/pkg/networkoperatorplugin/templates_test.go +++ b/pkg/networkoperatorplugin/templates_test.go @@ -586,8 +586,14 @@ func TestSanitizeIdentifier(t *testing.T) { input string expected string }{ - {"NVIDIA-H200", "nvidia-h200"}, - {"NVIDIA A100 80GB PCIe", "nvidia-a100-80gb-pcie"}, + {"NVIDIA-H200", "h200"}, + {"nvidia-h200", "h200"}, + {"NVIDIA A100 80GB PCIe", "a100-80gb-pcie"}, + {"DGX-B200-NVIDIA-H200", "dgx-b200-h200"}, + {"NVIDIA-DGX-NVIDIA-H200", "dgx-h200"}, + {"ThinkSystem-SR680a-V3-NVIDIA-H100-NVL", "ts-sr680a-v3-h100-nvl"}, + {"PowerEdge-XE9680-NVIDIA-H200", "pe-xe9680-h200"}, + {"NVIDIAN-H200", "nvidian-h200"}, {"already-lowercase", "already-lowercase"}, {"MixedCase GPU", "mixedcase-gpu"}, } @@ -599,20 +605,19 @@ func TestSanitizeIdentifier(t *testing.T) { } // labelledGroup builds a source group fixture in the post-Unit-6 shape: -// MachineType + GPUType resolved, Identifier follows resource-name -// conventions (lowercase via sanitizeIdentifier), NodeSelector keyed by -// config.MachineLabelKey with the raw `-` value. +// MachineType + GPUType resolved, with Identifier and the MachineLabelKey +// NodeSelector sharing the generated group identity. func labelledGroup(machineType, gpuType string, pfs []config.PFConfig, nodes []string, caps *config.ClusterCapabilities) config.ClusterConfig { - labelValue := config.MachineLabelValue(machineType, gpuType) + identifier := config.GeneratedGroupIdentifier(machineType, gpuType) return config.ClusterConfig{ - Identifier: config.SanitizeIdentifier(labelValue), + Identifier: identifier, MachineType: machineType, GPUType: gpuType, PFs: pfs, WorkerNodes: nodes, Capabilities: caps, - NodeSelector: map[string]string{config.MachineLabelKey: labelValue}, + NodeSelector: map[string]string{config.MachineLabelKey: identifier}, } } @@ -644,7 +649,7 @@ func TestMergeCompatibleGroups(t *testing.T) { // resource-name conventions (lowercase via sanitizeIdentifier) and // nodeSelector keys on the GPULabelKey written by `l8k discover` // — its value matches the gpuType verbatim. - assert.Equal(t, "nvidia-h200", merged.Identifier) + assert.Equal(t, "h200", merged.Identifier) assert.Equal(t, "NVIDIA-H200", merged.GPUType) assert.Equal(t, "DGX-B200", merged.MachineType) assert.Equal(t, map[string]string{config.GPULabelKey: "NVIDIA-H200"}, merged.NodeSelector) @@ -673,8 +678,8 @@ func TestMergeCompatibleGroups(t *testing.T) { assert.Len(t, result, 2) // Single-source buckets keep the source group's machine-label identifier. - assert.Equal(t, config.SanitizeIdentifier(config.MachineLabelValue("DGX-B200", "NVIDIA-H200")), result[0].Identifier) - assert.Equal(t, config.SanitizeIdentifier(config.MachineLabelValue("DGX-B200", "NVIDIA-A100")), result[1].Identifier) + assert.Equal(t, config.GeneratedGroupIdentifier("DGX-B200", "NVIDIA-H200"), result[0].Identifier) + assert.Equal(t, config.GeneratedGroupIdentifier("DGX-B200", "NVIDIA-A100"), result[1].Identifier) assert.Nil(t, result[0].RailPciAddresses) assert.Nil(t, result[1].RailPciAddresses) }) @@ -695,7 +700,7 @@ func TestMergeCompatibleGroups(t *testing.T) { result, _ := mergeCompatibleGroups(groups, false) assert.Len(t, result, 1) - assert.Equal(t, "nvidia-h200", result[0].Identifier) + assert.Equal(t, "h200", result[0].Identifier) assert.Equal(t, map[string]string{config.GPULabelKey: "NVIDIA-H200"}, result[0].NodeSelector) }) @@ -734,12 +739,12 @@ func TestMergeCompatibleGroups(t *testing.T) { assert.Len(t, result, 2) // First: merged H200 group (group-0 + group-2) — gpuType-based identifier - assert.Equal(t, "nvidia-h200", result[0].Identifier) + assert.Equal(t, "h200", result[0].Identifier) assert.Equal(t, []string{"node-a", "node-c"}, result[0].WorkerNodes) assert.Len(t, result[0].RailPciAddresses, 1) assert.Equal(t, []string{"0000:19:00.0", "0000:09:00.0"}, result[0].RailPciAddresses[0]) // Second: unmerged DGX-A100/A100 group keeps its machine-label identifier - assert.Equal(t, config.SanitizeIdentifier(config.MachineLabelValue("DGX-A100", "NVIDIA-A100")), result[1].Identifier) + assert.Equal(t, config.GeneratedGroupIdentifier("DGX-A100", "NVIDIA-A100"), result[1].Identifier) assert.Nil(t, result[1].RailPciAddresses) }) @@ -753,7 +758,7 @@ func TestMergeCompatibleGroups(t *testing.T) { result, _ := mergeCompatibleGroups(groups, false) assert.Len(t, result, 1) - assert.Equal(t, config.SanitizeIdentifier(config.MachineLabelValue("DGX-B200", "NVIDIA-H200")), result[0].Identifier) + assert.Equal(t, config.GeneratedGroupIdentifier("DGX-B200", "NVIDIA-H200"), result[0].Identifier) assert.Nil(t, result[0].RailPciAddresses) }) @@ -800,7 +805,7 @@ func TestMergeCompatibleGroups(t *testing.T) { // Should merge: both have 1 east-west rail (north-south excluded from count) assert.Len(t, result, 1) - assert.Equal(t, "nvidia-h200", result[0].Identifier) + assert.Equal(t, "h200", result[0].Identifier) assert.Len(t, result[0].RailPciAddresses, 1) assert.Equal(t, []string{"0000:19:00.0", "0000:1a:00.0"}, result[0].RailPciAddresses[0]) }) @@ -877,7 +882,7 @@ func TestMergeCompatibleGroups(t *testing.T) { t.Run("cross-rail PCI address conflict prevents merge", func(t *testing.T) { // Same PCI address 0000:9c:00.0 at rail 4 in group-1 and rail 5 in group-2. // Merging would cause the device plugin to claim it for the wrong rail. - expectedID := config.SanitizeIdentifier(config.MachineLabelValue("DGX-B200", "NVIDIA-H200")) + expectedID := config.GeneratedGroupIdentifier("DGX-B200", "NVIDIA-H200") groups := []config.ClusterConfig{ labelledGroup("DGX-B200", "NVIDIA-H200", []config.PFConfig{ewPF("0000:19:00.0", 0), ewPF("0000:9b:00.0", 1)}, diff --git a/skills/k8s-launch-kit-config/SKILL.md b/skills/k8s-launch-kit-config/SKILL.md index 186d65fa..1e4a9aa1 100644 --- a/skills/k8s-launch-kit-config/SKILL.md +++ b/skills/k8s-launch-kit-config/SKILL.md @@ -1,6 +1,6 @@ --- name: k8s-launch-kit-config -version: 1.2.3 +version: 1.2.4 description: "Use this skill when the user needs help understanding, creating, or editing a k8s-launch-kit (l8k) configuration file (l8k-config.yaml or cluster-config.yaml). Activate for: config file questions, parameter tuning, subnet configuration, NV-IPAM setup, DOCA driver settings, maintenance concurrency, NIC configuration operator settings, changing MTU, VFs, resource names, or understanding what any config field does." metadata: requires: @@ -66,13 +66,13 @@ false across rewrites. Each `clusterConfig[]` entry has these key fields: -- `identifier` — group name (used for `NicNodePolicy` naming). For groups with both `machineType` and `gpuType` resolved, this is the sanitised machine label (`-`), bounded to 40 bytes with a deterministic hash suffix when needed; otherwise a fallback `group-N`. +- `identifier` — group name (used for `NicNodePolicy` naming). For groups with both `machineType` and `gpuType` resolved, this is the lowercased machine/GPU identity with complete `NVIDIA` segments removed and common machine segments shortened (`ThinkSystem` → `ts`, `PowerEdge` → `pe`), bounded to 30 bytes with balanced component prefixes and a 6-character deterministic hash when needed; otherwise a fallback `group-N`. The Launch Kit machine node label uses the same value. - `machineType` — server model (e.g. `PowerEdge-XE9680`); populated from `nvidia.com/gpu.machine` label or DMI fallback. - `gpuType` — GPU SKU (e.g. `NVIDIA-H200`); populated from `nvidia.com/gpu.product` label or `nvidia-smi` fallback. **Note:** this field used to be called `productType` — the rename happened to disambiguate it from the server model. Old `productType:` keys in hand-authored configs must be renamed to `gpuType:`. - `capabilities.nodes.{sriov,rdma,ib}` — what the underlying hardware supports. - `pfs[]` — physical function list with PCI address, device ID, RDMA device, network interface, traffic class, rail, NUMA, GPU affinity, and `model` (the VPD model/description string read from `NicDevice.Status.modelName`). - **One rail per NIC (default).** Discovery advertises one rail per physical NIC: a NIC's multi-plane east-west PFs (planes of one port, e.g. Spectrum-X ConnectX-8/9) collapse to the master PF, so an 8-PF node lists 4 east-west PFs / 4 rails. A NIC whose `model` is genuinely dual-port (`2-port`/`Dual-port`) keeps a rail per port. Run `l8k discover --collapse-nic-rails=false` to emit one rail per PF (legacy/dev behaviour). -- `nodeSelector` — Kubernetes node selector for this group. Source groups key on the machine label written by `l8k discover`: `nvidia.kubernetes-launch-kit.machine: -`. Auto-merged groups (different machineTypes sharing a GPU type) key on `nvidia.kubernetes-launch-kit.gpu: ` instead — discovery writes both labels onto every node, so the merged selector binds correctly across source machineTypes. +- `nodeSelector` — Kubernetes node selector for this group. Source groups key on the machine label written by `l8k discover`: `nvidia.kubernetes-launch-kit.machine: `. Auto-merged groups (different machineTypes sharing a GPU type) key on `nvidia.kubernetes-launch-kit.gpu: ` instead — the GPU label retains its discovered value, including `NVIDIA`, so the merged selector binds correctly across source machineTypes. - `workerNodes` — explicit hostnames (populated by discovery). For the full field-by-field reference with types, defaults, and descriptions, diff --git a/skills/k8s-launch-kit-config/references/config-reference.md b/skills/k8s-launch-kit-config/references/config-reference.md index 06a9e53e..af60dbee 100644 --- a/skills/k8s-launch-kit-config/references/config-reference.md +++ b/skills/k8s-launch-kit-config/references/config-reference.md @@ -376,7 +376,7 @@ validation: # Array of hardware groups. Typically populated by --discover-cluster-config. # ============================================================================ clusterConfig: - - # string — Group identifier (auto-generated by discovery; max 40 bytes) + - # string — Group identifier/machine label (vendor-free; common machine aliases; max 30 bytes) identifier: "" capabilities: diff --git a/skills/k8s-launch-kit-discover/SKILL.md b/skills/k8s-launch-kit-discover/SKILL.md index 546603f4..bb928d80 100644 --- a/skills/k8s-launch-kit-discover/SKILL.md +++ b/skills/k8s-launch-kit-discover/SKILL.md @@ -1,6 +1,6 @@ --- name: k8s-launch-kit-discover -version: 1.2.3 +version: 1.2.4 description: "Use this skill when the user wants to discover their Kubernetes cluster's network hardware capabilities using k8s-launch-kit (l8k). Activate for: cluster discovery, hardware detection, NIC detection, finding what GPUs or NICs are in a cluster, creating a cluster config file, or when the user says 'discover' in the context of l8k or NVIDIA networking." metadata: requires: @@ -115,7 +115,7 @@ profile: deployment: sriov multirail: true clusterConfig: - - identifier: "dgx-b200-nvidia-h100-nvl" + - identifier: "dgx-b200-h100-nvl" machineType: DGX-B200 gpuType: NVIDIA-H100-NVL capabilities: @@ -128,25 +128,27 @@ clusterConfig: rail: 0 workerNodes: [node-01, node-02] nodeSelector: - nvidia.kubernetes-launch-kit.machine: "DGX-B200-NVIDIA-H100-NVL" + nvidia.kubernetes-launch-kit.machine: "dgx-b200-h100-nvl" thirdPartyRDMAModules: [nv_peer_mem] ``` Discovery patches every node in the group with two labels: -- `nvidia.kubernetes-launch-kit.machine: -` — per-source-group - identity, used as the source group's `nodeSelector`. +- `nvidia.kubernetes-launch-kit.machine: ` — per-source-group + identity, identical to the persisted group `identifier` and used as its + `nodeSelector`. - `nvidia.kubernetes-launch-kit.gpu: ` — used as the merged-group `nodeSelector` when `l8k generate` auto-merges source groups sharing a GPU type. -Label values keep their original case (matching `nvidia.com/gpu.product` style) since -upstream discovery already trims whitespace and replaces spaces with hyphens. The -combined machine/GPU label and the lowercase group `identifier` are bounded to 40 -bytes; long values retain a readable prefix plus an 8-character deterministic hash. -GPU-only labels use Kubernetes' 63-byte limit with the same shortening rule. When -`machineType` or `gpuType` couldn't be resolved (GPU operator labels absent and -hardware probe failed), a fallback `group-N` identifier is used and the machine label -is not written; the GPU label is still written when `gpuType` alone is resolved. +Group identifiers and machine-label values are lowercase and omit complete `NVIDIA` +segments. The same pass maps `ThinkSystem` to `ts` and `PowerEdge` to `pe`. They are +bounded to 30 bytes; long values retain balanced machine/GPU prefixes plus a +6-character deterministic hash, and unused prefix space moves to the longer component. +GPU-only labels retain their discovered case and vendor segment, use Kubernetes' +63-byte limit, and keep the existing 8-character shortening hash. When `machineType` +or `gpuType` couldn't be resolved (GPU operator labels absent and hardware probe +failed), a fallback `group-N` identifier is used and the machine label is not written; +the GPU label is still written when `gpuType` alone is resolved. ## Prerequisites diff --git a/skills/k8s-launch-kit-generate/SKILL.md b/skills/k8s-launch-kit-generate/SKILL.md index 84db5683..abeea4bb 100644 --- a/skills/k8s-launch-kit-generate/SKILL.md +++ b/skills/k8s-launch-kit-generate/SKILL.md @@ -1,6 +1,6 @@ --- name: k8s-launch-kit-generate -version: 1.2.6 +version: 1.2.7 description: "Use this skill when the user wants to generate Kubernetes YAML manifests for NVIDIA networking deployment using k8s-launch-kit (l8k). Activate for: manifest generation, profile selection, choosing between SR-IOV/host-device/RDMA-shared/IPoIB/MacVLAN/Spectrum-X, creating deployment files, or when the user asks 'which profile should I use' or needs help choosing a network configuration." metadata: requires: @@ -44,7 +44,7 @@ to that source file; embedded `--for` generation does not write a config. | `--topology-file` | Required with `--spectrum-x` | path | spcx-gen/reference-generator or contract-compliant NVIDIA AIR topology JSON. The format is detected from the JSON structure. | | `--multirail` | Auto-defaulted | — | Auto-defaults to `true`. Explicit `multirail: false` in YAML and `--multirail=false` on the CLI are both preserved. | | `--save-deployment-files` | Yes | — | Output directory for generated YAMLs | -| `--groups` | — | `dgx-b200-nvidia-h100-nvl,poweredge-xe9680-nvidia-h200` | Restrict output to the named source groups (comma-separated). Mutually exclusive with `--gpu-type`. | +| `--groups` | — | `dgx-b200-h100-nvl,pe-xe9680-h200` | Restrict output to the named source groups (comma-separated). Mutually exclusive with `--gpu-type`. | | `--gpu-type` | — | `NVIDIA-H200` | Restrict output to source groups whose `gpuType` matches (case-insensitive). Mutually exclusive with `--groups`. | | `--for` | — | preset directory name | Skip discovery: synthesize `clusterConfig` from a topology preset. Requires `--node-selector`. List options with `l8k preset list`. | | `--node-selector` | Required with `--for` | `key=val,key2=val2` | Identifies which nodes the synthesized clusterConfig targets at apply time. | @@ -188,8 +188,11 @@ win when a one-off override is needed. - RA2.2 and RA2.3 v1alpha2 `SpectrumXRailPoolConfig` output intentionally omits the removed `spec.withBCM` field; current CRDs reject it during strict decoding. -- Group identifiers produced by discovery are bounded to 40 bytes with a - deterministic hash suffix. Use the exact persisted identifier from +- Group identifiers produced by discovery omit complete `NVIDIA` segments and + shorten common machine segments (`ThinkSystem` → `ts`, `PowerEdge` → `pe`). + They are bounded to 30 bytes with balanced machine/GPU prefixes and a + 6-character deterministic hash suffix. The machine label uses the same value; + the GPU label still retains its discovered value such as `NVIDIA-H200`. Use the exact persisted identifier from `cluster-config.yaml` with `--groups`; do not reconstruct it from long `machineType` and `gpuType` strings. - Use `--groups ` (case-sensitive identifier list) or `--gpu-type ` (case-insensitive) to scope a generate to a subset of source groups in heterogeneous clusters. Mutually exclusive. Empty match is a validation error. Strict-subset filters split per-source rendering: NodePolicies emit one CR per source (each with its own machine-label nodeSelector but a shared bucket-level resourceName); IPPool/example DaemonSet emit one CR per bucket with an `In` list of source machine labels. diff --git a/skills/k8s-network-engineer/SKILL.md b/skills/k8s-network-engineer/SKILL.md index d69594c5..cfe1ce94 100644 --- a/skills/k8s-network-engineer/SKILL.md +++ b/skills/k8s-network-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: k8s-network-engineer -version: 1.4.1 +version: 1.4.2 description: "Embody a senior NVIDIA Networking Engineer who is an expert on deploying cloud-native networking on Kubernetes with k8s-launch-kit (l8k). Activate whenever the user mentions NVIDIA network profiles, SR-IOV, RDMA, Spectrum-X, BlueField, ConnectX, NIC configuration, Network Operator, DOCA drivers, multirail networking, l8k, k8s-launch-kit, or any Kubernetes networking topic involving NVIDIA hardware. Also activate when the user asks general questions about high-performance networking, GPU interconnect, or RDMA configuration." metadata: requires: diff --git a/skills/k8s-network-engineer/references/config-schema.md b/skills/k8s-network-engineer/references/config-schema.md index 9eaee496..e00e4fa2 100644 --- a/skills/k8s-network-engineer/references/config-schema.md +++ b/skills/k8s-network-engineer/references/config-schema.md @@ -237,7 +237,7 @@ homogeneous worker nodes with their NIC hardware. | Field | Type | Default | Description | |----------------------------|----------|---------|------------------------------------------------| -| `identifier` | string | `""` | Unique group name (auto-generated during discovery, max 40 bytes) | +| `identifier` | string | `""` | Unique group name/machine label (vendor-free, common machine aliases, max 30 bytes) | | `capabilities.nodes.sriov` | bool | `true` | Nodes have SR-IOV capable NICs | | `capabilities.nodes.rdma` | bool | `true` | Nodes have RDMA capable NICs | | `capabilities.nodes.ib` | bool | `false` | Nodes have InfiniBand capable NICs |