在前两篇中(第一篇 与 第二篇),我们完成了开发环境搭建与海光 DCU 计算节点的资源申请。今天,我们将直奔主题,攻克最后的堡垒:“在超算离线网络环境下全速下载 Stable Diffusion 大模型,修复本地加载 Bug,并用 Slurm 点火器实现一键自动追踪生图”。
1. 登录节点免卡时下载模型:自制 mt 动态拉取命令
超算计算节点通常无法访问外网,我们必须在登录节点提前下载好大模型。由于直接连线 Hugging Face 极其困难且极易超时挂死,国内首选 ModelScope (魔搭社区) 官方高速源。
然而,在超算老旧的环境里,使用魔搭 SDK 下载模型存在两个致命暗坑:
- Python 3.7 语法冲突:最新版 ModelScope SDK 源码中引入了 Python 3.8+ 独有的海象运算符(
:=),导致在 Python 3.7 下会直接报SyntaxError语法错误。 - 静默下载与参数变化:在
modelscope==1.9.5兼容老版本里,snapshot_download用来指定路径的参数是cache_dir而非local_dir,且默认没有进度条。
1.1 卸载与安装兼容版魔搭 SDK
首先,在你的虚拟环境中降级魔搭 SDK 至兼容 Python 3.7 且性能稳定的 1.9.5:
# 1. 卸载带有语法冲突的最新版魔搭
pip uninstall -y modelscope
# 2. 重新安装 1.9.5 兼容版
pip install "modelscope==1.9.5"1.2 物理下载指令:Python SDK 一行命令极速下载
在实际部署中,我们将 Stable Diffusion 1.5 原版和 Anything-V5 二次元模型直接通过 ModelScope SDK 提供的 snapshot_download 接口进行下载。
我们在超算的 /public/home/$(whoami)/sd_automation/models 目录下直接执行以下 Python 一行命令:
# 创建并切换到统一的模型存放目录
mkdir -p /public/home/$(whoami)/sd_automation/models
cd /public/home/$(whoami)/sd_automation/models
# 1. 极速拉取 Anything-V5
python -c "import logging; from modelscope.utils.logger import get_logger; get_logger().setLevel(logging.INFO); from modelscope import snapshot_download; snapshot_download('stablediffusionapi/anything-v5', cache_dir='.')"
# 2. 极速拉取 Stable Diffusion 1.5 官方模型
python -c "import logging; from modelscope.utils.logger import get_logger; get_logger().setLevel(logging.INFO); from modelscope import snapshot_download; snapshot_download('AI-ModelScope/stable-diffusion-v1-5', cache_dir='.')"由于魔搭上的配置经过了社区处理,下载下来的 stablediffusionapi/anything-v5[1] 和 AI-ModelScope/stable-diffusion-v1-5 已经自动去除了 Hugging Face 的联网校验元数据,可以直接离线加载,落地物理绝对路径分别为:
/public/home/$(whoami)/sd_automation/models/stablediffusionapi/anything-v5/public/home/$(whoami)/sd_automation/models/AI-ModelScope/stable-diffusion-v1-5
1.3 升级与进化:自制全局快捷命令 mt 动态拉取模型
为了在后续的使用中,不需要每次下载新模型都切换目录去敲冗长的 Python 一行命令,并且能在任何目录下通过指定魔搭的模型 ID 进行流式下载,我们可以将上述逻辑封装成一个系统快捷命令 mt(如 mt stablediffusionapi/counterfeit-v30)。
在终端中执行以下命令,自动将包装脚本写入系统的 ~/.local/bin/mt 中:
# 1. 确保用户的本地命令目录存在
mkdir -p ~/.local/bin
# 2. 写入动态传参的下载逻辑包装脚本
cat << 'EOF' > ~/.local/bin/mt
#!/bin/bash
# 强制激活超算专属虚拟环境
source /public/home/$(whoami)/sd_automation/venv/bin/activate
# 检查参数
if [ -z "$1" ]; then
echo "❌ 错误: 请指定魔搭模型ID!"
echo "使用示例: mt stablediffusionapi/anything-v5"
exit 1
fi
echo "====================================================================="
echo "📥 启动魔搭动态同步引擎 | 目标仓库: $1"
echo "====================================================================="
# 现场调用虚拟环境里的 Python,并锁死本地存储根目录
python -c "
import logging
import sys
from modelscope.utils.logger import get_logger
from modelscope import snapshot_download
# 激活魔搭内置 INFO 日志,确保在终端能看到拉取进度条
get_logger().setLevel(logging.INFO)
try:
downloaded_dir = snapshot_download(
'$1',
cache_dir='/public/home/$(whoami)/sd_automation/models',
revision='master'
)
print('\n' + '='*69)
print(f'🎉 动态落地成功!物理绝对路径如下,请复制用于推理参数:')
print(f'👉 {downloaded_dir}')
print('='*69)
except Exception as e:
print(f'\n❌ [ERROR] 魔搭下载中止!原因: {e}')
sys.exit(1)
"
EOF
# 3. 赋予可执行权限
chmod +x ~/.local/bin/mt
# 4. 将本地可执行目录注入系统环境变量并刷新
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc封装完成后,如果想要尝试其他模型(例如 Counterfeit-V3.0),只需直接在登录节点运行:
mt stablediffusionapi/counterfeit-v302. 攻克海光 DCU 上的两大硬核暗坑:MIOpen 死锁与 VAE FP16 溢出
当模型落地后,在海光 DCU(ROCm 生态)上运行 Python 推理时,会遭遇两个底层硬件加速库的经典 Bug。
2.1 暗坑一:MIOpen SQLite 数据库锁死 (miopenStatusUnknownError)
- 原因分析:海光卡底层的深度学习计算加速库 MIOpen 在动态编译算子(JIT)时,默认(
0 模式)会为追求性能去疯狂扫描底层硬件以探寻最佳算子,并将编译缓存写入系统的全局临时目录/tmp下的 SQLite 数据库。在超算多用户共享环境下,/tmp极易由于权限拒绝或多进程并发抢夺,导致 SQLite 的文件锁崩溃挂起。 另外,即使你把数据库放在你个人的家目录下,由于超算的本地家目录(/public/home/$(whoami)/...)通常是通过网络挂载的分布式文件系统(如 Lustre 或 NFS),当多核 CPU 现场动态编译产生并发时,网络文件系统根本无法提供 SQLite 所需的底层文件锁(WAL 模式),导致数据库直接变成“只读”并让 MIOpen 陷入“打不开数据库 → 报错 → 重试 → 再次打不开”的死循环卡死状态,进度条永远被扣留在0/28。 - 终极解决方法:在提交作业的 Shell 脚本(如
submit_sd.sh)中,以环境变量的形式强行指定 MIOpen 的编译策略,并将缓存数据库物理重定向到当前计算节点本地的共享内存虚拟盘(/dev/shm):
MIOPEN_FIND_MODE=1 \
MIOPEN_USER_DB_PATH=/dev/shm \
python generate.py ...这两个参数的底层“潜台词”非常有意思:
MIOPEN_FIND_MODE="1":“闭上眼睛,强行进入 Normal(标准)模式!不允许你为了追求极限加速去盲目试探和扫描不稳定的算子,老老实实走最稳健的 C++ 编译器内核编译!”MIOPEN_USER_DB_PATH="/dev/shm":“将算子缓存从系统报错的/tmp强行逼进当前计算节点本地的内存根盘/dev/shm中!”
/dev/shm 驻留在节点本地物理内存中,读写速度高达每秒数十 GB,具备完美的本地文件系统锁与严格的用户权限物理隔离(其他用户无法查看和读写您的缓存,任务退出后系统也会瞬间擦除回收)。这样双管齐下,直接在物理层面秒杀了 SQLite 锁死锁冲突,实现瞬间“破冰”出图!
2.2 暗坑二:二次元模型 VAE FP16 下溢出 (灰砂图/未解码图)
- 原因分析:官方原版的 SD 1.5 在半精度(FP16)下很稳,但是类似 Anything-V5、Counterfeit-V3.0 这种二次元融合模型(Merge Models)为了渲染细腻的皮肤和复杂光效,其 VAE 模块在 FP16 下极其容易发生数值下溢(Underflow),最终解码潜空间时会输出一张灰蒙蒙、糊成一片的“灰色砂砾噪声图”。
- 终极解决方法:动态精度降维打击 我们不能直接把整个推理过程都调到 FP32,那会导致显存占用翻倍且速度极慢。最优雅的思路是:让 UNet 骨架依然跑在高速的 FP16 下,但在最后一步解码临门一脚时,动态把 VAE 和潜空间张量升回到 FP32 全精度执行解码。
3. 核心 Python 推理脚本 generate.py
我们在 ~/sd_automation 目录下创建完全体离线推理脚本 generate.py,并将海光 DCU 环境防死锁逻辑与动漫 VAE 动态精度升级完美糅合:
关于 lpw_stable_diffusion.py 的获取
代码中指定了 custom_pipeline="./lpw_stable_diffusion.py"(长提示词权重管线),该文件需手动下载放置于脚本同级目录下。
- 官方下载链接:Hugging Face Diffusers Community Examples (v0.21.4)
- 特别注意:由于超算虚拟环境中锁定的
diffusers版本为0.21.4,下载此脚本时必须选用对应版本的分支(如上方链接),切勿直接去main分支下载最新版本,否则会因为 API 变更引发运行报错。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import os
import sys
import time
def main():
parser = argparse.ArgumentParser(description="超算纯净全自动离线版 SD 推理后端 (优化版)")
parser.add_argument('--prompt', type=str, required=True, help="正向提示词")
parser.add_argument('--negative_prompt', type=str, default='', help="反向提示词")
parser.add_argument('--output_dir', type=str, default='./output_images', help="输出目录")
parser.add_argument('--model_path', type=str, required=True, help="本地模型路径")
parser.add_argument('--steps', type=int, default=28, help="渲染步数")
parser.add_argument('--cfg', type=float, default=7.5, help="CFG 引导系数")
parser.add_argument('--seed', type=int, default=-1, help="随机种子 (-1 表示随机产生)")
parser.add_argument('--width', type=int, default=512, help="图片宽度 (默认 512)")
parser.add_argument('--height', type=int, default=512, help="图片高度 (默认 512)")
parser.add_argument('--num_images', type=int, default=1, help="批量生成图片张数 (默认 1)")
parser.add_argument('--scheduler', type=str, default='euler_a',
choices=['euler_a', 'euler', 'dpm++_2m', 'dpm++_2m_sde', 'ddim', 'pndm'],
help="采样器/调度器选择 (默认 euler_a)")
parser.add_argument('--vae_fp32', action='store_true', default=False,
help="强制 VAE 模块使用 FP32 精度以规避黑图/灰白图问题")
parser.add_argument('--low_vram', action='store_true', default=False,
help="启用低显存优化 (Attention Slicing),会牺牲 20%~40% 的速度,显存充足时切勿开启")
args = parser.parse_args()
os.makedirs(args.output_dir, exist_ok=True)
# 延迟加载重度依赖库,使 --help 及参数解析可以秒开
print(">> [DEBUG] [1/6] 正在初始化 Python 环境,加载 PyTorch 核心库 (共享存储网络读取较慢,请稍候)...", flush=True)
t_start = time.perf_counter()
import torch
print(f">> [DEBUG] [2/6] PyTorch 载入成功!当前 CUDA (DCU) 状态: {torch.cuda.is_available()} | 耗时: {time.perf_counter() - t_start:.2f}s", flush=True)
print(">> [DEBUG] [3/6] 正在载入 diffusers 核心组件...", flush=True)
t_diff = time.perf_counter()
from diffusers import StableDiffusionPipeline
print(f">> [DEBUG] [4/6] 核心组件载入完成!准备解析参数... | 耗时: {time.perf_counter() - t_diff:.2f}s", flush=True)
# 锁定运行设备(由超算系统隔离分配)
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"[INFO] 当前运行设备锁定为: {device}", flush=True)
print(f"[INFO] [5/6] 正在从存储节点加载本地模型权重 (大文件读取,请耐心等待)...", flush=True)
t_load = time.perf_counter()
# 显式指定低版本兼容组件并完全关闭安全检查器(防止额外加载安全检测器权重占用显存)
from transformers import CLIPFeatureExtractor
feature_extractor = CLIPFeatureExtractor.from_pretrained(f"{args.model_path}/feature_extractor")
pipe = StableDiffusionPipeline.from_pretrained(
args.model_path,
# 从下方链接下载与 diffusers v0.21.4 匹配的 lpw 脚本放置于同级目录下:
# https://github.com/huggingface/diffusers/blob/v0.21.4/examples/community/lpw_stable_diffusion.py
custom_pipeline="./lpw_stable_diffusion.py",
torch_dtype=torch.float16 if device == "cuda" else torch.float32,
use_safetensors=True,
feature_extractor=feature_extractor,
safety_checker=None, # 彻底禁用安全检查器模块,减少显存消耗和载入时间
requires_safety_checker=False
)
# 根据参数灵活配置 Scheduler
scheduler_name = args.scheduler.lower()
if scheduler_name == "euler_a":
from diffusers import EulerAncestralDiscreteScheduler
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)
elif scheduler_name == "euler":
from diffusers import EulerDiscreteScheduler
pipe.scheduler = EulerDiscreteScheduler.from_config(pipe.scheduler.config)
elif scheduler_name == "dpm++_2m":
from diffusers import DPMSolverMultistepScheduler
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
elif scheduler_name == "dpm++_2m_sde":
from diffusers import DPMSolverMultistepScheduler
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config, use_karras_sigmas=True, algorithm_type="sde-dpmsolver++")
elif scheduler_name == "ddim":
from diffusers import DDIMScheduler
pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config)
elif scheduler_name == "pndm":
from diffusers import PNDMScheduler
pipe.scheduler = PNDMScheduler.from_config(pipe.scheduler.config)
print(f"[INFO] 权重文件读取并解析完成。| 耗时: {time.perf_counter() - t_load:.2f}s", flush=True)
print(f"[INFO] [6/6] 开始将模型分块搬运至显卡内存...", flush=True)
t_to_device = time.perf_counter()
pipe = pipe.to(device)
if device == "cuda":
torch.cuda.synchronize() # 强行等待搬运彻底结束
print(f"[INFO] 模型上卡成功!| 耗时: {time.perf_counter() - t_to_device:.2f}s", flush=True)
# 显存优化策略配置
vae_fp32 = args.vae_fp32
if device == "cuda":
if args.low_vram:
print("[INFO] ⚡ 已开启低显存优化模式 (Attention Slicing),将牺牲部分运算速度以降低显存占用。", flush=True)
pipe.enable_attention_slicing()
else:
print("[INFO] 🚀 显存充足,采用满血极速模式运行(未启用 Attention Slicing)。", flush=True)
# 动漫模型精度自动对齐策略(或手动强制开启)
if not vae_fp32:
model_tag = args.model_path.lower()
if "anything" in model_tag or "counterfeit" in model_tag:
vae_fp32 = True
print("[INFO] 🛡️ 检测到二次元动漫模型,已自动触发开启 VAE FP32 精度对齐,防止灰沙图!", flush=True)
if vae_fp32:
print("[INFO] 🛡️ VAE 模块已强制升级至 FP32 精度,保证解码色域正常。", flush=True)
pipe.vae.to(dtype=torch.float32)
# 随机种子处理
if args.seed == -1:
import random
base_seed = random.randint(0, 2**32 - 1)
else:
base_seed = args.seed
print("==========================================", flush=True)
print(f"[INFO] 基础随机种子: {base_seed} | 计划出图数量: {args.num_images}", flush=True)
print("==========================================", flush=True)
print("[INFO] 海光核心开始爆轰,正在解构潜空间...", flush=True)
print("[INFO] 💡 提示:海光 DCU 在执行第 1 步时会默默编译 MIOpen 算子,耗时较长,请观察下方实时步数计数:", flush=True)
# 开始循环渲染多张图片,最大化复用已载入的模型,规避超算排队和 PyTorch/Model 加载时间瓶颈
for idx in range(args.num_images):
current_seed = (base_seed + idx) & 0xFFFFFFFF
generator = torch.Generator(device=device).manual_seed(current_seed)
print(f"\n[INFO] [图片 {idx + 1}/{args.num_images}] 开始渲染,随机种子 Seed: {current_seed}", flush=True)
# 定义渲染进度回调函数
def step_callback(step: int, timestep: int, latents: torch.FloatTensor):
print(f" -> [渲染雷达] 正在执行渲染: 图片 [{idx + 1}/{args.num_images}] | 步数 [{step + 1}/{args.steps}] | 戳记: {time.strftime('%H:%M:%S', time.localtime())}", flush=True)
with torch.inference_mode():
t_infer = time.perf_counter()
# 1. 强行拉起管线进行潜空间扩散
output = pipe(
prompt=args.prompt,
negative_prompt=args.negative_prompt,
num_inference_steps=args.steps,
guidance_scale=args.cfg,
generator=generator,
width=args.width,
height=args.height,
output_type="latent",
callback=step_callback,
callback_steps=1
)
latents = output.images if hasattr(output, "images") else output[0]
# 🔥【第一道硬件同步令】:斩断扩散流积压延迟
if device == "cuda":
torch.cuda.synchronize()
print(f"[INFO] [图片 {idx + 1}/{args.num_images}] 潜空间扩散生成完毕。| 纯推理耗时: {time.perf_counter() - t_infer:.2f}s", flush=True)
print("[INFO] 正在进入 VAE 图像解码与矩阵转换阶段...", flush=True)
t_vae = time.perf_counter()
# 如果是 FP32 解码,强制把输入 VAE 的 latents 数据也提升到 FP32 精度对齐
if vae_fp32 and device == "cuda":
latents = latents.to(dtype=torch.float32)
# 2. 用全面升级后的 FP32 精度进行 VAE 解码
image_tensor = pipe.vae.decode(latents / pipe.vae.config.scaling_factor, return_dict=False)[0]
# 🔥【第二道硬件同步令】:锁定 VAE 显存矩阵,防止异步流抢跑
if device == "cuda":
torch.cuda.synchronize()
print(f" -> [VAE明细] VAE网络解码矩阵完成。| 耗时: {time.perf_counter() - t_vae:.2f}s", flush=True)
# 3. 安全后处理与内存回收
t_post = time.perf_counter()
image_tensor = (image_tensor / 2 + 0.5).clamp(0, 1)
# 🔥【第三道硬件同步令】:拦截 D2H 跨节点搬运,防止后台总线死锁
if device == "cuda":
torch.cuda.synchronize()
image_tensor = image_tensor.cpu()
image_tensor = image_tensor.permute(0, 2, 3, 1).float().numpy()[0]
# 4. 转换回标准的 PIL 图片对象
image = pipe.numpy_to_pil(image_tensor)[0]
print(f"[INFO] 后处理与 PIL 对象转换全部搞定。| 耗时: {time.perf_counter() - t_post:.2f}s", flush=True)
# 保存落地
output_filename = f"sd_seed_{current_seed}.png"
output_path = os.path.join(args.output_dir, output_filename)
image.save(output_path)
print("==========================================", flush=True)
print(f"🎉 炼丹成功!图片已安全落地 -> {output_path}", flush=True)
print("==========================================", flush=True)
# 释放每轮循环的临时显存,防止显存积攒发生 OOM
if device == "cuda":
import gc
gc.collect()
torch.cuda.empty_cache()
if __name__ == '__main__':
main()4. Slurm 批处理任务与 go.sh 一键追踪点火器
为了规避 SSH 连接因网络波动断开导致的任务中断,我们需要采用 Slurm 后台批处理方式提交任务。
4.1 作业脚本 submit_sd.sh
#!/bin/bash
#SBATCH --job-name=SD_Inference
#SBATCH --output=logs/sd_%j.log
#SBATCH --error=logs/sd_%j.err
#SBATCH --partition=kshdtest # 昆山测试大分区
#SBATCH --gres=gres:dcu:1 # 锁定 1 张国产海光 DCU 显卡
#SBATCH --cpus-per-task=6 # 🔥 核心大招:强行索要 6 核 CPU,成倍自动翻上去系统内存,防止被 cgroup OOM 物理强杀
# 1. 彻底退出任何你当前终端的虚拟环境状态(防止干扰)
deactivate 2>/dev/null
# 2. 洗净全身,一键加载超算官方满血海光 PyTorch 1.10 环境底座
module purge
module load apps/PyTorch/1.10/dtk-22.04-py37
# 3. 动态获取脚本所在目录,确保路径不跑偏(支持任意位置/多用户提交)
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
cd "$SCRIPT_DIR"
# 4. 确保日志、输出文件夹正常存在
mkdir -p logs
mkdir -p output_images
# =====================================================================
# 🛡️ 安全隔离层:在 Shell 外层提前解析好路径与配置参数
# =====================================================================
MY_USER=$(whoami)
# 支持通过环境变量直接传入参数进行覆盖,否则读取本地文件或采用默认值
MY_PROMPT="${SD_PROMPT:-$(cat "${SCRIPT_DIR}/prompt.txt" 2>/dev/null | tr -d '\n\r')}"
MY_NEGATIVE="${SD_NEGATIVE:-$(cat "${SCRIPT_DIR}/negative.txt" 2>/dev/null | tr -d '\n\r')}"
MY_MODEL="${SD_MODEL:-/public/home/${MY_USER}/sd_automation/models/stablediffusionapi/anything-v5}"
# 图像控制参数
WIDTH="${SD_WIDTH:-512}"
HEIGHT="${SD_HEIGHT:-512}"
STEPS="${SD_STEPS:-28}"
CFG="${SD_CFG:-7.5}"
NUM_IMAGES="${SD_NUM_IMAGES:-1}"
SCHEDULER="${SD_SCHEDULER:-euler_a}"
SEED="${SD_SEED:--1}"
# 可选性能优化控制
EXTRA_ARGS=""
if [ "$SD_VAE_FP32" = "true" ]; then
EXTRA_ARGS="$EXTRA_ARGS --vae_fp32"
fi
if [ "$SD_LOW_VRAM" = "true" ]; then
EXTRA_ARGS="$EXTRA_ARGS --low_vram"
fi
# 6. 打印前置诊断日志,确保参数没丢
echo "=========================================="
echo "[SYSTEM CHECK] 运行用户: ${MY_USER}"
echo "[SYSTEM CHECK] 项目目录: ${SCRIPT_DIR}"
echo "[SYSTEM CHECK] 目标模型: ${MY_MODEL}"
echo "[SYSTEM CHECK] 图像尺寸: ${WIDTH}x${HEIGHT} | 采样器: ${SCHEDULER}"
echo "[SYSTEM CHECK] 渲染步数: ${STEPS} | CFG: ${CFG} | 生成张数: ${NUM_IMAGES}"
echo "[SYSTEM CHECK] 正向提示词长度: ${#MY_PROMPT} 字符"
echo "=========================================="
# 5. 🔥 降维打击开火:用官方 python 驱动,搭配全局 PYTHONPATH 强行把你的 venv 变成最高优先级依赖库
# 同时开启双离线模式与 MIOpen 本地纯内存盘重定向,秒杀死锁,-u 强制无缓存刷出第五步日志!
PYTHONPATH="${SCRIPT_DIR}/venv/lib/python3.7/site-packages" \
HF_HUB_OFFLINE=1 \
TRANSFORMERS_OFFLINE=1 \
MIOPEN_FIND_MODE=1 \
MIOPEN_USER_DB_PATH=/dev/shm \
python -u generate.py \
--prompt "${MY_PROMPT}" \
--negative_prompt "${MY_NEGATIVE}" \
--output_dir "${SCRIPT_DIR}/output_images" \
--steps "${STEPS}" \
--cfg "${CFG}" \
--model_path "${MY_MODEL}" \
--width "${WIDTH}" \
--height "${HEIGHT}" \
--num_images "${NUM_IMAGES}" \
--scheduler "${SCHEDULER}" \
--seed "${SEED}" \
$EXTRA_ARGS4.2 全自动点火器脚本 go.sh
在日常使用中,提交作业后需要手动运行 squeue 不断查看状态,非常麻烦。使用此脚本[2]可以让提交作业和状态追踪一气呵成:
#!/bin/bash
# ==========================================================
# 昆山超算 Stable Diffusion 一键追踪点火器 (作业状态硬绑定版)
# ==========================================================
# 强行清洗掉 Windows 下编辑可能带过来的 \r 换行符毒瘤
sed -i 's/\r$//' "$0" 2>/dev/null
sed -i 's/\r$//' submit_sd.sh 2>/dev/null # 👈 核心改进:把 submit_sd.sh 也清理一下,防止 DOS 格式报错
# 强制让所有派生的 Python 进程关闭输出缓冲,实时向日志吐进度条
export PYTHONUNBUFFERED=1
# 1. 自动触发提交作业,并用 grep 瞬间抓取超算分配给你的作业 ID (Job ID)
SUBMIT_OUTPUT=$(sbatch submit_sd.sh)
JOB_ID=$(echo "$SUBMIT_OUTPUT" | grep -oE '[0-9]+')
if [ -z "$JOB_ID" ]; then
echo "❌ 差评!作业提交失败,请检查 submit_sd.sh 是否有语法错误。"
echo "原始报错: $SUBMIT_OUTPUT"
exit 1
fi
echo "=================================================="
echo "🚀 炼丹火种已投递!超算作业 ID: [ $JOB_ID ]"
echo "──────────────────────────────────────────────────"
echo " 正在为你开启作业状态硬核雷达追踪,请勿关闭窗口..."
echo "=================================================="
# 定义日志路径
LOG_FILE="logs/sd_${JOB_ID}.log"
# 2. 阻塞式保活追踪回路
echo -n "[⏳ 正在等待超算调度分配显卡...]"
while [ ! -f "$LOG_FILE" ]; do
sleep 1
# 顺便检查作业是否被超算无情秒杀
if ! squeue -j $JOB_ID 2>/dev/null | grep -q $JOB_ID; then
echo -e "\n❌ 糟糕!作业还未生成日志就从队列消失了,可能排队超限被取消或环境爆了。"
echo "💡 提示:请立刻敲命令查看死因:cat logs/sd_${JOB_ID}.err"
exit 1
fi
done
echo -e " 成功!\n"
echo "🔥 显卡已经咬住,以下为超算核心实时渲染流:"
echo "--------------------------------------------------"
# 3. 🔥【核心破局大招】:真正的跨节点作业生命周期异步绑定
# 将 tail -f 放到本地 Shell 后台异步挂起,并捕获它在登录节点本地的真实 PID
tail -n +1 -f "$LOG_FILE" &
TAIL_PID=$!
# 主进程陷入高效高频雷达监控,直接向超算调度器中央索要作业状态
while true; do
# 只要在中央集群队列里查不到这个作业 ID 了,说明远程计算节点已经完全闭幕退出
if ! squeue -j $JOB_ID 2>/dev/null | grep -q $JOB_ID; then
break
fi
sleep 2 # 👈 改进:略微放缓轮询频率,对超算调度节点更友好
done
# 4. 作业寿命终结,在登录节点干净利落地切断本地日志流,安全收网
sleep 1.5 # 👈 核心改进:等待 1.5 秒钟,给 tail -f 充足的时间输出最后一波缓存日志 (如“图片已安全落地”等)
kill $TAIL_PID 2>/dev/null
wait $TAIL_PID 2>/dev/null
echo -e "\n--------------------------------------------------"
echo "🎉 恭喜!检测到超算作业 [ $JOB_ID ] 已安全释放,雷达追踪完美收网!"
# 5. 在黑窗口里直接给你画出来
# 👈 核心改进:直接从日志提取本次渲染落地的图片路径,而非仅仅找最新文件,防范历史残留干扰
LATEST_IMAGE=$(grep '图片已安全落地 ->' "$LOG_FILE" 2>/dev/null | tail -n 1 | sed 's/.*图片已安全落地 -> //')
if [ -n "$LATEST_IMAGE" ] && [ -f "$LATEST_IMAGE" ]; then
echo -e "\n🖼️ 当前出图预览 (字符画微缩粗略预览):"
echo "=================================================="
if command -v img2txt &> /dev/null; then
img2txt -W 60 -H 20 "$LATEST_IMAGE"
else
echo " [提示] 终端未安装 img2txt 工具,直接去可道云刷新看大图即可!"
fi
echo "=================================================="
echo "🔔 捷报:请立刻切换到你的【可道云浏览器窗口】!"
echo "📂 进到 output_images 目录刷新,高清大图已经平铺就位,直接双击开幕!"
else
# 作为一个安全备用,如果 grep 没抓到但最新图片还在,且是刚刚生成的,可以 fallback
LATEST_IMAGE_FALLBACK=$(ls -t output_images/sd_seed_*.png 2>/dev/null | head -n 1)
if [ -f "$LATEST_IMAGE_FALLBACK" ] && [ "$(( $(date +%s) - $(stat -c %Y "$LATEST_IMAGE_FALLBACK" 2>/dev/null || echo 0) ))" -lt 60 ]; then
LATEST_IMAGE="$LATEST_IMAGE_FALLBACK"
echo -e "\n🖼️ 当前出图预览 (Fallback 自动匹配最近生成的图片):"
echo "=================================================="
if command -v img2txt &> /dev/null; then
img2txt -W 60 -H 20 "$LATEST_IMAGE"
else
echo " [提示] 终端未安装 img2txt 工具,直接去可道云刷新看大图即可!"
fi
echo "=================================================="
else
echo "⚠️ 未能捕捉到最终图片文件,请去 logs/ 目录下查看具体排错日志。"
fi
fi现在,只需要在可道云的终端或是网页 SSH 中轻敲一行 ./go.sh,就可以悠闲地看着它从排队、锁卡、再到实时流式输出渲染日志,直至图片完美存入网盘目录。这套超算离线炼丹流程至此完美闭环落地!