文章摘要
加载中...|
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结

在前两篇中(第一篇第二篇),我们完成了开发环境搭建与海光 DCU 计算节点的资源申请。今天,我们将直奔主题,攻克最后的堡垒:“在超算离线网络环境下全速下载 Stable Diffusion 大模型,修复本地加载 Bug,并用 Slurm 点火器实现一键自动追踪生图”


1. 登录节点免卡时下载模型:自制 mt 动态拉取命令

超算计算节点通常无法访问外网,我们必须在登录节点提前下载好大模型。由于直接连线 Hugging Face 极其困难且极易超时挂死,国内首选 ModelScope (魔搭社区) 官方高速源。

然而,在超算老旧的环境里,使用魔搭 SDK 下载模型存在两个致命暗坑:

  1. Python 3.7 语法冲突:最新版 ModelScope SDK 源码中引入了 Python 3.8+ 独有的海象运算符(:=),导致在 Python 3.7 下会直接报 SyntaxError 语法错误。
  2. 静默下载与参数变化:在 modelscope==1.9.5 兼容老版本里,snapshot_download 用来指定路径的参数是 cache_dir 而非 local_dir,且默认没有进度条。

1.1 卸载与安装兼容版魔搭 SDK

首先,在你的虚拟环境中降级魔搭 SDK 至兼容 Python 3.7 且性能稳定的 1.9.5

bash
# 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 一行命令:

bash
# 创建并切换到统一的模型存放目录
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 中:

bash
# 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),只需直接在登录节点运行:

bash
mt stablediffusionapi/counterfeit-v30

2. 攻克海光 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
bash
MIOPEN_FIND_MODE=1 \
MIOPEN_USER_DB_PATH=/dev/shm \
python generate.py ...

这两个参数的底层“潜台词”非常有意思

  1. MIOPEN_FIND_MODE="1"“闭上眼睛,强行进入 Normal(标准)模式!不允许你为了追求极限加速去盲目试探和扫描不稳定的算子,老老实实走最稳健的 C++ 编译器内核编译!”
  2. 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 变更引发运行报错。
python
#!/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

bash
#!/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_ARGS

4.2 全自动点火器脚本 go.sh

在日常使用中,提交作业后需要手动运行 squeue 不断查看状态,非常麻烦。使用此脚本[2]可以让提交作业和状态追踪一气呵成:

bash
#!/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,就可以悠闲地看着它从排队、锁卡、再到实时流式输出渲染日志,直至图片完美存入网盘目录。这套超算离线炼丹流程至此完美闭环落地!


  1. 由于 stablediffusionapi/anything-v5 在魔搭上是已经洗过配置的 Diffusers 组件版本,通过 snapshot_download 获取后可直接离线加载,完全跳过了繁杂的手动 "借尸还魂" 拷配置操作。 ↩︎

  2. 一键追踪点火器极大减少了频繁手动刷 squeue 的枯燥体验。 ↩︎

评论 隐私政策