From 1b0983d832ac594ea7f943f8639f906c4e105284 Mon Sep 17 00:00:00 2001 From: rogliu Date: Thu, 10 Sep 2026 15:27:17 +0800 Subject: [PATCH 1/3] Add project page in ZH --- docs/developer/dev-notes/sflow_intro_zh.html | 1373 ++++++++++++++++++ 1 file changed, 1373 insertions(+) create mode 100644 docs/developer/dev-notes/sflow_intro_zh.html diff --git a/docs/developer/dev-notes/sflow_intro_zh.html b/docs/developer/dev-notes/sflow_intro_zh.html new file mode 100644 index 0000000..17ca36b --- /dev/null +++ b/docs/developer/dev-notes/sflow_intro_zh.html @@ -0,0 +1,1373 @@ + + + + + +sflow — 面向大规模 GPU 集群的声明式工作流描述器 + + + + + + + +
+
+ +
按 P 开启自动播放
+ + +
+
+
+
+01 / 16 +
+
NV-SFLOW
+

面向大规模 GPU 集群通用工作流编排引擎

+

一次描述,随处运行。_

+
+vLLMSGLangTensorRT-LLMDynamoPyTorch +sflow +KubernetesSlurmDocker +
+ +
+ +滚动 + + +
+ + +
+
+
+02 / 16 +
+工作负载演进 +

从本地单机容器,到集群级多阶段服务

+
+
+
部署规模跑在哪里
+
+ 过去 +
本地单机
+
1 个节点 · 1-8 张 GPU · 无跨节点依赖
+
+
+
+ 现在 +
多节点集群级服务
+
数十至数百个节点 · 跨节点通信与拓扑 · 调度器、配额与生命周期
+
+
+
+
服务架构怎么被服务
+
+ 过去 +
Agg ServerClient
+
单阶段 · 一条命令拉起服务
+
+
+
+ 现在 +
RouterPD 分离KV Transfer异构计算
+
多阶段 · 多组件协同 · 每个阶段各自的资源与就绪条件
+
+
+
+
复杂度就此转移:模型还是那个模型,但把它跑起来,已经从「起一个进程」变成「编排一整套分布式系统」。
+
+
+ + +
+
+
+03 / 16 +
+视角转变 +

更宏观的编排视角:从一台机器,到一个集群

+
+
单机视角五层都挤在同一台机器里,天然就是一致的 — 装好、跑起来,就结束了。
+
+
集群视角同样五层要在每个节点上各来一份,还必须彼此对齐 — 任何一层错位,整个工作流都跑不起来。
+
+
+
自下而上
+
+
node-0node-1node-2node-3
+
+
应用工作流DAG · 任务依赖 · 探针
+
+
跨节点依赖与就绪对齐
+
+
+
基础设施平台Slurm · Kubernetes · Docker
+
+
跨节点调度、配额与网络
+
+
+
软件vLLM · SGLang · TensorRT-LLM · 镜像
+
+
跨节点版本与镜像一致
+
+
+
驱动CUDA · NCCL · 网卡驱动
+
+
跨节点驱动与固件对齐
+
+
+
硬件GPU · NVLink · IB · 拓扑
+
+
跨节点拓扑与可见性一致
+
+
+
+
于是编排的对象变了:不再是「把一个进程跑起来」,而是把五层当成一个整体来组合,并让节点之间共享同一份信息、保持同一套约定
+
+
+ + +
+
+
+04 / 16 +
+基础设施差异 +

每个平台一套语义,迁移与适配成本倍增

+
+
+

Slurm

+
#SBATCH --gres=gpu:8 +srun --ntasks=16 ...
+
分区、账户与 QoS · sbatch 语法 · hostlist 展开规则
+
学习曲线
+
+
+

Kubernetes

+
resources.limits: + nvidia.com/gpu: 8
+
manifest 与 CRD · label 与 affinity · RBAC 与 namespace
+
学习曲线
+
+
+

Docker

+
docker run --gpus all + --network host ...
+
镜像与挂载 · 设备映射 · compose 语法
+
学习曲线
+
+
+
+
+ 应用迭代 +
+ 改一版 + 跑一次 + 卡住 — 等平台适配做完 + 改一版 +
+
+
+ 平台适配 +
+ + 学语法 · 对 schema · 调权限 · 补兼容 + +
+
+
+
真正被拖慢的是应用:模型和参数只改了一行,时间却全花在「让它能在这个平台上跑起来」— 换一个基础设施平台,就要重写整个工作流编排
+
+
+ + +
+
+
+05 / 16 +
+问题 +

工作负载始终不变,平台适配成本却成倍增长。

+
稳定的工作流: etcd → NATS → 前端 → GPU 工作节点 → 基准测试
+
+
重复的关注点
Slurm
Docker
Kubernetes
+
启动
sbatch + srun
docker run
Pod / Job
+
GPU 放置
GPU 参数
设备映射
claims + Affinity
+
网络
hostlist + 端口
host 网络
Service + DNS
+
就绪检测
shell 轮询
healthcheck
probes
+
日志与清理
sacct + 文件
docker logs
日志转发 + 生命周期
+
+
5 类关注点 × 3 个平台围绕同一个稳定的工作流,重复着同样的编排工作
+
+
+ + +
+
+
+06 / 16 +
+后端无关的设计 +

保留做什么,替换在哪跑,无需重写。

+
+

统一语义

部署逻辑始终不变 — 变的只是基础设施适配。

+

只换后端配置块

同一份 sflow.yaml 可运行在 Docker、Slurm 与 Kubernetes 上。

+

委托,而非重造

后端交由各平台的原生生态处理,而不是重新实现一遍。

+
+
+ + + + + + + +sflow.yaml +DAG · 脚本 · 探针 +资源 · 结果 · 产物 + + + + + + + + +sflow + + + + + + + + + + + + + +Local · bash + +Docker · docker_run + +Slurm · srun + +Kubernetes · k8s + + + + + +
+
+
+ + +
+
+
+07 / 16 +
+DAG 编排 +

任务与依赖,全部由你定义

+
+
+
有向无环图(DAG)   每个阶段在依赖就绪时自动启动
+
+
load_image
+ +
install_dependency
gpu_monitor
+ +
nats_server
etcd_server
+ +
frontend_server_0
frontend_server_1
frontend_server_2
+ +
nginx_server
prefill_server_0
prefill_server_1
prefill_server_2
prefill_server_3
decode_server_0
+ +
benchmark_client
+
+
+
+

任务 = 任意命令

bash、python、容器入口都可以。

+

依赖 = 任意 DAG

串行、并行、分支与汇聚,阶段数不设上限。

+

不限定领域

推理、训练、评测、数据处理均可自由编排。

+
+
+
+
+ + +
+
+
+08 / 16 +
+大规模集群级编排 +

拓扑感知的 GPU 分配

+
sflow 拓扑规划器GPU 容量 · 节点亲和性 · 任务依赖
+
▼ 以后端原生方式下发
+
+
+
node-0NVLink 域 A · 4 张 GPU
+
+
GPUGPU 0prefill_0
+
GPUGPU 1prefill_1
+
GPUGPU 2prefill_2
+
GPUGPU 3prefill_3
+
+
+
+
node-1NVLink 域 B · 4 张 GPU
+
+ decode_0 · 4 GPU 任务 +
+
GPUGPU 0decode_0
+
GPUGPU 1decode_0
+
GPUGPU 2decode_0
+
GPUGPU 3decode_0
+
+
+
+
+
节点与 GPU 放置、多节点副本与参数扫描、就绪探针、批量提交 — 于是一份描述文件即可驱动数百张 GPU,而不是一堆手写的 bash 脚本。
+
+
+ + +
+
+
+09 / 16 +
+工作流生命周期 +

定义。执行。洞察。

+
+
+

定义

+
DAG任务与依赖
一份声明式工作流
+
{ }变量与产物
可移植的输入与表达式
+
探针
就绪与失败判定
+
+
+
+

执行

+
GPU资源规划
节点、GPU、拓扑
+
副本与参数扫描
并行或串行
+
后端适配器
Local、Docker、Slurm、K8S
+
+
+
+

洞察

+
TUI实时状态
任务、日志、就绪
+
汇总与监控
时间线与硬件图表
+
结果与上传
JSON 指标与 S3
+
+
+
+
+ + +
+
+
+10 / 16 +
+后端适配器 +

同一份意图,落地为四套原生执行方案

+
可移植的工作流契约   DAG · 资源 · 探针 · 产物 · 结果
+
▼ sflow 委托给各平台的原生生态
+
+
>_

Local

bash 进程
模拟的节点 / GPU 映射

+

Docker

docker run
容器 GPU 设备

+
#

Slurm

salloc + srun
调度器分配

+

Kubernetes

pods + claims
绑定与生命周期

+
+
+
一致的输出   task.log · result.json · summary · 产物
+
+
+ + +
+
+
+11 / 16 +
+Merge Pod +

GPU 任务同机部署,共享高速的节点内互联

+
+
+

独立 Pod · 视图相互隔离

+
+
worker A仅可见 GPU 0-1
+ +
worker B仅可见 GPU 2-3
+
+
+
sflow merge
+
+

单个 Pod · 合并的 GPU 视图

+
worker Aworker B
+
GPU 0GPU 1GPU 2GPU 3
+ +
+
+
安全边界:同一工作流 + 同一后端 + 同一节点 + 同一镜像。典型场景:PD 分离的 KV 传输。同时契合当前每节点 IMEX 的约束。
+
+
+ + +
+
+
+12 / 16 +
+可观测性 +

每一次运行都会自我解释

+
+
sflow_summary.log +Task Duration Chart +prefill |####..........................| 58.412s READY +decode |##########################....| 6m14.882s READY +benchmark |..........................####| 47.906s COMPLETED + +Timeline +12:31:33 +00.000s prefill SUBMITTED +12:31:33 +00.000s decode SUBMITTED +12:32:31 +58.412s prefill READY +12:37:48 +06m15s decode READY +12:37:49 +06m16s benchmark SUBMITTED +12:38:36 +07m03s benchmark COMPLETED
+
sflow_monitor.log +Metric Summary +GPU util % min=42 avg=87 max=99 +GPU mem used GiB min=18 avg=61 max=76 +GPU power W min=310 avg=642 max=718 + +Timelines (cluster avg) +GPU util % ▁▂▅▇████▆▅ +GPU mem used GiB ▁▃▄▆▇█████ +Net RX MiB/s ▁▁▂▅▇▆▃▂▁
+
+
+
+
GPU 利用率87%
+ +
+
+
GPU 显存61 GiB
+ +
+
+
GPU 功耗642 W
+ +
+
+
+
+ + +
+
+
+13 / 16 +
+模块化组合 +

换掉一个叶子,复用整棵树。

+
+
recipe/组合根目录
+
+
+
backend/
+
local.yamldocker.yamlslurm.yamlk8s.yaml ✓
+
+
+
common/
+
common_workflow.yaml · 共享 DAG
+
+
+
workload/
+
vllm/ ✓sglang/trtllm/
+
+
+
benchmark/
+
aiperf.yaml ✓infmax.yaml
+
+
+
+
▼ 选中的叶子完成组合
+
composed.yaml   k8s + 共享工作流 + vLLM + AIPerf
+
只替换高亮的那个叶子,共享分支保持不变。
+
+
+ + +
+
+
+11 / 15 +
+真实场景调试 +

结构化的错误分析

+
+
+
Workflow: b200-fp8-low-latency-tep8-1p-1d +Model: DeepSeek R1 FP8 | 2 nodes × 8 GPUs | ISL=8192, OSL=1024 + +Allocation Map +├─ slurm-node-01 (node 0) +│ GPU 0-7: prefill_server_0 (TP=8) +│ Also: load_image, nats, etcd, frontend, benchmark_* +└─ slurm-node-02 (node 1) + GPU 0-7: decode_server_0 (TP=8) + Also: load_image, gpu_monitor + +Timeline +01:57:08 — load_image + install_aiperf submitted +01:59:10 — load_image COMPLETED on both nodes +01:59:43 — nats_server READY +01:59:45 — etcd_server READY +02:00:31 — frontend_server_0 READY (10.52.32.8) +02:05:14 — prefill + decode READY → benchmark_4 starts +02:05:20 — HTTP 500 — all benchmark requests fail +02:05:41 — Workflow finished (8m 33s) + +Error from frontend logs: +Invalid TCP address 'dynamo_prefill.generate-58b49ce145f56609' +Invalid TCP address 'dynamo_backend.generate-58b49ce145f5660b'
+
+
+

诊断 — sflow 编排层 vs 应用层错误

+ + + + + + + + + +
层级状态细节
sflow✓ OKDAG 已执行,全部任务已拉起,探针通过
GPU 分配✓ OK每节点 8 张 GPU,无重叠,每个 server TP=8
基础设施✓ OKetcd、NATS、frontend 均已 READY
路由✗ FAILfrontend 拿到的是服务名,而不是 host:port
基准测试✗ FAIL0/800 请求成功(全部 HTTP 500)
+
根因 +frontend 收到的是服务发现的服务名 +(例如 dynamo_prefill.generate-58b49...) +而不是 host:port 地址。 + +NATS/etcd 服务注册表返回的内部标识符 +TCP 路由无法解析。 + +修复:检查 DYN_REQUEST_PLANE 与 frontend +网络配置是否匹配 Dynamo 分离式路由。
+
+
+
+
+ + +
+
+12 / 15 +
+

CLI 速览

+ + + + + + + + + + + +
命令用途关键参数
sflow run执行工作流--dry-run --tui --set -f(多文件)
sflow batch生成 sbatch 脚本--submit --bulk-input --row
sflow compose合并多个 YAML--resolve --missable-tasks -o
sflow visualize渲染 DAG 图--format png/svg/mermaid
sflow sample列出 / 复制示例--list -o
sflow skill安装 AI Agent 技能--list -o
sflow upgrade原地重装(别名:sflow update--branch --dry-run --force
+
+
+ + +
+
+
+
+14 / 16 +
+开发者体验 +

Agent 原生的工作流,每跑一次就更好一点

+
+
+
01

编写 · Agent 原生

AGENTS.md 工作流规则writing-sflow-yaml 技能schema + 示例
+
+
02

校验

sflow run --dry-run解析后的变量资源 / GPU 方案
+
+
03

运行与观测

带时间戳的任务事件TUI + 就绪探针sflow_monitor.log
+
+
04

诊断与演进

sflow_summary.logtask.log + 失败提示result.json 指标
+
+
+ + AGENT 闭环 + + +
+
+
+
+ + +
+
+
+
+15 / 16 +
+愿景 +

异构计算

+

一份 recipe 就能把分离式流水线的每个阶段路由到最合适的资源池,同时保持同一份工作流契约。

+
+ + + + +Vera Rubin +GPU 资源池 • Prefill + +LPU Cluster +加速器 • Decode + + +sflow + + +prefill_server +Vera Rubin GPUs + +decode_server +LPU 加速器 + + + + + + + + + + + + +
+
一份 DAG · 专用加速器 · 同一份工作流契约
+
+
+ + +
+
+
+
+16 / 16 +
+立即上手 +

安装。探索。运行。

+

先在本地起步,在申请硬件之前完成校验,再以文档和代码仓库作为唯一事实来源。

+
+
# 从代码仓库安装 +$ uv venv --python python3 && source .venv/bin/activate +$ uv pip install "sflow @ git+https://github.com/NVIDIA/nv-sflow.git@main" + +# 验证并试跑一个本地工作流 +$ sflow --version +$ sflow sample self_contained/local/hello_world +$ sflow run -f hello_world.yaml --dry-run +✓ 先校验;确认方案无误后再运行
+ +
+ +
+
+ + + + From 7db63bb5e5ba3e9d848b1fc58f6c32d162a2425f Mon Sep 17 00:00:00 2001 From: rogliu Date: Thu, 10 Sep 2026 17:56:08 +0800 Subject: [PATCH 2/3] Fix pages and add Zh-cn --- docs-site/docusaurus.config.js | 3 + docs-site/scripts/prepare-versioned-docs.js | 10 +- docs-site/sidebars.js | 10 +- docs-site/sidebarsConfig.js | 28 +- docs-site/sidebarsConfig.test.js | 24 +- docs-site/src/pages/index.js | 114 +++++- docs-site/static/sflow_intro.html | 356 ++++++++++++++++-- .../static}/sflow_intro_zh.html | 108 ++++-- 8 files changed, 559 insertions(+), 94 deletions(-) rename {docs/developer/dev-notes => docs-site/static}/sflow_intro_zh.html (96%) diff --git a/docs-site/docusaurus.config.js b/docs-site/docusaurus.config.js index db1708b..4dc2508 100644 --- a/docs-site/docusaurus.config.js +++ b/docs-site/docusaurus.config.js @@ -121,6 +121,9 @@ const config = { }, items: [ { type: "doc", docId: "user/intro", label: "Docs", position: "left" }, + // Agent Skills is a peer of Docs, not a subsection of it. Targets the + // standalone `agents` sidebar so the entry follows the version dropdown. + { type: "docSidebar", sidebarId: "agents", label: "Agent Skills", position: "left" }, { type: "search", position: "left" }, { type: "docsVersionDropdown", position: "right" }, { diff --git a/docs-site/scripts/prepare-versioned-docs.js b/docs-site/scripts/prepare-versioned-docs.js index c66c1c2..d9a47f8 100644 --- a/docs-site/scripts/prepare-versioned-docs.js +++ b/docs-site/scripts/prepare-versioned-docs.js @@ -5,7 +5,7 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); -const { buildDocsSidebar } = require("../sidebarsConfig"); +const { buildDocsSidebar, buildAgentsSidebar } = require("../sidebarsConfig"); const { mirrorSkillsToAgents } = require("./mirror-skills"); const DOCS_SITE_DIR = path.resolve(__dirname, ".."); @@ -281,9 +281,15 @@ function prepareVersionedDocs(plan, options = {}) { handwrittenDir: paths.agentsSrcDir, destDir: path.join(versionDir, "agents"), }); + // Versions that predate the skills feature get no `agents` sidebar at all -- + // see buildAgentsSidebar. The navbar entry is guarded to match. + const agentsSidebar = buildAgentsSidebar(versionDir); writeJson( path.join(paths.versionedSidebarsDir, `${safeVersionDirName(version.label)}-sidebars.json`), - { docs: buildDocsSidebar(versionDir) }, + { + docs: buildDocsSidebar(versionDir), + ...(agentsSidebar ? { agents: agentsSidebar } : {}), + }, ); } diff --git a/docs-site/sidebars.js b/docs-site/sidebars.js index dbc187e..a02c00f 100644 --- a/docs-site/sidebars.js +++ b/docs-site/sidebars.js @@ -1,6 +1,6 @@ const path = require("path"); const fs = require("fs"); -const { buildDocsSidebar } = require("./sidebarsConfig"); +const { buildDocsSidebar, buildAgentsSidebar } = require("./sidebarsConfig"); // Mirror docusaurus.config.js: prefer the generated develop snapshot, otherwise // fall back to the repo-level docs/ directory for local dev. @@ -10,8 +10,14 @@ function currentDocsPath() { } /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ +const currentDocs = currentDocsPath(); +const agents = buildAgentsSidebar(currentDocs); + const sidebars = { - docs: buildDocsSidebar(currentDocsPath()), + docs: buildDocsSidebar(currentDocs), + // Spread rather than assign: Docusaurus rejects an empty/undefined sidebar, so a + // snapshot without skills must omit the key entirely. + ...(agents ? { agents } : {}), }; module.exports = sidebars; diff --git a/docs-site/sidebarsConfig.js b/docs-site/sidebarsConfig.js index 9befa05..83f3663 100644 --- a/docs-site/sidebarsConfig.js +++ b/docs-site/sidebarsConfig.js @@ -96,16 +96,9 @@ function buildDocsSidebar(docsDir) { if (categories.length) { sidebar.push({ type: "category", label: "Sflow User Guide", collapsed: false, items: categories }); } - // Agent Skills: a labeled category wrapping the autogenerated agents/ tree, - // present only when this snapshot ships the skills mirror. - if (dirHasDocs(docsDir, "agents")) { - sidebar.push({ - type: "category", - label: "Agent Skills", - collapsed: false, - items: [{ type: "autogenerated", dirName: "agents" }], - }); - } + // Agent Skills deliberately does NOT appear here -- it is its own top-level + // sidebar (see buildAgentsSidebar) reached from its own navbar entry, so it is + // a peer of the user guide rather than a subsection buried inside it. for (const dir of AUTOGEN_SECTIONS) { if (!EXCLUDED_DIRS.has(dir) && dirHasDocs(docsDir, dir)) { sidebar.push({ type: "autogenerated", dirName: dir }); @@ -120,6 +113,20 @@ function buildDocsSidebar(docsDir) { return sidebar; } +// Build the standalone `agents` sidebar for the Agent Skills navbar entry. +// The mirrored agents/ tree already carries sidebar_position / _category_.json +// ordering, so plain autogeneration is enough -- no wrapper category, which would +// only repeat the navbar label back at the reader. +// +// Returns null when this snapshot ships no skills (e.g. v0.1.0, which predates the +// feature). Callers must omit the sidebar entirely in that case: Docusaurus rejects +// an empty sidebar array, and a version that never had agent skills should not +// advertise them. +function buildAgentsSidebar(docsDir) { + if (!dirHasDocs(docsDir, "agents")) return null; + return [{ type: "autogenerated", dirName: "agents" }]; +} + module.exports = { USER_GUIDE_CATEGORIES, AUTOGEN_SECTIONS, @@ -127,4 +134,5 @@ module.exports = { listUserDocIds, dirHasDocs, buildDocsSidebar, + buildAgentsSidebar, }; diff --git a/docs-site/sidebarsConfig.test.js b/docs-site/sidebarsConfig.test.js index f828a91..b078eb5 100644 --- a/docs-site/sidebarsConfig.test.js +++ b/docs-site/sidebarsConfig.test.js @@ -4,7 +4,7 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); -const { buildDocsSidebar } = require("./sidebarsConfig"); +const { buildDocsSidebar, buildAgentsSidebar } = require("./sidebarsConfig"); function makeDocs(files) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sflow-sidebar-")); @@ -43,16 +43,26 @@ test("groups user docs, keeps only existing ones, and buckets the rest under Mor { type: "category", label: "More", collapsed: false, items: ["user/zzz-extra"] }, ], }, - { - type: "category", - label: "Agent Skills", - collapsed: false, - items: [{ type: "autogenerated", dirName: "agents" }], - }, { type: "autogenerated", dirName: "release_notes" }, ]); }); +test("agent skills live in their own sidebar, not inside the docs sidebar", () => { + const dir = makeDocs(["user/intro.md", "agents/intro.md", "agents/writing-sflow-yaml/index.md"]); + + // The navbar entry owns the "Agent Skills" label, so the sidebar itself is just + // the autogenerated tree -- and it must not also appear under the docs sidebar. + assert.deepEqual(buildAgentsSidebar(dir), [{ type: "autogenerated", dirName: "agents" }]); + assert.equal(JSON.stringify(buildDocsSidebar(dir)).includes("agents"), false); +}); + +test("omits the agents sidebar entirely for snapshots that predate agent skills", () => { + // Docusaurus rejects an empty sidebar, and a version that never shipped skills + // should not advertise them -- so the key has to be absent, not empty. + assert.equal(buildAgentsSidebar(makeDocs(["user/intro.md"])), null); + assert.equal(buildAgentsSidebar(makeDocs(["agents/_category_.json"])), null); +}); + test("never includes the excluded plc or developer directories and falls back when nothing matches", () => { const dir = makeDocs(["plc/sflow_srd.md", "developer/note.md"]); assert.deepEqual(buildDocsSidebar(dir), [{ type: "autogenerated", dirName: "." }]); diff --git a/docs-site/src/pages/index.js b/docs-site/src/pages/index.js index 58e5f78..c27a600 100644 --- a/docs-site/src/pages/index.js +++ b/docs-site/src/pages/index.js @@ -1,36 +1,114 @@ import React from "react"; import Head from "@docusaurus/Head"; import useBaseUrl from "@docusaurus/useBaseUrl"; +import useIsBrowser from "@docusaurus/useIsBrowser"; +import { useLocation } from "@docusaurus/router"; + +// The homepage is a full-viewport frame around the static intro deck. Language is +// carried in the query string (`/?lang=zh`) rather than by swapping the iframe in +// place, so the Chinese deck is shareable, bookmarkable and survives a reload -- +// an in-place swap leaves the address bar on "/" and silently reverts on refresh. +// +// The deck itself drives this: when it detects it is framed, its language toggle +// rewrites the TOP url instead of navigating its own document. See the +// `langToggle` handler in static/sflow_intro*.html. +const DECKS = { + en: { + file: "/sflow_intro.html", + title: "NV-sflow — Declarative Workflow Descriptor", + description: + "Declarative workflow descriptor with swappable backends. Describe once, run anywhere.", + frameTitle: "NV-sflow Introduction", + htmlLang: "en", + }, + zh: { + file: "/sflow_intro_zh.html", + title: "NV-sflow — 面向大规模 GPU 集群的声明式工作流描述器", + description: "声明式工作流描述器,后端可自由切换。一次描述,随处运行。", + frameTitle: "NV-sflow 介绍", + htmlLang: "zh-CN", + }, +}; + +const DECK_BG = "#060a10"; // matches the deck's own background, so the pre-load frame is invisible export default function Home() { - const introUrl = useBaseUrl("/sflow_intro.html"); + const { search, hash } = useLocation(); + const isBrowser = useIsBrowser(); + + const lang = new URLSearchParams(search).get("lang") === "zh" ? "zh" : "en"; + const deck = DECKS[lang]; + + // Both are resolved unconditionally: useBaseUrl is a hook and cannot be called + // behind a branch without breaking the rules of hooks. + const enUrl = useBaseUrl(DECKS.en.file); + const zhUrl = useBaseUrl(DECKS.zh.file); + + // Forward the slide anchor (#s7) so switching language deep in the deck lands + // on the same slide instead of resetting to the title. + const slide = /^#s[0-9a-z]+$/i.test(hash) ? hash : ""; + const src = (lang === "zh" ? zhUrl : enUrl) + slide; return ( <> - NV-sflow — Declarative Workflow Descriptor - + + {deck.title} + + + -
-