Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions hack/lib/helm-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ const path = require("node:path");
const { spawnSync } = require("node:child_process");
const YAML = require("yaml");
const { modelNames, versionFiles, variantFiles, kindOf } = require("./catalog");
const { valueSetAt } = require("./inputs");

function affectsValidator(file) {
return (
file.startsWith("schema/crds/") ||
/^hack\/(validate-helm|install-helm-tools|build-crds)(\.integration)?\.(js|test\.js)$/.test(file) ||
/^hack\/lib\/(helm-validation|crd-schema|download|catalog)(\.test)?\.js$/.test(file) ||
/^hack\/lib\/(helm-validation|crd-schema|download|catalog|inputs)(\.test)?\.js$/.test(file) ||
["package.json", "package-lock.json", ".github/workflows/lint.yml"].includes(file)
);
}
Expand Down Expand Up @@ -122,9 +123,9 @@ function parseVersions(file) {
// variant spells first-class projected on top, so they win over the same keys
// in values. image.digest is not projected.
//
// Each declared input is set to its default, else its example, standing in for
// the answer swiss puts in the derived layer at deploy, so the chart renders
// every path swiss fills. values never sets one; validate:schema refuses that.
// Each declared input renders as swiss composes it with no site answer: its
// default, else the value values gives it, else its example, standing in for
// the answer a deploy gives.
function catalogValues(variant) {
const values = structuredClone(variant.values ?? {});
const set = (keys, value) => {
Expand All @@ -140,7 +141,11 @@ function catalogValues(variant) {
set(["image", "repository"], variant.image.repository);
set(["image", "tag"], String(variant.image.tag ?? ""));
}
for (const input of variant.inputs ?? []) set(input.key.split("."), "default" in input ? input.default : input.example);
for (const input of variant.inputs ?? []) {
const keys = input.key.split(".");
if ("default" in input) set(keys, input.default);
else if (valueSetAt(values, keys) !== input.key) set(keys, input.example);
}
return values;
}

Expand Down
8 changes: 5 additions & 3 deletions hack/lib/helm-validation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,19 +197,21 @@ test("variant values get swiss's projection of servedName, requires.gpus and the
assert.deepEqual(catalogValues(v2), { model: { name: "served", gpus: "2" } });
});

test("a declared input renders with its default, else its example", () => {
test("a declared input renders with its default, else the value values gives it, else its example", () => {
const variant = {
servedName: "m",
requires: { gpus: 8 },
inputs: [
{ key: "storageKVOffload.mooncake.master", title: "t", example: "mooncake-master.example:50051" },
{ key: "storageKVOffload.mooncake.device", title: "t", example: "mlx5_0", default: "mlx5_3" },
{ key: "storageKVOffload.mooncake.te_meta_data_server", title: "t", example: "http://x.example/m" },
{ key: "storageKVOffload.mooncake.global_segment_size", title: "t", example: "2gb", default: "4gb" },
],
values: { storageKVOffload: { enabled: true, mooncake: { protocol: "rdma" } } },
values: { storageKVOffload: { enabled: true, mooncake: { protocol: "rdma", te_meta_data_server: "", global_segment_size: "1gb" } } },
};
assert.deepEqual(catalogValues(variant).storageKVOffload, {
enabled: true,
mooncake: { protocol: "rdma", master: "mooncake-master.example:50051", device: "mlx5_3" },
mooncake: { protocol: "rdma", master: "mooncake-master.example:50051", device: "mlx5_3", te_meta_data_server: "", global_segment_size: "4gb" },
});
assert.equal(variant.values.storageKVOffload.mooncake.master, undefined);
});
13 changes: 7 additions & 6 deletions hack/lib/inputs.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"use strict";
// A variant's inputs, past what the schema can see. A key is declared once, and
// values leaves it alone: swiss puts the answer, the site's or else the input's
// default, in the derived layer. The example and the default are valid answers.
// swiss repeats these checks when it loads a variant (internal/catalog).
// A variant's inputs, past what the schema can see. A key is declared once. A
// value values gives it is the base an answer overrides: swiss puts the answer,
// the site's or else the input's default, in the derived layer above it. The
// example and the default are valid answers. swiss repeats these checks when it
// loads a variant (internal/catalog).

// Problems with one variant's inputs, each line prefixed with at.
function inputProblems(variant, at = "") {
Expand All @@ -15,7 +16,7 @@ function inputProblems(variant, at = "") {
if (declared.has(input.key)) lines.push(`${where}declared twice`);
declared.add(input.key);
const set = valueSetAt(variant.values, input.key.split("."));
if (set) lines.push(`${where}values.${set} sets it; give the value as the input's default`);
if (set && set !== input.key) lines.push(`${where}values.${set} is not a map, so the answer has no place to go`);
const type = input.type ?? "string";
let pattern;
if (input.pattern !== undefined && type !== "string") {
Expand Down Expand Up @@ -55,4 +56,4 @@ function valueSetAt(values, keys) {
return keys.join(".");
}

module.exports = { inputProblems };
module.exports = { inputProblems, valueSetAt };
15 changes: 6 additions & 9 deletions hack/lib/inputs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,13 @@ test("a declared input that values leaves alone, with a valid example and defaul
assert.deepEqual(inputProblems({ requires: { gpus: 1 } }), []);
});

test("values may not set an input: at its key, under it, or with a non-map on the way", () => {
for (const [values, at] of [
[{ storageKVOffload: { mooncake: { master: "mc:50051" } } }, "storageKVOffload.mooncake.master"],
[{ storageKVOffload: { mooncake: { master: { host: "h" } } } }, "storageKVOffload.mooncake.master"],
[{ storageKVOffload: { mooncake: "off" } }, "storageKVOffload.mooncake"],
]) {
assert.deepEqual(inputProblems(variant([master], values), "v: "), [
`v: inputs[0] storageKVOffload.mooncake.master: values.${at} sets it; give the value as the input's default`,
]);
test("values may set an input's key, as the base an answer overrides, but no non-map above it", () => {
for (const value of ["mooncake-master.example:50051", ""]) {
assert.deepEqual(inputProblems(variant([master], { storageKVOffload: { mooncake: { master: value } } })), []);
}
assert.deepEqual(inputProblems(variant([master], { storageKVOffload: { mooncake: "off" } }), "v: "), [
"v: inputs[0] storageKVOffload.mooncake.master: values.storageKVOffload.mooncake is not a map, so the answer has no place to go",
]);
});

test("the default is an answer: of the input's type, matching its pattern", () => {
Expand Down
17 changes: 16 additions & 1 deletion models/deepseek-v4-flash-0731-nvfp4/sglang-tp8-l3-6000d.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: catalog.swiss/v2
kind: Variant
model: deepseek-v4-flash-0731-nvfp4
id: sglang-tp8-l3-6000d
version: 1.0.6
version: 1.0.7
servedName: deepseek-v4-flash-0731-nvfp4
engine: sglang
description: >-
Expand Down Expand Up @@ -34,6 +34,21 @@ requires:
# Not expressible in this block, but required on the node:
# * ~650 GiB of 2 MiB hugepages reserved AND advertised by kubelet
# * routes to the EBOF targets on the storage NICs
inputs:
- key: storageKVOffload.mooncake.master
title: Mooncake master
description: host:port of the standalone Mooncake master, outside Kubernetes, owning NoF volumes no other master holds.
pattern: '[^\s:/]+:[0-9]+'
example: mooncake-master.example:50051
- key: storageKVOffload.mooncake.te_meta_data_server
title: Mooncake metadata server
description: The transfer engine's metadata server URL, or P2PHANDSHAKE.
pattern: 'https?://\S+|P2PHANDSHAKE'
example: http://mooncake-master.example:8080/metadata
- key: storageKVOffload.mooncake.device
title: Mooncake storage HCAs
description: The storage NICs, comma-separated. Left to auto-detect, Mooncake picked the management NIC on the cluster this was tuned on.
example: mlx5_0,mlx5_1
values:
model:
localPath: /data/models/DeepSeek-V4-Flash-0731-NVFP4
Expand Down
17 changes: 16 additions & 1 deletion models/deepseek-v4-flash-0731-nvfp4/sglang-tp8-pd-l3-6000d.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: catalog.swiss/v2
kind: Variant
model: deepseek-v4-flash-0731-nvfp4
id: sglang-tp8-pd-l3-6000d
version: 1.0.4
version: 1.0.5
servedName: deepseek-v4-flash-0731-nvfp4
engine: sglang
description: >-
Expand Down Expand Up @@ -41,6 +41,21 @@ requires:
# ebof.modelsphere.io/storage-net=true; pd.prefill.affinityReplace pins them):
# * ~650 GiB of 2 MiB hugepages reserved AND advertised by kubelet
# * routes to the EBOF targets on the storage NICs
inputs:
- key: storageKVOffload.mooncake.master
title: Mooncake master
description: host:port of the standalone Mooncake master, outside Kubernetes, owning NoF volumes no other master holds.
pattern: '[^\s:/]+:[0-9]+'
example: mooncake-master.example:50051
- key: storageKVOffload.mooncake.te_meta_data_server
title: Mooncake metadata server
description: The transfer engine's metadata server URL, or P2PHANDSHAKE.
pattern: 'https?://\S+|P2PHANDSHAKE'
example: http://mooncake-master.example:8080/metadata
- key: storageKVOffload.mooncake.device
title: Mooncake storage HCAs
description: The storage NICs, comma-separated. Left to auto-detect, Mooncake picked the management NIC on the cluster this was tuned on.
example: mlx5_0,mlx5_1
values:
model:
localPath: /data/models/DeepSeek-V4-Flash-0731-NVFP4
Expand Down
17 changes: 16 additions & 1 deletion models/glm5.3/sglang-tp8-pp2-ep8-pd-l3-6000d.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: catalog.swiss/v2
kind: Variant
model: glm5.3
id: sglang-tp8-pp2-ep8-pd-l3-6000d
version: 1.0.5
version: 1.0.6
servedName: glm-5.3
engine: sglang
description: >-
Expand All @@ -24,6 +24,21 @@ requires:
rdma: true
gpuProduct:
- NVIDIA-RTX-6000D
inputs:
- key: storageKVOffload.mooncake.master
title: Mooncake master
description: host:port of the Mooncake Store master this KV tier uses.
pattern: '[^\s:/]+:[0-9]+'
example: mooncake-master.example:50051
- key: storageKVOffload.mooncake.te_meta_data_server
title: Mooncake metadata server
description: The transfer engine's metadata server URL, or P2PHANDSHAKE.
pattern: 'https?://\S+|P2PHANDSHAKE'
example: http://mooncake-master.example:8080/metadata
- key: storageKVOffload.mooncake.device
title: Mooncake storage HCAs
description: The storage NICs, comma-separated. Empty lets Mooncake pick.
example: mlx5_0,mlx5_1
values:
model:
localPath: /data/models/GLM-5.3-NVFP4
Expand Down
2 changes: 1 addition & 1 deletion schema/version.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@
]
},
"inputs": {
"description": "Chart values every deploy must answer, such as the address of a service the site runs. Unlike requires, which swiss interprets and checks against the cluster, an input names one chart value by its key, and swiss only relays its answer: swiss puts the answer in the derived layer -- the site profile's inputs, else the input's default -- and the deploy's overrides go over it. swiss refuses to compose a plan that leaves one without a valid answer. values never sets an input's key. model.localPath is never one: swiss answers it from the site's model.pathTemplate.",
"description": "Chart values every deploy must answer, such as the address of a service the site runs. Unlike requires, which swiss interprets and checks against the cluster, an input names one chart value by its key, and swiss only relays its answer: swiss puts the answer in the derived layer -- the site profile's inputs, else the input's default -- and the deploy's overrides go over it. swiss refuses to compose a plan that leaves one without a valid answer. A value values gives an input's key is the base its answer overrides. model.localPath is never one: swiss answers it from the site's model.pathTemplate.",
"type": "array",
"minItems": 1,
"items": {
Expand Down
Loading