From 8b1f1e483a7a1be5ef05c296bb3850f2388b61a6 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Sat, 10 Oct 2026 17:47:51 +0800 Subject: [PATCH 1/7] feat!: rewrite image location to docker hub --- .../deepseek-v4-flash-0731-1.0.0.yaml | 4 ++-- models/deepseek-v4-flash/deepseek-v4-flash-1.0.0.yaml | 4 ++-- models/deepseek-v4-flash/deepseek-v4-flash-1.0.1.yaml | 4 ++-- models/deepseek-v4-flash/deepseek-v4-flash-1.0.2.yaml | 4 ++-- models/deepseek-v4.1-flash/deepseek-v4.1-flash-1.0.0.yaml | 4 ++-- models/gemma-4-26b-a4b/gemma-4-26b-a4b-it-1.0.0.yaml | 8 ++++---- models/gemma-4-31b-it/google-gemma-4-31b-it-1.0.0.yaml | 6 +++--- models/glm5.3/glm5.3-1.0.0.yaml | 2 +- models/glm5.3/glm5.3-1.0.1.yaml | 2 +- models/glm5.3/glm5.3-1.0.2.yaml | 2 +- models/glm5.3/glm5.3-1.0.3.yaml | 6 +++--- models/glm5.3/glm5.3-1.0.4.yaml | 6 +++--- models/gpt-oss-120b/gpt-oss-120b-1.0.0.yaml | 4 ++-- models/gpt-oss-20b/gpt-oss-20b-1.0.0.yaml | 8 ++++---- models/hy3/hy3-1.0.0.yaml | 4 ++-- models/kimi-k3/kimi-k3-1.0.0.yaml | 2 +- models/kimi-k3/kimi-k3-1.0.1.yaml | 2 +- models/kimi-k3/kimi-k3-1.0.2.yaml | 2 +- models/kimi-k3/kimi-k3-1.0.3.yaml | 2 +- models/kimi-k3/kimi-k3-1.0.4.yaml | 6 +++--- models/kimi-k3/kimi-k3-1.0.5.yaml | 6 +++--- models/laguna-s-2.1/laguna-s-2.1-1.0.0.yaml | 4 ++-- models/ling-3.0-flash-vl/ling-3.0-flash-vl-1.0.0.yaml | 4 ++-- models/ling-3.0-flash/ling-3.0-flash-1.0.0.yaml | 4 ++-- models/mimo-v2.5/mimo-v2.5-1.0.0.yaml | 4 ++-- .../mistral-nemo-instruct-1.0.0.yaml | 8 ++++---- .../mistral-small-4-119b-2603-1.0.0.yaml | 6 +++--- .../nemotron-3.5-lightning-30b-a3b-1.0.0.yaml | 4 ++-- models/nex-n2.5-pro/nex-n2.5-pro-1.0.0.yaml | 4 ++-- models/qwen3.6-35b-a3b/qwen3.6-35b-a3b-1.0.0.yaml | 2 +- models/qwen3.6-35b-a3b/qwen3.6-35b-a3b-1.1.0.yaml | 6 +++--- models/qwen3.8-27b-fp8/qwen3.8-27b-fp8-1.0.0.yaml | 8 ++++---- models/qwen3.8-flash-next/qwen3.8-flash-next-1.0.0.yaml | 4 ++-- 33 files changed, 73 insertions(+), 73 deletions(-) diff --git a/models/deepseek-v4-flash-0731/deepseek-v4-flash-0731-1.0.0.yaml b/models/deepseek-v4-flash-0731/deepseek-v4-flash-0731-1.0.0.yaml index 837ad43..8dda640 100644 --- a/models/deepseek-v4-flash-0731/deepseek-v4-flash-0731-1.0.0.yaml +++ b/models/deepseek-v4-flash-0731/deepseek-v4-flash-0731-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 requires: gpus: 8 @@ -81,7 +81,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 requires: gpus: 8 diff --git a/models/deepseek-v4-flash/deepseek-v4-flash-1.0.0.yaml b/models/deepseek-v4-flash/deepseek-v4-flash-1.0.0.yaml index 2ed49ac..d03fd9d 100644 --- a/models/deepseek-v4-flash/deepseek-v4-flash-1.0.0.yaml +++ b/models/deepseek-v4-flash/deepseek-v4-flash-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 @@ -81,7 +81,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 diff --git a/models/deepseek-v4-flash/deepseek-v4-flash-1.0.1.yaml b/models/deepseek-v4-flash/deepseek-v4-flash-1.0.1.yaml index 70fec12..0663a3f 100644 --- a/models/deepseek-v4-flash/deepseek-v4-flash-1.0.1.yaml +++ b/models/deepseek-v4-flash/deepseek-v4-flash-1.0.1.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 @@ -81,7 +81,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 diff --git a/models/deepseek-v4-flash/deepseek-v4-flash-1.0.2.yaml b/models/deepseek-v4-flash/deepseek-v4-flash-1.0.2.yaml index ebbcedc..f9f889e 100644 --- a/models/deepseek-v4-flash/deepseek-v4-flash-1.0.2.yaml +++ b/models/deepseek-v4-flash/deepseek-v4-flash-1.0.2.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 @@ -81,7 +81,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 diff --git a/models/deepseek-v4.1-flash/deepseek-v4.1-flash-1.0.0.yaml b/models/deepseek-v4.1-flash/deepseek-v4.1-flash-1.0.0.yaml index 1e6aaae..8d26878 100644 --- a/models/deepseek-v4.1-flash/deepseek-v4.1-flash-1.0.0.yaml +++ b/models/deepseek-v4.1-flash/deepseek-v4.1-flash-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 @@ -90,7 +90,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 diff --git a/models/gemma-4-26b-a4b/gemma-4-26b-a4b-it-1.0.0.yaml b/models/gemma-4-26b-a4b/gemma-4-26b-a4b-it-1.0.0.yaml index 192c1e9..f791d81 100644 --- a/models/gemma-4-26b-a4b/gemma-4-26b-a4b-it-1.0.0.yaml +++ b/models/gemma-4-26b-a4b/gemma-4-26b-a4b-it-1.0.0.yaml @@ -11,9 +11,9 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 - digest: sha256:b7a3051b9e15939a5afc74de3564a72fd5b0b42bef80bbaacea9649d09f63d76 + digest: sha256:59e11312666e1c5c155210ea335589b91daa0d70848521b390b93b1b1e8fb0ef requires: gpus: 2 topology: single-node @@ -82,9 +82,9 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 - digest: sha256:b7a3051b9e15939a5afc74de3564a72fd5b0b42bef80bbaacea9649d09f63d76 + digest: sha256:59e11312666e1c5c155210ea335589b91daa0d70848521b390b93b1b1e8fb0ef requires: gpus: 2 topology: single-node diff --git a/models/gemma-4-31b-it/google-gemma-4-31b-it-1.0.0.yaml b/models/gemma-4-31b-it/google-gemma-4-31b-it-1.0.0.yaml index 7097657..247a538 100644 --- a/models/gemma-4-31b-it/google-gemma-4-31b-it-1.0.0.yaml +++ b/models/gemma-4-31b-it/google-gemma-4-31b-it-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 requires: gpus: 4 @@ -82,9 +82,9 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 - digest: sha256:b7a3051b9e15939a5afc74de3564a72fd5b0b42bef80bbaacea9649d09f63d76 + digest: sha256:59e11312666e1c5c155210ea335589b91daa0d70848521b390b93b1b1e8fb0ef requires: gpus: 4 topology: single-node diff --git a/models/glm5.3/glm5.3-1.0.0.yaml b/models/glm5.3/glm5.3-1.0.0.yaml index f6e6128..09a8691 100644 --- a/models/glm5.3/glm5.3-1.0.0.yaml +++ b/models/glm5.3/glm5.3-1.0.0.yaml @@ -15,7 +15,7 @@ variants: name: sglang version: 0.7.1 image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 4 diff --git a/models/glm5.3/glm5.3-1.0.1.yaml b/models/glm5.3/glm5.3-1.0.1.yaml index a765aff..7e39e27 100644 --- a/models/glm5.3/glm5.3-1.0.1.yaml +++ b/models/glm5.3/glm5.3-1.0.1.yaml @@ -15,7 +15,7 @@ variants: name: sglang version: ">=0.7.8" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 4 diff --git a/models/glm5.3/glm5.3-1.0.2.yaml b/models/glm5.3/glm5.3-1.0.2.yaml index c10560f..0b18b7d 100644 --- a/models/glm5.3/glm5.3-1.0.2.yaml +++ b/models/glm5.3/glm5.3-1.0.2.yaml @@ -15,7 +15,7 @@ variants: name: sglang version: ">=0.7.8" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 4 diff --git a/models/glm5.3/glm5.3-1.0.3.yaml b/models/glm5.3/glm5.3-1.0.3.yaml index 4743c8e..baee50d 100644 --- a/models/glm5.3/glm5.3-1.0.3.yaml +++ b/models/glm5.3/glm5.3-1.0.3.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.8" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 8 @@ -85,9 +85,9 @@ variants: name: sglang version: ">=0.7.8" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 - digest: sha256:88e6445cca3f7f22af819152f0ea5ac94d8702583634ab2d1a2ea2a5b282f986 + digest: sha256:d6e7288627be8b02be88e4bba38e73f6d50e2826869f753c13a4c4385ab3eda9 requires: gpus: 4 topology: single-node diff --git a/models/glm5.3/glm5.3-1.0.4.yaml b/models/glm5.3/glm5.3-1.0.4.yaml index 4e6d644..9b8942d 100644 --- a/models/glm5.3/glm5.3-1.0.4.yaml +++ b/models/glm5.3/glm5.3-1.0.4.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.8" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 8 @@ -85,9 +85,9 @@ variants: name: sglang version: ">=0.7.8" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 - digest: sha256:88e6445cca3f7f22af819152f0ea5ac94d8702583634ab2d1a2ea2a5b282f986 + digest: sha256:d6e7288627be8b02be88e4bba38e73f6d50e2826869f753c13a4c4385ab3eda9 requires: gpus: 4 topology: single-node diff --git a/models/gpt-oss-120b/gpt-oss-120b-1.0.0.yaml b/models/gpt-oss-120b/gpt-oss-120b-1.0.0.yaml index 717a5a4..979d118 100644 --- a/models/gpt-oss-120b/gpt-oss-120b-1.0.0.yaml +++ b/models/gpt-oss-120b/gpt-oss-120b-1.0.0.yaml @@ -10,7 +10,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 2 @@ -82,7 +82,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 diff --git a/models/gpt-oss-20b/gpt-oss-20b-1.0.0.yaml b/models/gpt-oss-20b/gpt-oss-20b-1.0.0.yaml index 45cdf01..6848e73 100644 --- a/models/gpt-oss-20b/gpt-oss-20b-1.0.0.yaml +++ b/models/gpt-oss-20b/gpt-oss-20b-1.0.0.yaml @@ -10,9 +10,9 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 - digest: sha256:b7a3051b9e15939a5afc74de3564a72fd5b0b42bef80bbaacea9649d09f63d76 + digest: sha256:59e11312666e1c5c155210ea335589b91daa0d70848521b390b93b1b1e8fb0ef requires: gpus: 1 topology: single-node @@ -81,9 +81,9 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 - digest: sha256:b7a3051b9e15939a5afc74de3564a72fd5b0b42bef80bbaacea9649d09f63d76 + digest: sha256:59e11312666e1c5c155210ea335589b91daa0d70848521b390b93b1b1e8fb0ef requires: gpus: 1 topology: single-node diff --git a/models/hy3/hy3-1.0.0.yaml b/models/hy3/hy3-1.0.0.yaml index 281230f..a546c9e 100644 --- a/models/hy3/hy3-1.0.0.yaml +++ b/models/hy3/hy3-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 @@ -82,7 +82,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 diff --git a/models/kimi-k3/kimi-k3-1.0.0.yaml b/models/kimi-k3/kimi-k3-1.0.0.yaml index da6a9fe..b300096 100644 --- a/models/kimi-k3/kimi-k3-1.0.0.yaml +++ b/models/kimi-k3/kimi-k3-1.0.0.yaml @@ -13,7 +13,7 @@ variants: name: sglang version: 0.7.1 image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 8 diff --git a/models/kimi-k3/kimi-k3-1.0.1.yaml b/models/kimi-k3/kimi-k3-1.0.1.yaml index ad0f904..0d70265 100644 --- a/models/kimi-k3/kimi-k3-1.0.1.yaml +++ b/models/kimi-k3/kimi-k3-1.0.1.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: 0.7.1 image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 8 diff --git a/models/kimi-k3/kimi-k3-1.0.2.yaml b/models/kimi-k3/kimi-k3-1.0.2.yaml index 09aa58a..87d87be 100644 --- a/models/kimi-k3/kimi-k3-1.0.2.yaml +++ b/models/kimi-k3/kimi-k3-1.0.2.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.9" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 8 diff --git a/models/kimi-k3/kimi-k3-1.0.3.yaml b/models/kimi-k3/kimi-k3-1.0.3.yaml index 4ec4cb9..d695d5c 100644 --- a/models/kimi-k3/kimi-k3-1.0.3.yaml +++ b/models/kimi-k3/kimi-k3-1.0.3.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.9" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 8 diff --git a/models/kimi-k3/kimi-k3-1.0.4.yaml b/models/kimi-k3/kimi-k3-1.0.4.yaml index 43f8f69..99692e5 100644 --- a/models/kimi-k3/kimi-k3-1.0.4.yaml +++ b/models/kimi-k3/kimi-k3-1.0.4.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.9" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 8 @@ -83,9 +83,9 @@ variants: name: sglang version: ">=0.7.9" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 - digest: sha256:88e6445cca3f7f22af819152f0ea5ac94d8702583634ab2d1a2ea2a5b282f986 + digest: sha256:d6e7288627be8b02be88e4bba38e73f6d50e2826869f753c13a4c4385ab3eda9 requires: gpus: 8 topology: single-node diff --git a/models/kimi-k3/kimi-k3-1.0.5.yaml b/models/kimi-k3/kimi-k3-1.0.5.yaml index 930fb7d..1bb03da 100644 --- a/models/kimi-k3/kimi-k3-1.0.5.yaml +++ b/models/kimi-k3/kimi-k3-1.0.5.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.9" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 requires: gpus: 8 @@ -83,9 +83,9 @@ variants: name: sglang version: ">=0.7.9" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19 - digest: sha256:88e6445cca3f7f22af819152f0ea5ac94d8702583634ab2d1a2ea2a5b282f986 + digest: sha256:d6e7288627be8b02be88e4bba38e73f6d50e2826869f753c13a4c4385ab3eda9 requires: gpus: 8 topology: single-node diff --git a/models/laguna-s-2.1/laguna-s-2.1-1.0.0.yaml b/models/laguna-s-2.1/laguna-s-2.1-1.0.0.yaml index 0fc5b66..e63b7ba 100644 --- a/models/laguna-s-2.1/laguna-s-2.1-1.0.0.yaml +++ b/models/laguna-s-2.1/laguna-s-2.1-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 4 @@ -84,7 +84,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 diff --git a/models/ling-3.0-flash-vl/ling-3.0-flash-vl-1.0.0.yaml b/models/ling-3.0-flash-vl/ling-3.0-flash-vl-1.0.0.yaml index eae4d1b..345b560 100644 --- a/models/ling-3.0-flash-vl/ling-3.0-flash-vl-1.0.0.yaml +++ b/models/ling-3.0-flash-vl/ling-3.0-flash-vl-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-Ling-3.0-flash-VL requires: gpus: 8 @@ -84,7 +84,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-Ling-3.0-flash-VL requires: gpus: 8 diff --git a/models/ling-3.0-flash/ling-3.0-flash-1.0.0.yaml b/models/ling-3.0-flash/ling-3.0-flash-1.0.0.yaml index 306bb06..1627e2a 100644 --- a/models/ling-3.0-flash/ling-3.0-flash-1.0.0.yaml +++ b/models/ling-3.0-flash/ling-3.0-flash-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-Ling-3.0-flash-VL requires: gpus: 8 @@ -85,7 +85,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-Ling-3.0-flash-VL requires: gpus: 8 diff --git a/models/mimo-v2.5/mimo-v2.5-1.0.0.yaml b/models/mimo-v2.5/mimo-v2.5-1.0.0.yaml index 53da94e..4b79843 100644 --- a/models/mimo-v2.5/mimo-v2.5-1.0.0.yaml +++ b/models/mimo-v2.5/mimo-v2.5-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 @@ -86,7 +86,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 diff --git a/models/mistral-nemo-instruct/mistral-nemo-instruct-1.0.0.yaml b/models/mistral-nemo-instruct/mistral-nemo-instruct-1.0.0.yaml index caa2b3c..aa1ca52 100644 --- a/models/mistral-nemo-instruct/mistral-nemo-instruct-1.0.0.yaml +++ b/models/mistral-nemo-instruct/mistral-nemo-instruct-1.0.0.yaml @@ -11,9 +11,9 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 - digest: sha256:b7a3051b9e15939a5afc74de3564a72fd5b0b42bef80bbaacea9649d09f63d76 + digest: sha256:59e11312666e1c5c155210ea335589b91daa0d70848521b390b93b1b1e8fb0ef requires: gpus: 1 topology: single-node @@ -82,9 +82,9 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 - digest: sha256:b7a3051b9e15939a5afc74de3564a72fd5b0b42bef80bbaacea9649d09f63d76 + digest: sha256:59e11312666e1c5c155210ea335589b91daa0d70848521b390b93b1b1e8fb0ef requires: gpus: 1 topology: single-node diff --git a/models/mistral-small-4-119b-2603/mistral-small-4-119b-2603-1.0.0.yaml b/models/mistral-small-4-119b-2603/mistral-small-4-119b-2603-1.0.0.yaml index 7851aa7..0b807eb 100644 --- a/models/mistral-small-4-119b-2603/mistral-small-4-119b-2603-1.0.0.yaml +++ b/models/mistral-small-4-119b-2603/mistral-small-4-119b-2603-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 requires: gpus: 2 @@ -83,9 +83,9 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 - digest: sha256:b7a3051b9e15939a5afc74de3564a72fd5b0b42bef80bbaacea9649d09f63d76 + digest: sha256:59e11312666e1c5c155210ea335589b91daa0d70848521b390b93b1b1e8fb0ef requires: gpus: 2 topology: single-node diff --git a/models/nemotron-3.5-lightning-30b-a3b/nemotron-3.5-lightning-30b-a3b-1.0.0.yaml b/models/nemotron-3.5-lightning-30b-a3b/nemotron-3.5-lightning-30b-a3b-1.0.0.yaml index 9e6759f..34129cb 100644 --- a/models/nemotron-3.5-lightning-30b-a3b/nemotron-3.5-lightning-30b-a3b-1.0.0.yaml +++ b/models/nemotron-3.5-lightning-30b-a3b/nemotron-3.5-lightning-30b-a3b-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 1 @@ -87,7 +87,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 diff --git a/models/nex-n2.5-pro/nex-n2.5-pro-1.0.0.yaml b/models/nex-n2.5-pro/nex-n2.5-pro-1.0.0.yaml index c26f6dc..9eef152 100644 --- a/models/nex-n2.5-pro/nex-n2.5-pro-1.0.0.yaml +++ b/models/nex-n2.5-pro/nex-n2.5-pro-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 @@ -83,7 +83,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: dev-dsv41 requires: gpus: 8 diff --git a/models/qwen3.6-35b-a3b/qwen3.6-35b-a3b-1.0.0.yaml b/models/qwen3.6-35b-a3b/qwen3.6-35b-a3b-1.0.0.yaml index ff3c5ee..540acdb 100644 --- a/models/qwen3.6-35b-a3b/qwen3.6-35b-a3b-1.0.0.yaml +++ b/models/qwen3.6-35b-a3b/qwen3.6-35b-a3b-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: 0.7.1 image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.18-cu129 requires: gpus: 2 diff --git a/models/qwen3.6-35b-a3b/qwen3.6-35b-a3b-1.1.0.yaml b/models/qwen3.6-35b-a3b/qwen3.6-35b-a3b-1.1.0.yaml index 9b1ae37..15e7dfe 100644 --- a/models/qwen3.6-35b-a3b/qwen3.6-35b-a3b-1.1.0.yaml +++ b/models/qwen3.6-35b-a3b/qwen3.6-35b-a3b-1.1.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.18-cu129 requires: gpus: 2 @@ -89,7 +89,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 requires: gpus: 8 @@ -159,7 +159,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 requires: gpus: 8 diff --git a/models/qwen3.8-27b-fp8/qwen3.8-27b-fp8-1.0.0.yaml b/models/qwen3.8-27b-fp8/qwen3.8-27b-fp8-1.0.0.yaml index ca05c7b..90025e4 100644 --- a/models/qwen3.8-27b-fp8/qwen3.8-27b-fp8-1.0.0.yaml +++ b/models/qwen3.8-27b-fp8/qwen3.8-27b-fp8-1.0.0.yaml @@ -11,9 +11,9 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 - digest: sha256:b7a3051b9e15939a5afc74de3564a72fd5b0b42bef80bbaacea9649d09f63d76 + digest: sha256:59e11312666e1c5c155210ea335589b91daa0d70848521b390b93b1b1e8fb0ef requires: gpus: 1 topology: single-node @@ -81,9 +81,9 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: v0.5.19-cu129 - digest: sha256:b7a3051b9e15939a5afc74de3564a72fd5b0b42bef80bbaacea9649d09f63d76 + digest: sha256:59e11312666e1c5c155210ea335589b91daa0d70848521b390b93b1b1e8fb0ef requires: gpus: 2 topology: single-node diff --git a/models/qwen3.8-flash-next/qwen3.8-flash-next-1.0.0.yaml b/models/qwen3.8-flash-next/qwen3.8-flash-next-1.0.0.yaml index 1c60cc4..9ca9c6a 100644 --- a/models/qwen3.8-flash-next/qwen3.8-flash-next-1.0.0.yaml +++ b/models/qwen3.8-flash-next/qwen3.8-flash-next-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: qwen38flashnext requires: gpus: 8 @@ -83,7 +83,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: lmsysorg/sglang tag: qwen38flashnext requires: gpus: 8 From 7f24b7ec9390cd77679f68311c7af206a4c8da02 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Sat, 10 Oct 2026 18:48:35 +0800 Subject: [PATCH 2/7] simply validate check --- .github/workflows/lint.yml | 44 +++--------- README.md | 1 + hack/lib/helm-validation.js | 2 +- hack/lib/images.js | 132 ++++++++++++++++++++++++++++++++++++ hack/lib/images.test.js | 70 +++++++++++++++++++ hack/validate-helm.js | 9 ++- hack/validate-helm.test.js | 12 +++- hack/validate-images.js | 84 +++++++++++++++++++++++ package.json | 2 + 9 files changed, 314 insertions(+), 42 deletions(-) create mode 100644 hack/lib/images.js create mode 100644 hack/lib/images.test.js create mode 100644 hack/validate-images.js diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2655b43..7f7bad8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,9 +7,10 @@ permissions: contents: read jobs: - validate: - name: validate + lint: + name: lint runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v7 @@ -18,7 +19,7 @@ jobs: node-version: 24 cache: npm - - name: Install schema checker + - name: Install dependencies run: npm ci - name: Validate model schemas @@ -27,41 +28,16 @@ jobs: - name: Test the catalog library run: npm run test:lib - validate-helm: - name: validate-helm - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - # Compare the PR head, not GitHub's synthetic merge commit. - ref: ${{ github.event.pull_request.head.sha }} - - - uses: actions/setup-node@v7 - with: - node-version: 24 - cache: npm - - - name: Install validator dependencies - run: npm ci - - name: Test Helm validator run: npm run test:helm - name: Install pinned Helm and kubeconform run: npm run helm:install - - name: Validate changed model variants against latest stable charts - env: - BASE_REF: ${{ github.event.pull_request.base.sha }} - run: npm run validate:helm -- --base "$BASE_REF" --chart-mode latest + - name: Validate every model variant against latest stable charts + run: npm run validate:helm - - name: Save Helm validation logs and manifests - if: always() - uses: actions/upload-artifact@v4 - with: - name: helm-validation - path: artifacts/helm-validation/ - if-no-files-found: ignore - retention-days: 14 + # A catalog is public, so every image must be pullable by anyone. The + # runner is the outside world: no login, no VPN. + - name: Check every image is publicly pullable + run: npm run validate:images diff --git a/README.md b/README.md index e9ec695..193aa05 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ linking it. | `npm run test:lib` | tests for `hack/lib` | | `npm run helm:install` | pinned Helm and kubeconform, once | | `npm run validate:helm -- ` | render a version's variants against the charts (`--chart-mode declared` for what a deploy uses) | +| `npm run validate:images -- ` | every image is publicly pullable, no login (CI runs it on changed version files) | | `npm run build:site` | the site and `index.json` into `site/` | | `npm test` | `hack/validate.sh`: naming and layout checks (needs `yq`) | | `./hack/serve.sh` | rebuild `index.json` and serve the tree | diff --git a/hack/lib/helm-validation.js b/hack/lib/helm-validation.js index 8971117..7594d3c 100644 --- a/hack/lib/helm-validation.js +++ b/hack/lib/helm-validation.js @@ -35,7 +35,7 @@ function git(root, args) { function selectFiles(root, {all = false, base = "origin/HEAD", files = []} = {}) { const models = path.join(root, "models"); const versions = modelNames(models).flatMap((name) => versionFiles(name, models)); - if (all) return {files: versions, added: [], reason: "--all"}; + if (all) return {files: versions, added: [], reason: "every version file"}; if (files.length) { const selected = [...new Set(files.map((file) => path.relative(root, path.resolve(root, file)).split(path.sep).join("/")))].sort(); if (selected.some((file) => !versions.includes(file))) throw new Error("explicit files must be existing model version YAMLs under models/"); diff --git a/hack/lib/images.js b/hack/lib/images.js new file mode 100644 index 0000000..0a22fa2 --- /dev/null +++ b/hack/lib/images.js @@ -0,0 +1,132 @@ +"use strict"; +// Whether an image the catalog names can be pulled by anyone: no login, from +// the registry it names. A catalog is public; an image only one site can pull +// is a variant only that site can deploy. + +const dns = require("dns").promises; +const net = require("net"); +const path = require("path"); +const { command } = require("./helm-validation"); +const { modelNames, versionFiles } = require("./catalog"); + +const ACCEPT = [ + "application/vnd.oci.image.index.v1+json", + "application/vnd.docker.distribution.manifest.list.v2+json", + "application/vnd.oci.image.manifest.v1+json", + "application/vnd.docker.distribution.manifest.v2+json", +].join(", "); + +// A repository the way containerd reads it: no registry host means Docker Hub, +// and a bare name is an official image under library/. +function parseRepository(repository) { + const parts = repository.split("/"); + const first = parts[0]; + if (parts.length > 1 && (first.includes(".") || first.includes(":") || first === "localhost")) { + return { registry: first, host: first, name: parts.slice(1).join("/") }; + } + return { registry: "docker.io", host: "registry-1.docker.io", name: parts.length === 1 ? `library/${repository}` : repository }; +} + +// RFC 1918, loopback, link-local and unique-local: nobody outside reaches them. +function isPrivateAddress(ip) { + if (net.isIPv4(ip)) { + const [a, b] = ip.split(".").map(Number); + return a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254); + } + const v6 = ip.toLowerCase(); + return v6 === "::1" || /^f[cd]/.test(v6) || /^fe[89ab]/.test(v6); +} + +// The private address a registry host resolves to, when it resolves to +// nothing else; null for a host the internet can reach (or that does not +// resolve, which the request itself reports). A proxy or VPN that reaches it +// from here does not make it public. +async function privateNetwork(host, lookup = dns.lookup) { + const name = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, ""); + let addresses; + try { + addresses = net.isIP(name) ? [name] : (await lookup(name, { all: true })).map((a) => a.address); + } catch { + return null; + } + return addresses.length && addresses.every(isPrivateAddress) ? addresses[0] : null; +} + +function bearerChallenge(header) { + if (!/^\s*bearer\s/i.test(header || "")) return null; + const params = {}; + for (const [, k, v] of header.matchAll(/(\w+)="([^"]*)"/g)) params[k.toLowerCase()] = v; + return params.realm ? params : null; +} + +// Why ref (a tag or digest) cannot be pulled anonymously from repository, or +// null when it can. HEAD first: Docker Hub does not count it against pulls. +async function manifestProblem(repository, ref, { fetch = globalThis.fetch, lookup, tokens = new Map(), attempts = 3 } = {}) { + const { host, name } = parseRepository(repository); + const internal = await privateNetwork(host, lookup); + if (internal) return `${host} is on a private network (${internal})`; + const url = `https://${host}/v2/${name}/manifests/${ref}`; + const request = async (method) => { + const token = tokens.get(`${host}/${name}`); + return fetch(url, { method, headers: { Accept: ACCEPT, ...(token ? { Authorization: `Bearer ${token}` } : {}) } }); + }; + let lastError; + for (let i = 0; i < attempts; i++) { + try { + let r = await request("HEAD"); + if (r.status === 405) r = await request("GET"); + if (r.status === 401 && !tokens.has(`${host}/${name}`)) { + const challenge = bearerChallenge(r.headers.get("www-authenticate")); + if (!challenge) return "the registry requires a login"; + const q = new URLSearchParams({ scope: `repository:${name}:pull` }); + if (challenge.service) q.set("service", challenge.service); + const t = await fetch(`${challenge.realm}?${q}`); + const body = t.ok ? await t.json() : {}; + if (!(body.token || body.access_token)) return "the registry gives no anonymous pull token"; + tokens.set(`${host}/${name}`, body.token || body.access_token); + r = await request("HEAD"); + if (r.status === 405) r = await request("GET"); + } + if (r.ok) return null; + if (r.status === 401 || r.status === 403) return "not pullable without a login (private, or no such repository)"; + if (r.status === 404) return "no such manifest"; + if (r.status === 429 || r.status >= 500) throw new Error(`HTTP ${r.status}`); + return `HTTP ${r.status}`; + } catch (err) { + lastError = err; + await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** i)); + } + } + const reason = lastError && lastError.cause ? lastError.cause.code || lastError.cause.message : lastError && lastError.message; + return `could not reach ${host}: ${reason}`; +} + +// Why an image block cannot be pulled anonymously, or null when it can: its +// tag must resolve, and a pinned digest must exist in the same repository. +async function imageProblem(image, opts = {}) { + const ref = `${image.repository}:${image.tag}`; + const tag = await manifestProblem(image.repository, image.tag, opts); + if (tag) return `${ref}: ${tag}`; + if (image.digest) { + const pinned = await manifestProblem(image.repository, image.digest, opts); + if (pinned) return `${ref}@${image.digest}: ${pinned}`; + } + return null; +} + +// Version files a branch adds or changes since its merge-base with base; +// deleted ones have nothing left to pull. +function changedVersionFiles(root, base) { + const git = (args) => { + const result = command("git", args, { cwd: root }); + if (!result.ok) throw new Error(result.log); + return result.stdout; + }; + const mergeBase = git(["merge-base", base, "HEAD"]).trim(); + const changed = git(["diff", "--name-only", "-z", "--diff-filter=AMR", mergeBase, "HEAD"]).split("\0").filter(Boolean); + const models = path.join(root, "models"); + const versions = modelNames(models).flatMap((name) => versionFiles(name, models)); + return versions.filter((file) => changed.includes(file)); +} + +module.exports = { parseRepository, isPrivateAddress, manifestProblem, imageProblem, changedVersionFiles }; diff --git a/hack/lib/images.test.js b/hack/lib/images.test.js new file mode 100644 index 0000000..c1f8d54 --- /dev/null +++ b/hack/lib/images.test.js @@ -0,0 +1,70 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const { test } = require("node:test"); +const { parseRepository, isPrivateAddress, manifestProblem, imageProblem } = require("./images"); + +const publicDns = async () => [{ address: "52.45.125.121" }]; + +// A registry that wants an anonymous token, as Docker Hub and Harbor do. +function registry({ manifests = {}, token = "t", failures = 0 } = {}) { + const calls = []; + const fetch = async (url, init = {}) => { + calls.push(url); + if (failures-- > 0) throw new TypeError("fetch failed", { cause: { code: "ECONNRESET" } }); + if (url.startsWith("https://auth.example/token")) { + return { ok: Boolean(token), status: token ? 200 : 401, json: async () => (token ? { token } : {}) }; + } + const authed = init.headers && init.headers.Authorization === `Bearer ${token}`; + if (!authed) { + return { ok: false, status: 401, headers: new Headers({ "www-authenticate": 'Bearer realm="https://auth.example/token",service="reg"' }) }; + } + const ref = url.split("/manifests/")[1]; + return { ok: Boolean(manifests[ref]), status: manifests[ref] ? 200 : 404, headers: new Headers() }; + }; + return { fetch, calls }; +} + +test("repositories resolve the way containerd resolves them", () => { + assert.deepEqual(parseRepository("lmsysorg/sglang"), { registry: "docker.io", host: "registry-1.docker.io", name: "lmsysorg/sglang" }); + assert.deepEqual(parseRepository("nginx"), { registry: "docker.io", host: "registry-1.docker.io", name: "library/nginx" }); + assert.deepEqual(parseRepository("quay.io/ascend/vllm-ascend"), { registry: "quay.io", host: "quay.io", name: "ascend/vllm-ascend" }); + assert.deepEqual(parseRepository("localhost:5000/x"), { registry: "localhost:5000", host: "localhost:5000", name: "x" }); +}); + +test("private, loopback and link-local addresses are not public", () => { + for (const ip of ["10.1.2.3", "172.27.133.35", "192.168.0.1", "127.0.0.1", "169.254.1.1", "::1", "fd00::1", "fe80::1"]) { + assert.ok(isPrivateAddress(ip), ip); + } + for (const ip of ["172.32.0.1", "52.45.125.121", "100.63.226.34", "2600:1f18::1"]) assert.ok(!isPrivateAddress(ip), ip); +}); + +test("an image anyone can pull passes, after an anonymous token", async () => { + const { fetch } = registry({ manifests: { "v1": true, "sha256:abc": true } }); + assert.equal(await imageProblem({ repository: "org/app", tag: "v1", digest: "sha256:abc" }, { fetch, lookup: publicDns }), null); +}); + +test("a missing tag or pinned digest fails", async () => { + const { fetch } = registry({ manifests: { "v1": true } }); + assert.match(await imageProblem({ repository: "org/app", tag: "v2" }, { fetch, lookup: publicDns }), /org\/app:v2: no such manifest/); + assert.match(await imageProblem({ repository: "org/app", tag: "v1", digest: "sha256:abc" }, { fetch, lookup: publicDns }), /@sha256:abc: no such manifest/); +}); + +test("a registry that gives no anonymous token fails", async () => { + const { fetch } = registry({ token: null }); + assert.match(await manifestProblem("org/app", "v1", { fetch, lookup: publicDns }), /no anonymous pull token/); +}); + +test("a registry on a private network fails without being asked", async () => { + const { fetch, calls } = registry({ manifests: { "v1": true } }); + const lookup = async () => [{ address: "172.27.133.35" }]; + assert.match(await manifestProblem("harbor.example.io/team/app", "v1", { fetch, lookup }), /harbor\.example\.io is on a private network \(172\.27\.133\.35\)/); + assert.equal(calls.length, 0); +}); + +test("a dropped connection is retried", async () => { + const { fetch } = registry({ manifests: { "v1": true }, failures: 1 }); + assert.equal(await manifestProblem("org/app", "v1", { fetch, lookup: publicDns }), null); + const down = registry({ failures: 10 }); + assert.match(await manifestProblem("org/app", "v1", { fetch: down.fetch, lookup: publicDns, attempts: 2 }), /could not reach registry-1\.docker\.io: ECONNRESET/); +}); diff --git a/hack/validate-helm.js b/hack/validate-helm.js index 6d120c5..a592b3e 100644 --- a/hack/validate-helm.js +++ b/hack/validate-helm.js @@ -14,16 +14,15 @@ function options(args) { const opts = {files: [], chartMode: "latest", output: path.join(root, "artifacts/helm-validation")}; for (let i = 0; i < args.length; i++) { const arg = args[i]; - if (arg === "--all") opts.all = true; - else if (["--base", "--output", "--chart-mode"].includes(arg)) { + if (["--base", "--output", "--chart-mode"].includes(arg)) { if (!args[i + 1] || args[i + 1].startsWith("--")) throw new Error(`${arg}: missing argument`); opts[arg === "--chart-mode" ? "chartMode" : arg.slice(2)] = args[++i]; } else if (arg.startsWith("-")) throw new Error(`unknown option ${arg}`); else opts.files.push(arg); } - if ([Boolean(opts.all), Boolean(opts.base), Boolean(opts.files.length)].filter(Boolean).length > 1) { - throw new Error("choose one of --all, --base REF, or explicit files"); - } + if (opts.base && opts.files.length) throw new Error("choose --base REF or explicit files, not both"); + // Nothing narrows it: every version file. + opts.all = !opts.base && !opts.files.length; opts.output = path.resolve(root, opts.output); if (!["latest", "declared"].includes(opts.chartMode)) throw new Error("--chart-mode must be latest or declared"); return opts; diff --git a/hack/validate-helm.test.js b/hack/validate-helm.test.js index a3cb69e..a0c541f 100644 --- a/hack/validate-helm.test.js +++ b/hack/validate-helm.test.js @@ -147,11 +147,19 @@ for (const failure of ["crds", "resolution", "download", "schema", "lint", "temp } test("CLI rejects ambiguous selectors and incomplete arguments", () => { - assert.throws(() => options(["--all", "--base", "main"])); + assert.throws(() => options(["--base", "main", "models/m/m-1.0.0.yaml"])); + assert.throws(() => options(["--all"])); assert.throws(() => options(["--base"])); assert.throws(() => options(["--unknown"])); assert.throws(() => options(["--chart-mode", "invalid"])); assert.throws(() => options(["--chart-mode"])); assert.equal(options([]).chartMode, "latest"); - assert.equal(options(["--all", "--chart-mode", "declared"]).chartMode, "declared"); + assert.equal(options(["--chart-mode", "declared"]).chartMode, "declared"); +}); + +test("CLI checks every version file unless files or --base narrow it", () => { + assert.equal(options([]).all, true); + assert.equal(options(["--chart-mode", "declared"]).all, true); + assert.equal(options(["--base", "main"]).all, false); + assert.equal(options(["models/m/m-1.0.0.yaml"]).all, false); }); diff --git a/hack/validate-images.js b/hack/validate-images.js new file mode 100644 index 0000000..9b9744b --- /dev/null +++ b/hack/validate-images.js @@ -0,0 +1,84 @@ +#!/usr/bin/env node +// Check every image the catalog names can be pulled anonymously. +// +// npm run validate:images # the whole catalog +// npm run validate:images -- models/x/x-1.0.0.yaml # only these files +// npm run validate:images -- --base origin/master # only what a branch adds or changes +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const YAML = require("yaml"); +const {root, modelNames, versionFiles} = require("./lib/catalog"); +const {imageProblem, changedVersionFiles} = require("./lib/images"); + +function select(args) { + const files = []; + let base = null; + for (let i = 0; i < args.length; i++) { + if (args[i] === "--base") { + if (!args[i + 1] || args[i + 1].startsWith("--")) throw new Error("--base: missing argument"); + base = args[++i]; + } else if (args[i].startsWith("-")) throw new Error(`unknown option ${args[i]}`); + else files.push(path.relative(root, path.resolve(args[i])).split(path.sep).join("/")); + } + if (base && files.length) throw new Error("choose --base REF or explicit files, not both"); + if (base) return changedVersionFiles(root, base); + const versions = modelNames().flatMap((name) => versionFiles(name)); + if (!files.length) return versions; + const unknown = files.filter((f) => !versions.includes(f)); + if (unknown.length) throw new Error(`not a model version file: ${unknown.join(", ")}`); + return files; +} + +async function main() { + const files = select(process.argv.slice(2)); + if (files.length === 0) { + console.log("no version files to check"); + return 0; + } + + // Each distinct image once, however many variants share it. + const uses = new Map(); + for (const file of files) { + for (const v of YAML.parse(fs.readFileSync(path.join(root, file), "utf8")).variants ?? []) { + if (!v?.image?.repository) continue; + const key = JSON.stringify([v.image.repository, String(v.image.tag ?? "latest"), v.image.digest ?? ""]); + if (!uses.has(key)) uses.set(key, []); + uses.get(key).push({file, id: v.id}); + } + } + + const tokens = new Map(); + const problems = new Map(); + const keys = [...uses.keys()]; + // A few at a time: enough to be quick, few enough not to trip rate limits. + for (let i = 0; i < keys.length; i += 4) { + await Promise.all(keys.slice(i, i + 4).map(async (key) => { + const [repository, tag, digest] = JSON.parse(key); + const problem = await imageProblem({repository, tag, digest}, {tokens}); + if (problem) problems.set(key, problem); + })); + } + + const byFile = new Map(); + for (const [key, problem] of problems) { + for (const {file, id} of uses.get(key)) { + if (!byFile.has(file)) byFile.set(file, []); + byFile.get(file).push(`${id}: ${problem}`); + } + } + [...byFile.keys()].sort().forEach((file, i) => { + if (i) console.error(""); + console.error(file); + for (const line of byFile.get(file)) console.error(` ${line}`); + }); + const ok = keys.length - problems.size; + console.log(`${ok} of ${keys.length} images publicly pullable, in ${files.length} version file${files.length === 1 ? "" : "s"}`); + return problems.size ? 1 : 0; +} + +main().then((code) => process.exit(code), (err) => { + console.error(err.message); + process.exit(2); +}); diff --git a/package.json b/package.json index 8d28861..7127eb5 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "main": "index.js", "scripts": { "test": "./hack/validate.sh", + "validate": "npm run validate:schema && npm run validate:helm && npm run validate:images", "validate:schema": "node hack/validate-schema.js", "validate:helm": "node hack/validate-helm.js", + "validate:images": "node --use-env-proxy hack/validate-images.js", "helm:install": "node hack/install-helm-tools.js", "helm:build-crds": "node hack/build-crds.js", "test:helm": "node --test hack/lib/helm-validation.test.js hack/lib/crd-schema.test.js hack/validate-helm.test.js", From 12a1ed69193328778e56c3cb4f70db941e0d66f8 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Sat, 10 Oct 2026 19:27:25 +0800 Subject: [PATCH 3/7] validate html report images --- README.md | 22 ++++++++++------ hack/lib/images.js | 56 ++++++++++++++++++++++++++++++++++++++--- hack/lib/images.test.js | 41 +++++++++++++++++++++++++++++- hack/validate-images.js | 44 +++++++++++++++++++++++++------- 4 files changed, 142 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 193aa05..52d902f 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ models// ```sh npm run validate:schema npm run helm:install && npm run validate:helm -- models/my-model/my-model-1.0.0.yaml + npm run validate:images -- models/my-model/my-model-1.0.0.yaml PUBLISHED_INDEX=none npm run build:site # open site/index.html ``` @@ -83,6 +84,7 @@ What a variant needs to know: | `requires.gpus` | per pod; `nodes` + `topology: lws` for multi-node, with `values.lws.size` equal to `nodes` | | `requires.vendor` | `nvidia` (default), `ascend`, `cambricon`, `hygon` or `amd`; one vendor per variant | | `requires.gpuProduct` | canonical card names, any one matches: the `nvidia.com/gpu.product` value for NVIDIA (`NVIDIA-H100-80GB-HBM3`), `-` otherwise (`Ascend-910B3`). swiss maps names to each cluster's node labels | +| `image` | on a public registry, pullable without a login | | `chart.version` | exact (`0.8.0`) or a [semver range](https://github.com/Masterminds/semver#checking-version-constraints), quoted (`">=0.8.0"`, `"^0.8.0"`) | | `values` | chart values. Site-specific keys (namespace, host paths, registry, route ConfigMaps) are refused by the schema | @@ -104,7 +106,8 @@ tuning: A report must be a complete HTML document whose embedded JSON parses. `validate:schema` fails on a missing, empty or invalid one; the site skips -linking it. +linking it. The images it records must be the same builds (name and tag) as the +two variants', which `validate:images` checks. ## Development @@ -113,18 +116,23 @@ linking it. | `npm run validate:schema` | schemas, tuning references, report HTML (pre-commit and CI) | | `npm run test:lib` | tests for `hack/lib` | | `npm run helm:install` | pinned Helm and kubeconform, once | -| `npm run validate:helm -- ` | render a version's variants against the charts (`--chart-mode declared` for what a deploy uses) | -| `npm run validate:images -- ` | every image is publicly pullable, no login (CI runs it on changed version files) | +| `npm run validate:helm` | render every variant against the charts (`--chart-mode declared` for what a deploy uses) | +| `npm run validate:images` | every image is publicly pullable without a login, and every perf report measured its variants' builds | +| `npm run validate` | `validate:schema`, `validate:helm` and `validate:images` | | `npm run build:site` | the site and `index.json` into `site/` | | `npm test` | `hack/validate.sh`: naming and layout checks (needs `yq`) | | `./hack/serve.sh` | rebuild `index.json` and serve the tree | +`validate:helm` and `validate:images` check every version file; pass files, or +`--base REF` for what a branch changes, to narrow them. + Needs Node.js 24. -- **CI.** `lints` runs on pull requests: `validate:schema`, `test:lib`, and - `validate-helm` on changed version files against the latest stable charts - ([Helm validation](schema/crds/README.md)). `pages` builds the site on pull - requests and deploys it on push to `master`. +- **CI.** `lints` runs on pull requests: `validate:schema`, `test:lib`, + `test:helm`, then `validate:helm` against the latest stable charts + ([Helm validation](schema/crds/README.md)) and `validate:images`, both on every + version file. `pages` builds the site on pull requests and deploys it on push + to `master`. - **`index.json` is generated, not committed.** It is a pure function of `models/` and `catalog.yaml`, with no timestamp, so the same tree gives the same catalog ref in swiss. diff --git a/hack/lib/images.js b/hack/lib/images.js index 0a22fa2..dc6936c 100644 --- a/hack/lib/images.js +++ b/hack/lib/images.js @@ -114,8 +114,55 @@ async function imageProblem(image, opts = {}) { return null; } -// Version files a branch adds or changes since its merge-base with base; -// deleted ones have nothing left to pull. +// The images a perf report's benchmark ran, from the JSON it embeds: the +// baseline's, and each tuning attempt's. Empty sets when it records none. +function reportImages(html) { + const out = { baseline: new Set(), attempts: new Set() }; + const block = /]*type="application\/json"[^>]*>([\s\S]*?)<\/script\s*>/i.exec(html); + let data; + try { + data = JSON.parse(block[1]); + } catch { + return out; + } + const image = (run) => run && run.launch && run.launch.config && run.launch.config.image; + for (const r of data.reports || []) { + const c = r && r.comparison; + if (!c) continue; + if (image(c.baseline)) out.baseline.add(image(c.baseline)); + for (const a of c.attempts || []) if (image(a)) out.attempts.add(image(a)); + } + return out; +} + +// A build, wherever it is pulled from: the image's own name, which a registry +// mirror keeps (swiss's does), and its tag. +function buildOf(ref) { + const name = String(ref).split("@")[0]; + const slash = name.lastIndexOf("/"); + const colon = name.lastIndexOf(":"); + const repo = colon > slash ? name.slice(0, colon) : name; + return `${repo.split("/").pop()}:${colon > slash ? name.slice(colon + 1) : "latest"}`; +} + +// Why a tuned pair's report did not measure the builds its two variants run. +// The tuned build need only be among the attempts: a tuning run may try more +// than one, and its best overall can be the baseline. +function reportMismatches(html, { baseline, optimized }) { + const ran = reportImages(html); + const out = []; + for (const [role, want, got] of [["baseline", baseline, ran.baseline], ["tuned", optimized, ran.attempts]]) { + if (!got.size) out.push(`records no ${role} image`); + else if (![...got].some((x) => buildOf(x) === buildOf(want))) { + out.push(`the ${role} variant runs ${want}, but the report measured ${[...got].join(", ")}`); + } + } + return out; +} + +// Version files a branch adds or changes since its merge-base with base, and +// every version of a model whose metadata or perf report changed. Deleted +// files have nothing left to check. function changedVersionFiles(root, base) { const git = (args) => { const result = command("git", args, { cwd: root }); @@ -124,9 +171,10 @@ function changedVersionFiles(root, base) { }; const mergeBase = git(["merge-base", base, "HEAD"]).trim(); const changed = git(["diff", "--name-only", "-z", "--diff-filter=AMR", mergeBase, "HEAD"]).split("\0").filter(Boolean); + const touched = new Set(changed.filter((f) => /^models\/[^/]+\/(metadata\.yaml|[^/]+\.html)$/.test(f)).map((f) => f.split("/")[1])); const models = path.join(root, "models"); const versions = modelNames(models).flatMap((name) => versionFiles(name, models)); - return versions.filter((file) => changed.includes(file)); + return versions.filter((file) => changed.includes(file) || touched.has(file.split("/")[1])); } -module.exports = { parseRepository, isPrivateAddress, manifestProblem, imageProblem, changedVersionFiles }; +module.exports = { parseRepository, isPrivateAddress, manifestProblem, imageProblem, reportImages, buildOf, reportMismatches, changedVersionFiles }; diff --git a/hack/lib/images.test.js b/hack/lib/images.test.js index c1f8d54..ec7d9c8 100644 --- a/hack/lib/images.test.js +++ b/hack/lib/images.test.js @@ -2,7 +2,7 @@ const assert = require("node:assert/strict"); const { test } = require("node:test"); -const { parseRepository, isPrivateAddress, manifestProblem, imageProblem } = require("./images"); +const { parseRepository, isPrivateAddress, manifestProblem, imageProblem, buildOf, reportMismatches } = require("./images"); const publicDns = async () => [{ address: "52.45.125.121" }]; @@ -68,3 +68,42 @@ test("a dropped connection is retried", async () => { const down = registry({ failures: 10 }); assert.match(await manifestProblem("org/app", "v1", { fetch: down.fetch, lookup: publicDns, attempts: 2 }), /could not reach registry-1\.docker\.io: ECONNRESET/); }); + +// A perf report as AutoTune writes it: each language carries the comparison. +const report = (baseline, ...attempts) => { + const run = (image) => ({ launch: { config: { image } } }); + const comparison = { baseline: run(baseline), attempts: attempts.map(run) }; + const data = { reports: [{ lang: "en", comparison }, { lang: "zh", comparison }] }; + return ``; +}; + +test("a build is the image's own name and tag, wherever it is pulled from", () => { + assert.equal(buildOf("harbor.example.io/team/sglang:v0.5.19"), "sglang:v0.5.19"); + assert.equal(buildOf("lmsysorg/sglang:v0.5.19"), "sglang:v0.5.19"); + assert.equal(buildOf("harbor.example.io/team/vllm/vllm-openai:v0.30.0"), buildOf("vllm/vllm-openai:v0.30.0")); + assert.equal(buildOf("quay.io/ascend/vllm-ascend:v1@sha256:ff"), "vllm-ascend:v1"); + assert.equal(buildOf("localhost:5000/app"), "app:latest"); +}); + +test("a report that measured its variants' builds passes, from any registry", () => { + const html = report("harbor.example.io/team/sglang:v0.5.19", "harbor.example.io/team/sglang:v0.5.19"); + assert.deepEqual(reportMismatches(html, { baseline: "lmsysorg/sglang:v0.5.19", optimized: "lmsysorg/sglang:v0.5.19" }), []); +}); + +test("a report that measured another build fails, per side", () => { + const html = report("x/sglang:v0.5.18", "x/sglang:v0.5.19"); + const got = reportMismatches(html, { baseline: "lmsysorg/sglang:v0.5.19", optimized: "lmsysorg/sglang:v0.5.20" }); + assert.equal(got.length, 2); + assert.match(got[0], /baseline variant runs lmsysorg\/sglang:v0\.5\.19, but the report measured x\/sglang:v0\.5\.18/); + assert.match(got[1], /tuned variant runs lmsysorg\/sglang:v0\.5\.20/); +}); + +test("the tuned build need only be among the attempts", () => { + const html = report("x/sglang:a", "x/sglang:b", "x/sglang:c"); + assert.deepEqual(reportMismatches(html, { baseline: "y/sglang:a", optimized: "y/sglang:c" }), []); +}); + +test("a report that records no images says so", () => { + const got = reportMismatches("", { baseline: "a:1", optimized: "a:1" }); + assert.deepEqual(got, ["records no baseline image", "records no tuned image"]); +}); diff --git a/hack/validate-images.js b/hack/validate-images.js index 9b9744b..47b194d 100644 --- a/hack/validate-images.js +++ b/hack/validate-images.js @@ -1,5 +1,6 @@ #!/usr/bin/env node -// Check every image the catalog names can be pulled anonymously. +// Check every image the catalog names can be pulled anonymously, and that each +// tuned pair's perf report measured the builds its two variants run. // // npm run validate:images # the whole catalog // npm run validate:images -- models/x/x-1.0.0.yaml # only these files @@ -10,7 +11,7 @@ const fs = require("node:fs"); const path = require("node:path"); const YAML = require("yaml"); const {root, modelNames, versionFiles} = require("./lib/catalog"); -const {imageProblem, changedVersionFiles} = require("./lib/images"); +const {imageProblem, reportMismatches, changedVersionFiles} = require("./lib/images"); function select(args) { const files = []; @@ -38,10 +39,12 @@ async function main() { return 0; } + const docs = new Map(files.map((file) => [file, YAML.parse(fs.readFileSync(path.join(root, file), "utf8"))])); + // Each distinct image once, however many variants share it. const uses = new Map(); - for (const file of files) { - for (const v of YAML.parse(fs.readFileSync(path.join(root, file), "utf8")).variants ?? []) { + for (const [file, doc] of docs) { + for (const v of doc.variants ?? []) { if (!v?.image?.repository) continue; const key = JSON.stringify([v.image.repository, String(v.image.tag ?? "latest"), v.image.digest ?? ""]); if (!uses.has(key)) uses.set(key, []); @@ -62,11 +65,32 @@ async function main() { } const byFile = new Map(); + const note = (file, line) => { + if (!byFile.has(file)) byFile.set(file, []); + byFile.get(file).push(line); + }; for (const [key, problem] of problems) { - for (const {file, id} of uses.get(key)) { - if (!byFile.has(file)) byFile.set(file, []); - byFile.get(file).push(`${id}: ${problem}`); - } + for (const {file, id} of uses.get(key)) note(file, `${id}: ${problem}`); + } + + // Tuned pairs of the selected versions. A missing report or variant is + // validate:schema's to report. + let reports = 0; + for (const name of new Set([...docs.keys()].map((file) => file.split("/")[1]))) { + const meta = `models/${name}/metadata.yaml`; + const tuning = YAML.parse(fs.readFileSync(path.join(root, meta), "utf8"))?.tuning ?? []; + tuning.forEach((t, i) => { + const doc = [...docs].find(([file, d]) => file.split("/")[1] === name && String(d?.version) === String(t?.version))?.[1]; + const variant = (id) => doc?.variants?.find((v) => v?.id === id); + const [b, o] = [variant(t?.baseline), variant(t?.optimized)]; + const html = path.join(root, "models", name, String(t?.report ?? "")); + if (!t?.report || !b?.image || !o?.image || !fs.existsSync(html)) return; + reports++; + const image = (v) => `${v.image.repository}:${v.image.tag ?? "latest"}`; + for (const m of reportMismatches(fs.readFileSync(html, "utf8"), {baseline: image(b), optimized: image(o)})) { + note(meta, `tuning[${i}] (${t.report}): ${m}`); + } + }); } [...byFile.keys()].sort().forEach((file, i) => { if (i) console.error(""); @@ -74,8 +98,10 @@ async function main() { for (const line of byFile.get(file)) console.error(` ${line}`); }); const ok = keys.length - problems.size; + const bad = [...byFile.keys()].filter((f) => f.endsWith("metadata.yaml")).reduce((n, f) => n + byFile.get(f).length, 0); console.log(`${ok} of ${keys.length} images publicly pullable, in ${files.length} version file${files.length === 1 ? "" : "s"}`); - return problems.size ? 1 : 0; + console.log(`${reports} perf report${reports === 1 ? "" : "s"} checked against their variants' builds, ${bad} mismatch${bad === 1 ? "" : "es"}`); + return problems.size || bad ? 1 : 0; } main().then((code) => process.exit(code), (err) => { From 7a191119ee95dd366ce579916447dcdbea3b04f0 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Sat, 10 Oct 2026 19:27:40 +0800 Subject: [PATCH 4/7] update image ref in html --- .../deepseek-v4-flash-0731-h100-report.html | 2 +- .../deepseek-v4-flash-h100-report.html | 2 +- .../deepseek-v4.1-flash-h100-report.html | 2 +- .../gemma-4-26b-a4b-it-h100-report.html | 2 +- models/gemma-4-31b-it/gemma-4-31b-h100-report.html | 2 +- models/glm5.3/glm5.3-1.0.3-b300-report.html | 2 +- models/gpt-oss-120b/gpt-oss-120b-h100-report.html | 2 +- models/gpt-oss-20b/gpt-oss-20b-h100-report.html | 2 +- models/hy3/hy3-h100-report.html | 2 +- models/kimi-k3/kimi-k3-1.0.4-b300-report.html | 2 +- models/laguna-s-2.1/laguna-s-2.1-h100-report.html | 2 +- .../ling-3.0-flash-vl-h100-report.html | 2 +- models/ling-3.0-flash/ling-3.0-flash-h100-report.html | 2 +- models/mimo-v2.5/mimo-v2.5-h100-report.html | 2 +- .../mistral-nemo-instruct-h100-report.html | 2 +- .../mistral-small-4-119b-2603-h100-report.html | 2 +- .../nemotron-3.5-lightning-30b-a3b-h100-report.html | 2 +- models/nex-n2.5-pro/nex-n2.5-pro-h100-report.html | 2 +- .../qwen3.6-35b-a3b/qwen3.6-35b-a3b-h100-report.html | 2 +- .../qwen3.8-27b-fp8/qwen3.8-27b-fp8-h100-report.html | 2 +- .../qwen3.8-flash-next-h100-report.html | 2 +- schema/crds/README.md | 11 +++++------ 22 files changed, 26 insertions(+), 27 deletions(-) diff --git a/models/deepseek-v4-flash-0731/deepseek-v4-flash-0731-h100-report.html b/models/deepseek-v4-flash-0731/deepseek-v4-flash-0731-h100-report.html index 9cf6132..7ac994a 100644 --- a/models/deepseek-v4-flash-0731/deepseek-v4-flash-0731-h100-report.html +++ b/models/deepseek-v4-flash-0731/deepseek-v4-flash-0731-h100-report.html @@ -7,7 +7,7 @@
- + + + + + + + + + + + + + + + + + + + + + `; }; -test("a build is the image's own name and tag, wherever it is pulled from", () => { - assert.equal(buildOf("harbor.example.io/team/sglang:v0.5.19"), "sglang:v0.5.19"); - assert.equal(buildOf("lmsysorg/sglang:v0.5.19"), "sglang:v0.5.19"); - assert.equal(buildOf("harbor.example.io/team/vllm/vllm-openai:v0.30.0"), buildOf("vllm/vllm-openai:v0.30.0")); - assert.equal(buildOf("quay.io/ascend/vllm-ascend:v1@sha256:ff"), "vllm-ascend:v1"); - assert.equal(buildOf("localhost:5000/app"), "app:latest"); +test("an image is its registry, repository and tag", () => { + assert.equal(imageRef("lmsysorg/sglang:v0.5.19"), "docker.io/lmsysorg/sglang:v0.5.19"); + assert.equal(imageRef("docker.io/library/nginx"), imageRef("nginx:latest")); + assert.equal(imageRef("quay.io/ascend/vllm-ascend:v1@sha256:ff"), "quay.io/ascend/vllm-ascend:v1"); + assert.equal(imageRef("localhost:5000/app"), "localhost:5000/app:latest"); + assert.notEqual(imageRef("registry.example.io/team/sglang:v0.5.19"), imageRef("lmsysorg/sglang:v0.5.19")); }); -test("a report that measured its variants' builds passes, from any registry", () => { - const html = report("harbor.example.io/team/sglang:v0.5.19", "harbor.example.io/team/sglang:v0.5.19"); +test("a report that measured its variants' images passes", () => { + const html = report("docker.io/lmsysorg/sglang:v0.5.19", "lmsysorg/sglang:v0.5.19"); assert.deepEqual(reportMismatches(html, { baseline: "lmsysorg/sglang:v0.5.19", optimized: "lmsysorg/sglang:v0.5.19" }), []); }); +test("a report that measured the same tag from another registry fails", () => { + const html = report("registry.example.io/team/sglang:glm", "registry.example.io/team/sglang:glm"); + assert.deepEqual(reportMismatches(html, { baseline: "org/sglang:glm", optimized: "org/sglang:glm" }), [ + "the baseline variant runs org/sglang:glm, but the report measured registry.example.io/team/sglang:glm", + "the tuned variant runs org/sglang:glm, but the report measured registry.example.io/team/sglang:glm", + ]); +}); + test("a report that measured another build fails, per side", () => { const html = report("x/sglang:v0.5.18", "x/sglang:v0.5.19"); const got = reportMismatches(html, { baseline: "lmsysorg/sglang:v0.5.19", optimized: "lmsysorg/sglang:v0.5.20" }); @@ -98,9 +141,9 @@ test("a report that measured another build fails, per side", () => { assert.match(got[1], /tuned variant runs lmsysorg\/sglang:v0\.5\.20/); }); -test("the tuned build need only be among the attempts", () => { +test("the tuned image need only be among the attempts", () => { const html = report("x/sglang:a", "x/sglang:b", "x/sglang:c"); - assert.deepEqual(reportMismatches(html, { baseline: "y/sglang:a", optimized: "y/sglang:c" }), []); + assert.deepEqual(reportMismatches(html, { baseline: "x/sglang:a", optimized: "x/sglang:c" }), []); }); test("a report that records no images says so", () => { diff --git a/hack/lib/report.js b/hack/lib/report.js index 6e3586b..0db5e95 100644 --- a/hack/lib/report.js +++ b/hack/lib/report.js @@ -3,7 +3,7 @@ // and validate:schema fails on any other, so the two cannot disagree. const fs = require("fs"); -const { HtmlValidate, StaticConfigLoader } = require("html-validate"); +const { HtmlValidate, Parser, StaticConfigLoader } = require("html-validate"); // Conformance to the HTML standard for a whole document, without style rules: // a cut-off file fails as an unclosed element, a non-HTML one on its doctype. @@ -11,9 +11,17 @@ const validator = new HtmlValidate( new StaticConfigLoader({ extends: ["html-validate:standard", "html-validate:document"] }) ); -// A report renders from the JSON it embeds, so a page that is valid HTML but -// carries broken JSON still shows nothing. -const JSON_BLOCK = /]*\btype\s*=\s*["']?application\/(?:ld\+)?json["']?[^>]*>([\s\S]*?)<\/script\s*>/gi; +// The JSON data blocks of an HTML document, '>\n` + + `\n` + + `\n` + + `\n` + + `\n\n`; + assert.deepEqual(jsonBlocks(html), [ + { text: '{"a": 1}', line: 4 }, + { text: '{"b": 2}', line: 5 }, + { text: '{"c": 3}', line: 6 }, + ]); +}); diff --git a/hack/validate-images.js b/hack/validate-images.js index 47b194d..5e1ec13 100644 --- a/hack/validate-images.js +++ b/hack/validate-images.js @@ -1,39 +1,49 @@ #!/usr/bin/env node -// Check every image the catalog names can be pulled anonymously, and that each -// tuned pair's perf report measured the builds its two variants run. +// Check every image the catalog names can be pulled anonymously, by tag and by +// any digest it pins, and that each tuned pair's perf report measured the +// builds its two variants run. Each image is logged as ok, warning (its tag +// has moved off a pinned digest that is still pullable) or error. // // npm run validate:images # the whole catalog // npm run validate:images -- models/x/x-1.0.0.yaml # only these files // npm run validate:images -- --base origin/master # only what a branch adds or changes +// npm run validate:images -- --match-digest # and each pinned digest is what its tag resolves to now "use strict"; const fs = require("node:fs"); +const http = require("node:http"); const path = require("node:path"); const YAML = require("yaml"); -const {root, modelNames, versionFiles} = require("./lib/catalog"); -const {imageProblem, reportMismatches, changedVersionFiles} = require("./lib/images"); +const { root, modelNames, versionFiles } = require("./lib/catalog"); +const { checkImage, reportMismatches, changedVersionFiles } = require("./lib/images"); -function select(args) { +// Reach registries through HTTP(S)_PROXY / NO_PROXY when they are set, as +// --use-env-proxy would, without the caller having to pass it. +http.setGlobalProxyFromEnv(); + +function parse(args) { const files = []; let base = null; + let matchDigest = false; for (let i = 0; i < args.length; i++) { if (args[i] === "--base") { if (!args[i + 1] || args[i + 1].startsWith("--")) throw new Error("--base: missing argument"); base = args[++i]; - } else if (args[i].startsWith("-")) throw new Error(`unknown option ${args[i]}`); + } else if (args[i] === "--match-digest") matchDigest = true; + else if (args[i].startsWith("-")) throw new Error(`unknown option ${args[i]}`); else files.push(path.relative(root, path.resolve(args[i])).split(path.sep).join("/")); } if (base && files.length) throw new Error("choose --base REF or explicit files, not both"); - if (base) return changedVersionFiles(root, base); + if (base) return { files: changedVersionFiles(root, base), matchDigest }; const versions = modelNames().flatMap((name) => versionFiles(name)); - if (!files.length) return versions; + if (!files.length) return { files: versions, matchDigest }; const unknown = files.filter((f) => !versions.includes(f)); if (unknown.length) throw new Error(`not a model version file: ${unknown.join(", ")}`); - return files; + return { files, matchDigest }; } async function main() { - const files = select(process.argv.slice(2)); + const { files, matchDigest } = parse(process.argv.slice(2)); if (files.length === 0) { console.log("no version files to check"); return 0; @@ -48,20 +58,21 @@ async function main() { if (!v?.image?.repository) continue; const key = JSON.stringify([v.image.repository, String(v.image.tag ?? "latest"), v.image.digest ?? ""]); if (!uses.has(key)) uses.set(key, []); - uses.get(key).push({file, id: v.id}); + uses.get(key).push({ file, id: v.id }); } } const tokens = new Map(); - const problems = new Map(); + const results = new Map(); const keys = [...uses.keys()]; // A few at a time: enough to be quick, few enough not to trip rate limits. for (let i = 0; i < keys.length; i += 4) { - await Promise.all(keys.slice(i, i + 4).map(async (key) => { + const batch = keys.slice(i, i + 4); + await Promise.all(batch.map(async (key) => { const [repository, tag, digest] = JSON.parse(key); - const problem = await imageProblem({repository, tag, digest}, {tokens}); - if (problem) problems.set(key, problem); + results.set(key, await checkImage({ repository, tag, digest }, { tokens, matchDigest })); })); + for (const key of batch) console.log(`${results.get(key).level.padEnd(7)} ${results.get(key).message}`); } const byFile = new Map(); @@ -69,8 +80,8 @@ async function main() { if (!byFile.has(file)) byFile.set(file, []); byFile.get(file).push(line); }; - for (const [key, problem] of problems) { - for (const {file, id} of uses.get(key)) note(file, `${id}: ${problem}`); + for (const [key, { level, message }] of results) { + if (level === "error") for (const { file, id } of uses.get(key)) note(file, `${id}: ${message}`); } // Tuned pairs of the selected versions. A missing report or variant is @@ -87,7 +98,7 @@ async function main() { if (!t?.report || !b?.image || !o?.image || !fs.existsSync(html)) return; reports++; const image = (v) => `${v.image.repository}:${v.image.tag ?? "latest"}`; - for (const m of reportMismatches(fs.readFileSync(html, "utf8"), {baseline: image(b), optimized: image(o)})) { + for (const m of reportMismatches(fs.readFileSync(html, "utf8"), { baseline: image(b), optimized: image(o) })) { note(meta, `tuning[${i}] (${t.report}): ${m}`); } }); @@ -97,11 +108,13 @@ async function main() { console.error(file); for (const line of byFile.get(file)) console.error(` ${line}`); }); - const ok = keys.length - problems.size; + const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`; + const count = (level) => [...results.values()].filter((r) => r.level === level).length; + const errors = count("error"); const bad = [...byFile.keys()].filter((f) => f.endsWith("metadata.yaml")).reduce((n, f) => n + byFile.get(f).length, 0); - console.log(`${ok} of ${keys.length} images publicly pullable, in ${files.length} version file${files.length === 1 ? "" : "s"}`); - console.log(`${reports} perf report${reports === 1 ? "" : "s"} checked against their variants' builds, ${bad} mismatch${bad === 1 ? "" : "es"}`); - return problems.size || bad ? 1 : 0; + console.log(`${plural(keys.length, "image")} in ${plural(files.length, "version file")}: ${count("ok")} ok, ${plural(count("warning"), "warning")}, ${plural(errors, "error")}`); + console.log(`${reports} perf report${reports === 1 ? "" : "s"} checked against their variants' images, ${bad} mismatch${bad === 1 ? "" : "es"}`); + return errors || bad ? 1 : 0; } main().then((code) => process.exit(code), (err) => { diff --git a/models/glm-5.3-flash/glm-5.3-flash-1.0.0.yaml b/models/glm-5.3-flash/glm-5.3-flash-1.0.0.yaml index 535ab66..483be1c 100644 --- a/models/glm-5.3-flash/glm-5.3-flash-1.0.0.yaml +++ b/models/glm-5.3-flash/glm-5.3-flash-1.0.0.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: 4pdosc/sglang tag: glm-5.3-flash requires: gpus: 8 @@ -83,7 +83,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: 4pdosc/sglang tag: glm-5.3-flash requires: gpus: 8 diff --git a/models/glm-5.3-flash/glm-5.3-flash-1.0.1.yaml b/models/glm-5.3-flash/glm-5.3-flash-1.0.1.yaml index 9e33b74..5a9596d 100644 --- a/models/glm-5.3-flash/glm-5.3-flash-1.0.1.yaml +++ b/models/glm-5.3-flash/glm-5.3-flash-1.0.1.yaml @@ -11,7 +11,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: 4pdosc/sglang tag: glm-5.3-flash requires: gpus: 8 @@ -83,7 +83,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: 4pdosc/sglang tag: glm-5.3-flash requires: gpus: 8 @@ -157,7 +157,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: 4pdosc/sglang tag: glm-5.3-flash requires: gpus: 8 @@ -230,7 +230,7 @@ variants: name: sglang version: ">=0.7.1" image: - repository: harbor.4pd.io/hardcore-tech/sglang + repository: 4pdosc/sglang tag: glm-5.3-flash digest: sha256:127b3812d892ee1a4cce74475e1e6b645e74de5c67204b34615bc6279c84a539 requires: diff --git a/models/glm-5.3-flash/glm-5.3-flash-b300-report.html b/models/glm-5.3-flash/glm-5.3-flash-b300-report.html index 7f413dd..4946e1c 100644 --- a/models/glm-5.3-flash/glm-5.3-flash-b300-report.html +++ b/models/glm-5.3-flash/glm-5.3-flash-b300-report.html @@ -7,7 +7,7 @@
- + + `; + return ``; }; test("an image is its registry, repository and tag", () => { @@ -150,3 +150,11 @@ test("a report that records no images says so", () => { const got = reportMismatches("", { baseline: "a:1", optimized: "a:1" }); assert.deepEqual(got, ["records no baseline image", "records no tuned image"]); }); + +test("only the block the report renders from is read", () => { + const runs = { baseline: "x/sglang:a", optimized: "x/sglang:b" }; + const decoy = report("y/sglang:a", "y/sglang:b").replace("autotune-report-data", "other-data"); + const decoyBlock = decoy.match(//)[0]; + assert.deepEqual(reportMismatches(report("x/sglang:a", "x/sglang:b").replace("", `${decoyBlock}`), runs), []); + assert.deepEqual(reportMismatches(decoy, runs), ["records no baseline image", "records no tuned image"]); +}); diff --git a/hack/lib/report.js b/hack/lib/report.js index 0db5e95..ca414fa 100644 --- a/hack/lib/report.js +++ b/hack/lib/report.js @@ -12,7 +12,7 @@ const validator = new HtmlValidate( ); // The JSON data blocks of an HTML document, '>\n` + - `\n` + + `\n` + `\n` + `\n` + `\n\n`; assert.deepEqual(jsonBlocks(html), [ - { text: '{"a": 1}', line: 4 }, - { text: '{"b": 2}', line: 5 }, - { text: '{"c": 3}', line: 6 }, + { id: "a", text: '{"a": 1}', line: 4 }, + { id: undefined, text: '{"b": 2}', line: 5 }, + { id: undefined, text: '{"c": 3}', line: 6 }, ]); }); From 7f64ae0b869ab618c8ae747c67080aa45fe2349e Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Sat, 10 Oct 2026 23:59:08 +0800 Subject: [PATCH 7/7] fix ci --- .github/workflows/pages.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 7fb8bac..7d6102a 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -34,6 +34,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Install pinned Helm and kubeconform + run: npm run helm:install + - name: validate run: npm run validate