195 lines
6.9 KiB
Markdown
195 lines
6.9 KiB
Markdown
# 远程临时实验 Smoke
|
||
|
||
## 适用范围
|
||
|
||
本模板用于一次性远程 build、Docker GPU、模型下载或硬件 smoke。目标是让执行者在一个实验目录内完成:
|
||
|
||
- 一次 preflight;
|
||
- 一个或多个串行后台 job;
|
||
- 一次原入口 smoke;
|
||
- 一份有界 evidence;
|
||
- 一次幂等 cleanup。
|
||
|
||
正式 CI/CD、YAML-first apply、常驻服务、Secret 下发和公网暴露继续使用对应专项 CLI 或 skill,不能套用本模板。
|
||
|
||
## 必填参数
|
||
|
||
开始写入目标前必须明确以下参数,不得根据宿主名、历史目录或上一任务猜测:
|
||
|
||
| 参数 | 约束 |
|
||
| --- | --- |
|
||
| `route` | 任务上下文提供的 `Target:absoluteWorkspace` 或对应 k3s route。 |
|
||
| `work_dir` | 目标允许范围内的绝对临时目录,默认位于 `/tmp`。 |
|
||
| `resource_prefix` | 容器、镜像、网络、PID 和证据文件共享的任务前缀。 |
|
||
| `job_id` | 当前长步骤的稳定短标识,例如 `model-download` 或 `image-build`。 |
|
||
| `command` | 写入 job 脚本的单一长命令,不在同步 `trans` 中等待完成。 |
|
||
| `artifact_allowlist` | 允许保留的模型缓存、WAV、JSON 或摘要文件。 |
|
||
| `stop_condition` | OOM、ABI、许可证、上游 ref 或硬件能力等首次确定停止条件。 |
|
||
|
||
参数缺失时先返回可操作错误:
|
||
|
||
- 不要创建第二条执行路径;
|
||
- 不要把 Secret、token 或完整环境变量写入参数、脚本、日志和 evidence。
|
||
|
||
## 单次 Smoke 流程
|
||
|
||
### 1. Preflight
|
||
|
||
只做一次有界探测,确认目录、资源前缀、依赖入口和停止条件:
|
||
|
||
```bash
|
||
trans <Target>:/tmp sh <<'SH'
|
||
set -eu
|
||
work_dir=<absolute-work-dir>
|
||
resource_prefix=<resource-prefix>
|
||
|
||
test "${work_dir#/tmp/}" != "$work_dir"
|
||
case "$resource_prefix" in
|
||
''|*[!a-zA-Z0-9_.-]*) echo 'invalid resource_prefix' >&2; exit 2 ;;
|
||
esac
|
||
|
||
mkdir -p "$work_dir/jobs" "$work_dir/artifacts"
|
||
printf 'host='; hostname
|
||
df -h "$work_dir" | tail -1
|
||
docker version --format 'docker={{.Server.Version}}' 2>/dev/null || true
|
||
nvidia-smi --query-gpu=name,driver_version,memory.total,memory.free --format=csv,noheader 2>/dev/null || true
|
||
docker ps -a --filter "label=unidesk.task=$resource_prefix" --format '{{.Names}} {{.Status}} {{.Image}}' 2>/dev/null || true
|
||
SH
|
||
```
|
||
|
||
若目标范围不是 `/tmp`,必须由 owning issue 或任务上下文明确授权,再相应替换目录断言。
|
||
|
||
### 2. 写入 Job 脚本
|
||
|
||
远端文本修改只使用 `apply-patch`。每个长步骤的脚本必须写最终 exit code 和 done 标记:
|
||
|
||
```bash
|
||
trans <Target>:<absolute-work-dir> apply-patch <<'PATCH'
|
||
*** Begin Patch
|
||
*** Add File: jobs/<job-id>.sh
|
||
+#!/bin/sh
|
||
+set +e
|
||
+
|
||
+<one-long-command>
|
||
+code=$?
|
||
+printf '%s\n' "$code" > /absolute/work-dir/jobs/<job-id>.exit
|
||
+touch /absolute/work-dir/jobs/<job-id>.done
|
||
+exit "$code"
|
||
*** End Patch
|
||
PATCH
|
||
```
|
||
|
||
job 脚本只能引用任务已确认的绝对目录、资源前缀和公开依赖身份。需要 Secret 的任务不使用本模板自行读取运行面值。
|
||
|
||
### 3. Submit
|
||
|
||
submit 只负责清理旧状态、后台启动和返回 PID,不等待长命令:
|
||
|
||
```bash
|
||
trans <Target>:<absolute-work-dir> sh <<'SH'
|
||
set -eu
|
||
job_id=<job-id>
|
||
job_dir=/absolute/work-dir/jobs
|
||
|
||
rm -f "$job_dir/$job_id.done" "$job_dir/$job_id.exit" "$job_dir/$job_id.stdout" "$job_dir/$job_id.stderr"
|
||
chmod +x "$job_dir/$job_id.sh"
|
||
nohup sh "$job_dir/$job_id.sh" \
|
||
> "$job_dir/$job_id.stdout" \
|
||
2> "$job_dir/$job_id.stderr" \
|
||
< /dev/null &
|
||
printf '%s\n' "$!" > "$job_dir/$job_id.pid"
|
||
printf 'job=%s state=submitted pid=%s\n' "$job_id" "$(cat "$job_dir/$job_id.pid")"
|
||
SH
|
||
```
|
||
|
||
### 4. Poll
|
||
|
||
每次 poll 只输出状态和有界日志尾部。不得重复 submit,也不得恢复完整 stdout dump:
|
||
|
||
```bash
|
||
trans <Target>:<absolute-work-dir> sh <<'SH'
|
||
set -eu
|
||
job_id=<job-id>
|
||
job_dir=/absolute/work-dir/jobs
|
||
pid=$(cat "$job_dir/$job_id.pid" 2>/dev/null || true)
|
||
|
||
if [ -f "$job_dir/$job_id.done" ]; then
|
||
state=finished
|
||
exit_code=$(cat "$job_dir/$job_id.exit")
|
||
elif [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||
state=running
|
||
exit_code=-
|
||
else
|
||
state=lost
|
||
exit_code=-
|
||
fi
|
||
|
||
printf 'job=%s state=%s exit=%s pid=%s\n' "$job_id" "$state" "$exit_code" "${pid:--}"
|
||
printf '%s\n' '--- stdout tail ---'
|
||
tail -n 20 "$job_dir/$job_id.stdout" 2>/dev/null || true
|
||
printf '%s\n' '--- stderr tail ---'
|
||
tail -n 20 "$job_dir/$job_id.stderr" 2>/dev/null || true
|
||
SH
|
||
```
|
||
|
||
`state=lost` 表示 PID 已消失但没有 done 标记:
|
||
|
||
- 先保留状态文件和日志;
|
||
- 不要自动重提;
|
||
- 根据首个确定错误决定停止或创建新的 `job_id`。
|
||
|
||
### 5. 原入口 Smoke 与 Evidence
|
||
|
||
只执行 owning issue 要求的一个 smoke。通用 evidence 至少包含:
|
||
|
||
- `sourceRef`、模型或依赖 ref、许可证;
|
||
- 容器基础镜像 digest、构建镜像 ID、runtime 和 GPU request;
|
||
- 精确 API 或命令参数;
|
||
- 首次调用和热调用耗时;
|
||
- GPU 基线、峰值显存和 OOM 状态;
|
||
- 生成物大小、哈希和原生解码/检查摘要;
|
||
- 首个失败点和是否触发 `stop_condition`;
|
||
- cleanup 结果和允许保留的 artifact 路径。
|
||
|
||
证据文件只写摘要、presence、fingerprint、ID、digest 和有界日志尾部,不复制 Secret、完整环境、模型正文或无界 stdout。
|
||
|
||
### 6. Cleanup
|
||
|
||
cleanup 必须幂等,并按资源前缀或 label 精确删除:
|
||
|
||
```bash
|
||
trans <Target>:<absolute-work-dir> sh <<'SH'
|
||
set -eu
|
||
work_dir=/absolute/work-dir
|
||
resource_prefix=<resource-prefix>
|
||
|
||
for container in $(docker ps -aq --filter "label=unidesk.task=$resource_prefix"); do
|
||
docker rm -f "$container"
|
||
done
|
||
for image in $(docker images -q --filter "label=unidesk.task=$resource_prefix"); do
|
||
docker image rm "$image" || true
|
||
done
|
||
for network in $(docker network ls -q --filter "label=unidesk.task=$resource_prefix"); do
|
||
docker network rm "$network" || true
|
||
done
|
||
|
||
find "$work_dir/jobs" -type f \( -name '*.pid' -o -name '*.done' -o -name '*.exit' \) -delete
|
||
|
||
printf '%s\n' 'containers:'
|
||
docker ps -a --filter "label=unidesk.task=$resource_prefix" --format '{{.Names}} {{.Status}} {{.Image}}'
|
||
printf '%s\n' 'images:'
|
||
docker images --filter "label=unidesk.task=$resource_prefix" --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.Size}}'
|
||
printf '%s\n' 'networks:'
|
||
docker network ls --filter "label=unidesk.task=$resource_prefix" --format '{{.Name}}'
|
||
SH
|
||
```
|
||
|
||
只保留 `artifact_allowlist` 明确列出的缓存和证据。临时容器、镜像、网络、PID 和非复用 scratch 不得伪装成正式运行面。
|
||
|
||
## 停止判定
|
||
|
||
- 首个确定的 OOM、ABI、许可证、上游 ref 或硬件能力错误触发 `stop_condition` 后立即停止同族试配。
|
||
- 依赖下载失败与模型失败必须分层记录,不能把网络、registry 或包 ABI 问题误判为模型不可用。
|
||
- smoke 未进入原入口时,首次调用、热调用、峰值显存和生成物必须明确写 `未执行`,不能填推测值。
|
||
- 后续候选模型或依赖组合使用新的 issue、TaskTree Task、资源前缀和实验目录,不能在当前 smoke 中无界扩展;遗留 MDTODO 按 `$unidesk-tasktree` 迁移。
|