ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Pollinations DreamShaper GPU 路由:从 Sana 别名到 Vast 双 Worker 的自托管图像推理链路

Pollinations DreamShaper GPU 路由:从 Sana 别名到 Vast 双 Worker 的自托管图像推理链路
  • AI 应用
  • 媒体生成
  • 后端
  • 前端

【免费下载链接】pollinations

Your Friendly Open-Source Gen-AI Platform

项目地址:https://gitcode.com/gh_mirrors/po/pollinations
点击查看免费下载

本篇指南围绕operations/infrastructure/gpu/dreamshaper/SERVERS.md展开,讲清 Pollinations 平台上sana模型别名如何被保留并映射到 DreamShaper 8 LCM 的 Vast.ai 双 Worker 生产路由:包括当前部署形态、注册表池键(pool key)的历史成因、Vast 实例与命名 Cloudflare Tunnel 的部署方式,以及 gen Worker 侧的加权池选择、503 跨 Worker 重试与队列限流机制。读完本文,你可以理解这条"别名兼容路由"从请求进入到图像返回的完整调用链,并掌握扩容、替换 Worker 时的运维检查点。

1. 路由背景:SANA Sprint 退役后留下的sana别名

SERVERS.md给出的第一条关键事实是:SANA Sprint 已不在生产图像路由中。公开模型名sana被保留为dreamshaper的别名,实际由 DreamShaper 8 LCM 服务,部署在两台 Vast Worker 上;同时内部注册表池键也继续叫sana,以保持兼容。

model=sana -> registry alias dreamshaper -> pool type sana -> named Cloudflare tunnels -> DreamShaper workers

这条链路在仓库源码中有完整印证:

  • 模型注册表中,lykon/dreamshaper-8-lcm显式声明了别名列表(见 shared/registry/image.ts):
"lykon/dreamshaper-8-lcm": { // "sana" is kept as an alias so existing callers and the legacy image // proxy worker keep working unchanged. aliases: ["sana", "dreamshaper"], provider: "vast", ... },
  • gen Worker 的图像分发对lykon/dreamshaper-8-lcm走自托管池,且注释明确指出池键仍是sana(见 gen.pollinations.ai/src/image/createAndReturnImages.ts):
case "lykon/dreamshaper-8-lcm": // pool key stays "sana" — see VALID_TYPES in availableServers.ts return await callSelfHostedServer(prompt, safeParams, "sana");
  • 池键的白名单定义在 gen.pollinations.ai/src/image/availableServers.ts。源码注释解释了保留旧名的根本原因:/register会拒绝未知类型,如果直接改用dreamshaper作为池键,Worker 就无法在路由变更部署之前先行注册进池,会存在"请求打向空池"的窗口期。

注意:没有外部自动 fallback。文档明确写道:"There is no automatic external fallback. If no worker is registered, the model is down." 可用性完全依赖两台 Vast Worker 与跨 Worker 的 503 重试。

2. 当前部署形态:两台 Vast Worker + 命名 Tunnel

SERVERS.md(Last updated: 2026-09-02)记录的部署快照如下:

WorkerVast instanceMachine / regionGPUAll-in rateRegistered hostname
dreamshaper-vast-0149063196143507 / Mexico, MXRTX 3060$0.075556/hrdreamshaper-canary-49063196.myceli.ai
dreamshaper-vast-0247789794100803 / Romania, RORTX 4070$0.093889/hrdreamshaper-canary-47789794.myceli.ai

更晚更新(2026-09-26)的 GPU_INSTANCES.md 记录了后续替换:实例47789794(Romania, RTX 4070)已被人审批准的实例52741049(machine35285,US,RTX 3060 12 GB,$0.081481/hr all-in)替换,槽位成本从 $0.093889/hr 降至 $0.081481/hr;两台 DreamShaper GPU 合计 $100.27/30d。因此排查当前sana路由时应以GPU_INSTANCES.md的最新记录为准,SERVERS.md表格代表 2026-09-02 时的状态。

文档同时给出两条硬性运维约束:

  1. 两台 Worker 都运行operations/infrastructure/gpu/dreamshaper/setup-vast.sh,Vast 在容器重启后执行/root/onstart.sh恢复服务(而非/workspace/onstart.sh);
  2. 实时舰队与合格性证据见 GPU_INSTANCES.md,部署与 API 细节见 DreamShaper README。

2.1 历史 Sana 基础设施(已废弃)

文档最后一段明确划清了历史边界:

Vast instance30937024、Scaleway Sana workers 以及遗留的 SSH-tunnel 路由属于历史设施,不参与生产分发。诊断当前sana别名时,不要使用旧 runbook 里的旧 IP 地址或 SSH 命令。

这一点有现实风险:sana这个名字横跨了三个时代——GH200 上的 SANA-Sprint(GPU_INSTANCES.md 提到"Replaced SANA-Sprint on the GH200")、Lambda Labs 上的 Sana(端口 8766,systemdsana.service,见 GPU_INSTANCES.md 的 Lambda Labs 章节)、以及当前的 DreamShaper Vast 路由。只有最后一条是当前生产链路。

3. Worker 模型配置:三个"静默失败"的关键项

DreamShaper README 定义了这个 FastAPI Worker 的核心配置,且指出三项配置改错了不会抛异常,只会出坏图——这属于典型的静默失败点,值得逐条对照源码 operations/infrastructure/gpu/dreamshaper/server.py:

3.1 LCMScheduler 是强制项

DreamShaper-8 仓库自带DEISMultistepScheduler(solver_order=2、timestep_spacing="leading")。LCM 模型跑在这个调度器上会产出"washed-out mush"(发虚的糊图),且不抛异常。源码在管道装配处显式替换调度器(server.py):

# MANDATORY: dreamshaper-8 ships DEISMultistepScheduler with solver_order=2 # and timestep_spacing="leading". Running an LCM model on that produces # washed-out mush with no exception raised. pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)

3.2 guidance_scale=0.0

LCM-LoRA 已把 guidance 烘焙进模型。实测 cfg 1.5 与 cfg 0.0 视觉上无差异,但 cfg > 1 会开启 classifier-free guidance,每步多跑一次无条件前向,白白损失约 1.8× 吞吐。对应源码(server.py):

# LCM-LoRA bakes guidance into the model. Measured: cfg 1.5 and cfg 0.0 are # visually identical, but cfg > 1 turns on classifier-free guidance, which runs # a second unconditional pass per step and costs 1.8x throughput for nothing. GUIDANCE_SCALE = float(os.getenv("GUIDANCE_SCALE", "0.0"))

3.3 peft 是必需依赖

diffusers ≥0.30 移除了非 PEFT 的 LoRA 后端,没有peft时fuse_lora()直接失败。requirements.txt 中peft与diffusers>=0.38.0、transformers<5等并列:

diffusers>=0.38.0 transformers<5 accelerate peft safetensors huggingface-hub fastapi uvicorn pydantic aiohttp pillow requests

另外两点配置决策:

  • TAESD 微型解码器:madebyollin/taesd替换标准 VAE,在 RTX 3060 上端到端实测 3.9× 加速且无可见质量差异——低步数下瓶颈在 decode 而非 UNet(server.py);
  • 采样步数取 3:3 是可用输出的下限。512×512、batch 1 的历史单客户端实测数据:
StepsQualityRTX 3090RTX 3060
1unrecoverable mush13.80 img/s9.04 img/s
2holds up on portraits, soft on scenes and flat-vector8.795.45
3clean6.183.88
4marginally crisper5.063.01

文档特别提醒这些是历史单客户端测量值而非生产能力:历史需求峰值为5.72 img/s,生产部署为 2 张 GPU、每张 3 个 Uvicorn 进程;早期正是单客户端基准高估了可用容量,导致生产队列持续增长。更大的卡也不见得更快:RTX 4090 在 3 步下仅 4.99 img/s,比 3090 还慢——因为共享的 64 核 EPYC 宿主机是 CPU-dispatch 瓶颈,GPU 从未打满;4090 只在 micro-batching(batch 16 时 22.8 img/s)才反超,而该 Worker 不做批处理。

4. 部署:Vast 实例、setup-vast.sh 与重启恢复

4.1 三级递进的部署命令

README 给出的操作流程(canary 隔离验证 → 公网路径 canary → 生产)必须按顺序执行:

# 1) 租用 Vast SSH 实例(至少 30 GB 磁盘),rent 时配置启动钩子: vastai create instance <OFFER> \ --image nvidia/cuda:12.8.1-cudnn-runtime-ubuntu24.04 \ --disk 30 --ssh --label dreamshaper-vast-NN \ --onstart-cmd 'if [ -x /root/onstart.sh ]; then /root/onstart.sh; fi' vastai attach ssh <INSTANCE> ~/.ssh/id_ed25519.pub # 2) Isolated canary:默认不注册生产心跳、不起 tunnel。 PLN_GPU_TOKEN=... PUBLIC_HOSTNAME=dreamshaper-canary.example.com \ GIT_BRANCH=main bash setup-vast.sh # 3) Public-path canary:专用 canary tunnel,仍不注册生产心跳。 PLN_GPU_TOKEN=... CLOUDFLARED_TUNNEL_TOKEN=... \ PUBLIC_HOSTNAME=dreamshaper-canary.example.com TUNNEL_ENABLED=true \ HEARTBEAT_ENABLED=false GIT_BRANCH=main bash setup-vast.sh # 4) 生产:仅在命名晋升审批通过后。 PLN_GPU_TOKEN=... CLOUDFLARED_TUNNEL_TOKEN=... \ PUBLIC_HOSTNAME=dreamshaper-vast-NN.example.com TUNNEL_ENABLED=true \ HEARTBEAT_ENABLED=true GIT_BRANCH=main bash setup-vast.sh

镜像选型有实战依据:使用nvidia/cuda:12.8.1-cudnn-runtime-ubuntu24.04运行时镜像即可(DreamShaper 只安装固定版本的 PyTorch CUDA wheel,不编译 CUDA 扩展),更大的开发镜像在 2026-08-19 的替换中曾反复卡在容器创建之前,而运行时镜像启动成功。

4.2 环境变量全集

setup-vast.sh生成的.env.dreamshaper(setup-vast.sh)与 README 声明一致,Worker 侧完整清单为:

变量默认值说明
MODEL_IDLykon/dreamshaper-8基础模型
LCM_LORAlatent-consistency/lcm-lora-sdv1-5LCM LoRA,加载后 fuse
TINY_VAEmadebyollin/taesd微型解码器
NUM_INFERENCE_STEPS3采样步数(服务端决定,忽略调用方)
GUIDANCE_SCALE0.0关闭 CFG
MAX_DIM768单边像素上限
MAX_PIXELS512²总像素上限
PORT8766服务端口
REGISTER_URLhttps://gen.pollinations.ai/register心跳注册端点
SERVICE_TYPEsana注册表池键(兼容保留)
HEARTBEAT_ENABLED—是否发送注册心跳
TUNNEL_ENABLED—是否启动 cloudflared
WORKERS3Uvicorn 进程数
QUEUE_LIMIT2(每进程)每进程最多 1 运行 + 1 排队

4.3 /root/onstart.sh:重启恢复的关键细节

Vast 在容器重启后执行的是/root/onstart.sh。setup-vast.sh 生成的脚本先做彻底清理再拉起服务,这段逻辑解释了为什么"只杀监听端口不够":

pkill -TERM -f "/root/run-dreamshaper.sh" 2>/dev/null || true # 先杀 detached supervisor screen -S dreamshaper -X quit ... pkill -TERM -f "$VENV/bin/python" ... # Uvicorn 派生的 fuser -k -TERM \"$PORT/tcp\" ... # worker 与 resource tracker fuser -k -KILL \"$PORT/tcp\" ... # 可能比父进程活得久

只杀端口监听者是不充分的:spawn 出来的 worker 和 resource tracker 会存活并引发Address already in use死循环。清理完成后,脚本用 screen 会话拉起server.py(带 5 秒重启循环),并在/health就绪后才启动 cloudflared 与 tunnel watchdog。

4.4 替换排空(drain)时的检查点

替换 GPU 前不能只看保存的HEARTBEAT_ENABLED=false设置,而要核对运行中的进程:旧部署的启动脚本可能没有上述清理逻辑,会留下携带旧环境、持续发心跳的 Python 进程。正确做法是停掉那个陈旧的 supervisor/进程树、以禁用注册的方式恢复旧端点,等它的 URL 从/register中消失后再销毁——注册表排除 180 秒无心跳的条目,KV 条目 TTL 为 240 秒(与 availableServers.ts 中SERVER_TIMEOUT = 180000、REGISTRY_TTL_SECONDS = 240一致)。2026-09-26 的替换记录就实际踩到了这个坑:旧主机遗留的启动脚本在标志位改变后仍让陈旧 Python 进程带着心跳运行,被人工停掉后注册表条目才正常过期(GPU_INSTANCES.md)。

5. 请求路径:从model=sana到 Worker 的源码级链路

5.1 gen 侧:加权池选择与 503 跨 Worker 重试

callSelfHostedServer组装请求体时硬编码steps = 4(createAndReturnImages.ts),并携带x-backend-token: $PLN_GPU_TOKEN,随后调用fetchFromWeightedServer("sana", ...)。池选择逻辑(availableServers.ts)按1/lastMs做加权随机——快后端按比例分到更多流量,避免把慢 GPU 和快 GPU 平摊。

fetchFromWeightedServer的重试语义是整条高可用链的支点(availableServers.ts):

  • 仅当请求未被接受时重试其他 Worker:HTTP 503(队列满)、Cloudflare 1033 disconnected-tunnel 错误、或已知的网络故障;
  • 其他 5xx 可能已经开始推理,不重试直接抛出;
  • 成功的响应延迟会被recordLatency写入独立 KV 键,供加权选择使用(心跳写与延迟写分离,防止陈旧心跳冲掉路由权重)。

这解释了 Worker 侧限流设计的动机:Worker 快速 503 不是失败,而是让 gen 立刻换另一台已注册 DreamShaper Worker 的重试信号。

5.2 Worker 侧:队列限流与多进程

server.py中每个 Uvicorn 进程用BoundedSemaphore(QUEUE_LIMIT)实现"1 运行 + 1 等待"(server.py):

# Per Uvicorn worker process: one request runs on the GPU while one may wait. # Further requests receive 503 so gen can retry another registered Vast worker. QUEUE_LIMIT = int(os.getenv("QUEUE_LIMIT", "2"))

超出即503 Queue full。QUEUE_LIMIT是每进程的,因此默认WORKERS=3时单张 GPU 最多容纳 6 个 in-flight 请求,而不是在一块 GPU 上堆出无界队列。

为什么必须多进程?server.py 的注释记录了生产实测:单进程串行在generate_lock上时,GPU 有约 60% 时间空闲(GPU 占用仅 26–45%、单进程封顶约 4.3 img/s)——瓶颈是 Python 路径(GIL 下的 JPEG 编码、base64、HTTP),不是 GPU 或步数。模型只占约 3 GB 显存,多进程各自持有独立管道与锁后 GPU 工作与 Python 开销才能真正重叠。

5.3 心跳注册:30 秒一次,池键固定为 sana

send_heartbeat每 30 秒向https://gen.pollinations.ai/registerPOST{url, type: "sana"}(server.py)。两个陷阱都写进了文档:

  1. 必须传PUBLIC_HOSTNAME:否则 Worker 注册的是裸 VastIP:port,gen Worker 根本取不到该源地址——而心跳仍会报告健康。这次故障在 rollout 中真实发生过;
  2. 池键写死sana:源码注释与VALID_TYPES白名单相互印证,/register拒绝未知类型,所以 Worker 必须使用已部署的路由池键,才能"先注册、后切流"。

6. API 契约:/generate 与 /health

Worker 对外契约与旧 sana Worker 相同(DreamShaper README 的 API 章节):

6.1 POST /generate

要求请求头x-backend-token: $PLN_GPU_TOKEN,缺失或不匹配返回 403(verify_backend_token校验见 server.py)。请求体:

{ "prompts": ["a cat wearing sunglasses"], "width": 512, "height": 512, "steps": 3, "seed": 42 }

尺寸处理规则:按MAX_DIM(768)与MAX_PIXELS(512²)钳制,并使用同一缩放因子保持宽高比,再向下取整到 /32。因此 1024×1024 会返回 512×512,1280×720 会返回 672×384(分别钳制两边曾把 1280×720 变成 512×480,这是修复过的历史 bug)。钳制逻辑与单测一一对应,见 clamp_dims 和 test_server.py:

def clamp_dims(w, h): w, h = max(1, w), max(1, h) scale = min(1.0, MAX_DIM / max(w, h), (MAX_PIXELS / (w * h)) ** 0.5) w = max(32, int(w * scale) // 32 * 32) h = max(32, int(h * scale) // 32 * 32) return w, h

steps字段被接受但刻意忽略——与 sana Worker 行为一致。原因:gen 侧每个请求体都硬编码steps: 4,若尊重调用方步数会覆盖服务端 3 步配置,把 3090 的吞吐从 6.18 img/s 拉到 5.06 img/s,低于该部署按峰值小时容量规划的水位。步数由 Worker 端的NUM_INFERENCE_STEPS唯一决定。

响应是单元素数组,含 base64 JPEG:

[{"image": "<base64>", "has_nsfw_concept": false, "concept": [], "width": 512, "height": 512, "seed": 42, "prompt": "..."}]

6.2 GET /health

{"status": "healthy", "model": "Lykon/dreamshaper-8", "lora": "latent-consistency/lcm-lora-sdv1-5", "steps": 3, "guidance": 0.0}

模型未加载完成时返回 503(Not loaded)。OOM 时进程直接sys.exit(1),由/root/onstart.sh的重启循环兜底拉起。

7. 变更顺序与验证清单:文档反复强调的操作纪律

SERVERS.md与 README 中沉淀了多条"顺序错了就出事"的纪律,汇总如下:

7.1 上线顺序

  • QUEUE_LIMIT 的开启顺序:不要在 gen 生产环境具备跨 Worker 503 重试之前给生产 Worker 启用QUEUE_LIMIT。正确顺序:先把main同步到production、通过 GitHub Actions 部署 gen、确认重试已上线,然后在两台 DreamShaper Worker 上重跑 setup。顺序颠倒会把"饱和"从静默排队变成用户可见的 503。
  • 替换 sana 的顺序:改造前sana走的是绕过注册表池的硬编码后端 URL。必须先部署并确认 Worker 已注册,再合并路由变更;先合并会把所有流量打到空池。

7.2 命名 Tunnel,不要用 quick tunnel

生产必须走命名Cloudflare Tunnel(指向http://localhost:8766),quick tunnel 曾直接造成事故 #12254:free、限流、无 SLA 的trycloudflare.com地址在生产负载下劣化,静态/docs抓取要 15–39 秒(本机 1024×1024 渲染只要 3.9 秒),请求超过 CloudFront 超时后用户看到的是无限挂起而非报错——而 Worker 的心跳全程是绿的。这引出一条普适结论:心跳健康不能证明数据路径可用,必须用真实生成请求验证。

7.3 销毁旧 GPU 前的最终验证

不能只看后端日志(后端 200 不构成退役门槛),还要看最终 API 结果。在 Tinybird 中按model_requested IN ('dreamshaper', 'sana')与is_final过滤——注意resolved_model_requested里存的是规范名lykon/dreamshaper-8-lcm而非公开 slug,2026-09-26 的退役检查就因按公开 slug 过滤而误查成空集。要对比替换前后的完成窗口、区分客户端 4xx 与最终 5xx、并覆盖代表性突发(同次替换通过了 210-RPM canary,却在随后 484 请求/分钟的舰队突发中出现了用户请求被丢弃——支持突发容量不足的判断,属于事后需要跟进的回归,文档要求用单独批准的候选实例恢复容量后再退役其他 Worker)。

7.4 别名路由的自动化测试锚点

仓库测试对这条链路有两处直接锁定,可作为回归依据:

  • 模型列表断言:/image/models返回的lykon/dreamshaper-8-lcm携带aliases: ["sana", "dreamshaper"]与固定单图计价(见 gen.pollinations.ai/test/index.test.ts);
  • 端到端别名路由:请求model=sana时,x-model-used响应头必须为lykon/dreamshaper-8-lcm,且 Tinybird 事件保留调用方实际使用的名称sana(见 gen.pollinations.ai/test/generation-vcr.test.ts)。

8. 小结

SERVERS.md的篇幅虽短,但浓缩了这条路由的全部运维真相:sana同时是公开别名(模型注册表)、池键(VALID_TYPES白名单)与Worker 心跳类型(SERVICE_TYPE)三处的一致残留,其唯一目的是让 Worker 能在路由切换部署前先行注册、消灭空池窗口;生产可用性由两台 Vast Worker、命名 Tunnel、每进程QUEUE_LIMIT=2的快速 503 与 gen 侧跨 Worker 重试共同构成,且没有外部 fallback。理解这些之后,无论是诊断model=sana的请求为什么失败,还是执行下一次 GPU 替换,都应当以"注册表池键是 sana、池内主机名以/register实时查询为准、历史 Sana/SSH-tunnel 资料一律弃用"作为出发点,并结合 GPU_INSTANCES.md 中最新的实例记录与 DreamShaper README 的部署契约执行变更。

  • AI 应用
  • 媒体生成
  • 后端
  • 前端

【免费下载链接】pollinations

Your Friendly Open-Source Gen-AI Platform

项目地址:https://gitcode.com/gh_mirrors/po/pollinations
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进