[diffusion] Refresh eager optimization skills and benchmark safeguards (#35679)
This commit is contained in:
@@ -120,7 +120,7 @@ Once you have the reference code, study it thoroughly:
|
||||
**Before creating any new files, check whether an existing pipeline or stage can be reused or extended.** Only create new pipelines/stages when the existing ones would require extensive modifications or when no similar implementation exists.
|
||||
|
||||
Specifically:
|
||||
1. **Compare the new model's architecture against existing pipelines** before creating files. Current native families include MiniMax-H3, Krea-2, LTX-2/2.3, HunyuanVideo/FastHunyuan, Wan/FastWan/TurboWan/LingBot World/LingBot Video MoE, MOVA, FLUX/FLUX.2/Klein, Z-Image, Qwen-Image/edit/layered, GLM-Image, SD3, Hunyuan3D, Helios, Cosmos3, SANA/SANA-WM, FireRed, ERNIE-Image, JoyAI, and Ideogram4. If the new model shares most of its structure with an existing one (e.g., same text encoders, similar latent format, compatible denoising loop), prefer:
|
||||
1. **Compare the new model's architecture against existing pipelines** before creating files. Current native families include MiniMax-H3, Krea-2, LTX-2/2.3/2.5, HunyuanVideo/FastHunyuan, Wan/FastWan/TurboWan/LingBot World/LingBot Video MoE, MOVA, FLUX/FLUX.2/Klein, LongCat-Image, Z-Image, Qwen-Image/edit/layered, GLM-Image, SD3, Hunyuan3D, Helios, Cosmos3 Nano/Super/Edge/distilled, SANA/SANA-Video/SANA-WM, FireRed, ERNIE-Image, JoyAI, and Ideogram4. If the new model shares most of its structure with an existing one (e.g., same text encoders, similar latent format, compatible denoising loop), prefer:
|
||||
- Adding a new config variant to the existing pipeline rather than creating a new pipeline class
|
||||
- Reusing the existing `BeforeDenoisingStage` with minor parameter differences
|
||||
- Using `add_standard_t2i_stages()` / `add_standard_ti2i_stages()` / `add_standard_ti2v_stages()` if the model fits standard patterns
|
||||
@@ -583,6 +583,7 @@ After implementation, **you must verify that the generated output is not noise**
|
||||
| GLM-Image | `runtime/pipelines/glm_image.py` | `stages/model_specific_stages/glm_image.py` | `configs/pipeline_configs/glm_image.py` |
|
||||
| Qwen-Image-Layered | `runtime/pipelines/qwen_image.py` (`QwenImageLayeredPipeline`) | `stages/model_specific_stages/qwen_image_layered.py` | `configs/pipeline_configs/qwen_image.py` (`QwenImageLayeredPipelineConfig`) |
|
||||
| Cosmos3 | `runtime/pipelines/cosmos3_pipeline.py` | `stages/model_specific_stages/cosmos3.py` | `configs/pipeline_configs/cosmos3.py` |
|
||||
| LongCat-Image | `runtime/pipelines/longcat_image.py` | `stages/model_specific_stages/longcat_image.py` | `configs/pipeline_configs/longcat_image.py` |
|
||||
| ErnieImage | `runtime/pipelines/ernie_image.py` | `stages/model_specific_stages/ernie_image_pe.py` | `configs/pipeline_configs/ernie_image.py` |
|
||||
| Hunyuan3D | `runtime/pipelines/hunyuan3d_pipeline.py` | `stages/model_specific_stages/hunyuan3d/` | `configs/pipeline_configs/hunyuan3d.py` |
|
||||
| SANA-WM | `runtime/pipelines/sana_wm_pipeline.py`, `sana_wm_realtime_pipeline.py` | `stages/model_specific_stages/sana_wm/` | `configs/pipeline_configs/sana_wm.py` |
|
||||
@@ -600,8 +601,9 @@ After implementation, **you must verify that the generated output is not noise**
|
||||
| Z-Image | `runtime/pipelines/zimage_pipeline.py` | Uses standard image pipeline stages plus Z-Image-specific config/model code |
|
||||
| Ideogram4 | `runtime/pipelines/ideogram.py` | Uses dedicated text encoding and denoising stages while keeping standard latent prep |
|
||||
| SANA | `runtime/pipelines/sana.py` | Spatial image pipeline; reuse the spatial image config pattern |
|
||||
| SANA-Video | `runtime/pipelines/sana_video.py` | Native 3D transformer with model-specific text encoding and otherwise standard T2V stages |
|
||||
| Stable Diffusion 3/3.5 | `runtime/pipelines/stable_diffusion_3.py` | Spatial image pipeline; compare scheduler, VAE scale, and conditioning layout |
|
||||
| LTX-2 / LTX-2.3 | `runtime/pipelines/ltx_2_pipeline.py` | Video pipeline family with one-stage, two-stage, and HQ variants |
|
||||
| LTX-2 / LTX-2.3 / LTX-2.5 | `runtime/pipelines/ltx_2_pipeline.py` | Video pipeline family with one-stage, two-stage, HQ, joint audio/video, and optional LTX-2.5 diffusion-decoder variants; prefer config/loader specialization over a new pipeline |
|
||||
| Helios | `runtime/pipelines/helios_pipeline.py` | Video pipeline family with custom denoising and decoding stages |
|
||||
| FireRed/JoyAI image edit | `runtime/pipelines/qwen_image.py`, `runtime/pipelines/joy_image.py` | FireRed reuses Qwen edit-plus config; JoyAI has its own edit pipeline |
|
||||
| Wan | `runtime/pipelines/wan_pipeline.py` | Uses `add_standard_ti2v_stages()` |
|
||||
|
||||
+12
-6
@@ -26,6 +26,9 @@ Before running any benchmark, profiler, or kernel-validation command:
|
||||
- set `SGLANG_DIFFUSION_SYNC_STAGE_PROFILING=1` when comparing stage-level
|
||||
denoise/decode timings; the preset helper sets it by default unless the
|
||||
caller explicitly overrides it
|
||||
- for downloaded checkpoints, use the preset helper's task-owned
|
||||
`--model-cache-root` together with `--cleanup-model-cache`; verify the JSONL
|
||||
ledger reports zero residual weight files before moving to the next model
|
||||
- choose idle GPU(s) before starting perf work
|
||||
|
||||
## Native Backend Gate
|
||||
@@ -45,10 +48,10 @@ If any benchmark, perf-dump, or `torch.profiler` command prints one of those sig
|
||||
|
||||
## Main Reference
|
||||
|
||||
- [benchmark-and-profile.md](benchmark-and-profile.md) — canonical denoise benchmark, perf dump, and `torch.profiler` workflow; uses checked-in nightly-aligned presets plus current-source extras such as MiniMax-H3 joint video/audio T2VA, FLUX.2 Klein, Cosmos3, Ideogram4, ERNIE/GLM/SANA image models, FastWan2.2, `LTX-2.3` one-stage/two-stage/HQ, HunyuanVideo, MOVA, Helios, JoyAI/FireRed image edit, and Hunyuan3D shape
|
||||
- [benchmark-and-profile.md](benchmark-and-profile.md) — canonical denoise benchmark, perf dump, and `torch.profiler` workflow; uses checked-in nightly-aligned presets plus current-source extras such as LongCat-Image, SANA-Video, LingBot Video MoE, Cosmos3 Edge/distilled, LTX-2.5 and its diffusion decoder, MiniMax-H3, FLUX.2 Klein, Ideogram4, ERNIE/GLM/SANA image models, FastWan2.2, `LTX-2.3`, HunyuanVideo, MOVA, Helios, image edit, and Hunyuan3D shape
|
||||
- [existing-fast-paths.md](existing-fast-paths.md) — map bottlenecks to existing fused kernels, packed QKV paths, fused `QK norm + RoPE`, distributed overlap patterns, and open optimization PRs before proposing new code
|
||||
- [scripts/diffusion_skill_env.py](scripts/diffusion_skill_env.py) — preflight helper: repo root discovery via `sglang.__file__`, write-access probe, benchmark/profile output directories, idle GPU selection
|
||||
- [scripts/bench_diffusion_denoise.py](scripts/bench_diffusion_denoise.py) — end-to-end denoise benchmark preset runner via `sglang generate`; supports `--no-torch-compile`, forces the H3 preset to its eager consistency mode, enables synchronized stage attribution for perf dumps, validates nightly preset drift with `--validate-nightly-alignment`, and saves perf dumps by label for `compare_perf.py`
|
||||
- [scripts/bench_diffusion_denoise.py](scripts/bench_diffusion_denoise.py) — end-to-end denoise benchmark preset runner via `sglang generate`; defaults to eager, supports opt-in `--torch-compile`, forces H3 to its eager consistency mode, enables synchronized stage attribution, validates nightly preset drift, and can clean an isolated model cache in a `finally` block with a JSONL ledger
|
||||
|
||||
## Opportunity Discovery Rule
|
||||
|
||||
@@ -59,6 +62,8 @@ Always rule out these existing families first:
|
||||
- LTX upsampler GroupNorm+SiLU
|
||||
- Z-Image bf16-native Triton RMSNorm scale/tanh-residual modulation
|
||||
- SANA packed self-attention Q/K/V and cross-attention K/V GEMMs
|
||||
- SANA-Video reuse of SANA's bit-exact bias/activation, residual-gate, and
|
||||
LayerNorm-modulation fast paths before adding video-only kernels
|
||||
- MiniMax-H3 indexed modulation, fused QK norm + RoPE, packed Ulysses QKV,
|
||||
USP relayout, and batched TP AdaLN collectives
|
||||
- bit-exact diffusion adaLN modulation and fused LayerNorm + modulation for
|
||||
@@ -68,6 +73,8 @@ Always rule out these existing families first:
|
||||
- fused diffusion `QK norm + RoPE`
|
||||
- LTX2 split RoPE
|
||||
- LTX2 residual-gate add
|
||||
- LTX-2.5 diffusion-decoder NATTEN selection before interpreting a
|
||||
FlexAttention fallback trace
|
||||
- varlen USP attention pack/scatter
|
||||
- NVFP4 / Nunchaku packed QKV
|
||||
- Nunchaku fused GELU MLP
|
||||
@@ -77,10 +84,9 @@ Always rule out these existing families first:
|
||||
- breakable CUDA graph capture for supported fixed-resolution pipelines
|
||||
- dual-stream diffusion execution
|
||||
|
||||
If the user explicitly requires `torch.compile` to stay off, do not use the
|
||||
default benchmark preset invocation unchanged. Either pass the checked-in
|
||||
benchmark helper its no-compile switch or run the equivalent manual command
|
||||
without `--enable-torch-compile`.
|
||||
The checked-in helper defaults to eager. Use `--torch-compile` only for a
|
||||
controlled comparator, never for the eager ground truth. The legacy
|
||||
`--no-torch-compile` spelling remains accepted but is redundant.
|
||||
|
||||
MiniMax-H3 is always an eager consistency case on current main. Use
|
||||
`--model minimax-h3-t2va`; its preset writes the H3 request fields through a
|
||||
|
||||
+27
-7
@@ -135,20 +135,32 @@ PYTHONPATH=python python3 "$BENCH_PY" \
|
||||
--output-dir "${BENCH_DIR}"
|
||||
```
|
||||
|
||||
The helper defaults to eager. Add `--torch-compile` only for a labeled compile
|
||||
control. `--no-torch-compile` remains accepted for compatibility but is no
|
||||
longer required.
|
||||
|
||||
The helper sets `SGLANG_DIFFUSION_SYNC_STAGE_PROFILING=1` for accurate stage
|
||||
attribution. Set it to `0` explicitly only when collecting an e2e-only run and
|
||||
do not compare its per-stage values with synchronized results.
|
||||
|
||||
Keep `torch.compile` off when the task requires it:
|
||||
For downloaded checkpoints, isolate and clean the model cache after the preset
|
||||
finishes. Cleanup also runs after an error or interruption, and appends a JSONL
|
||||
record with pre/post byte and weight-file counts:
|
||||
|
||||
```bash
|
||||
MODEL_CACHE_ROOT=/path/to/task-owned/model-caches
|
||||
PYTHONPATH=python python3 "$BENCH_PY" \
|
||||
--model flux \
|
||||
--model longcat-image \
|
||||
--label baseline \
|
||||
--output-dir "${BENCH_DIR}" \
|
||||
--no-torch-compile
|
||||
--model-cache-root "${MODEL_CACHE_ROOT}" \
|
||||
--cleanup-model-cache
|
||||
```
|
||||
|
||||
The helper refuses to reuse an existing per-run cache directory and never
|
||||
redirects `SGLANG_CACHE_DIR`, so compiled kernel caches remain separate. Never
|
||||
point this option at a shared Hugging Face or ModelScope cache.
|
||||
|
||||
Run the `LTX-2.3` one-stage skill preset:
|
||||
|
||||
```bash
|
||||
@@ -177,7 +189,7 @@ PYTHONPATH=python python3 "$BENCH_PY" \
|
||||
```
|
||||
|
||||
Run the current-source MiniMax-H3 T2VA preset. The helper forces eager mode
|
||||
for this model even when its global compile default is enabled:
|
||||
for this model even when `--torch-compile` is requested:
|
||||
|
||||
```bash
|
||||
export CUDA_VISIBLE_DEVICES=$(python3 "$ENV_PY" print-idle-gpus --count 4)
|
||||
@@ -215,8 +227,8 @@ Use the preset categories this way:
|
||||
|
||||
| Preset | Model | Nightly | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `flux` | `black-forest-labs/FLUX.1-dev` | Yes: `flux1_dev_t2i_1024` | Prompt, 1024x1024, seed 42, 2 GPUs, TP size 2, `--dit-layerwise-offload false`; no explicit steps/guidance override |
|
||||
| `flux2` | `black-forest-labs/FLUX.2-dev` | Yes: `flux2_dev_t2i_1024` | Prompt, 1024x1024, seed 42, 2 GPUs, TP size 2, `--dit-layerwise-offload false`; no explicit steps/guidance override |
|
||||
| `flux` | `black-forest-labs/FLUX.1-dev` | Yes: `flux1_dev_t2i_1024` | Prompt, 1024x1024, seed 42, 2 GPUs, TP size 2, resident DiT; no explicit steps/guidance override |
|
||||
| `flux2` | `black-forest-labs/FLUX.2-dev` | Yes: `flux2_dev_t2i_1024` | Prompt, 1024x1024, seed 42, 2 GPUs, TP size 2, resident DiT; no explicit steps/guidance override |
|
||||
| `qwen` | `Qwen/Qwen-Image-2512` | Yes: `qwen_image_2512_t2i_1024` | Prompt, 1024x1024, seed 42, 2 GPUs, TP size 2; no explicit steps/guidance override |
|
||||
| `qwen-edit` | `Qwen/Qwen-Image-Edit-2511` | Yes: `qwen_image_edit_2511` | Uses the nightly cat image and edit prompt, 2 GPUs, TP size 2 |
|
||||
| `zimage` | `Tongyi-MAI/Z-Image-Turbo` | Yes: `zimage_turbo_t2i_1024` | Prompt, 1024x1024, seed 42, 2 GPUs, TP size 2; no explicit steps/guidance override |
|
||||
@@ -226,7 +238,14 @@ Use the preset categories this way:
|
||||
| `ideogram4-fp8` | `ideogram-ai/ideogram-4-fp8` | Yes: `ideogram4_fp8_t2i_2gpu` | Prompt, 1024x1024, seed 42, 2 GPUs, TP size 2, FlashAttention backend; sampling preset owns steps/guidance |
|
||||
| `cosmos3-super-t2v` | `nvidia/Cosmos3-Super` | Yes: `cosmos3_super_t2v_2gpu` | Prompt, 1280x720, 81 frames, seed 42, 2 GPUs, TP size 2, guardrails disabled for benchmark isolation |
|
||||
| `wan-i2v` | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | Yes: `wan22_i2v_a14b_720p` | Nightly cat image and motion prompt, 1280x720, 81 frames, 4 GPUs, CFG parallel, Ulysses degree 2, text encoder CPU offload and pinned CPU memory |
|
||||
| `minimax-h3-t2va` | `MiniMaxAI/MiniMax-H3` | No | Current-source H3 FL2VA-partition T2VA baseline: 1344x768 resolved canvas, 5 seconds / 124 frames at 24 fps, 50 joint video-audio steps, 4 GPUs, TP2 + Ulysses2, eager BF16/FP32. The helper writes H3's `task`, `conditions`, `target`, and audio/video flow shifts to a generated config. |
|
||||
| `minimax-h3-t2va` | `MiniMaxAI/MiniMax-H3` | Yes: `minimax_h3_t2va_5s` | H3 FL2VA-partition T2VA baseline: 1344x768 resolved canvas, 5 seconds / 124 frames at 24 fps, 50 joint video-audio steps, 4 GPUs, TP2 + Ulysses2, eager BF16/FP32. The helper writes H3's request contract to a generated config. |
|
||||
| `longcat-image` | `meituan-longcat/LongCat-Image` | No | Eager DiT baseline at 1024x1024, 50 steps, guidance 4.5; prompt rewrite is disabled so Qwen2.5-VL does not contaminate the DiT A/B. |
|
||||
| `sana-video` | `Efficient-Large-Model/SANA-Video_2B_480p_diffusers` | No | CI-sized eager T2V baseline: 832x480, 17 frames, 8 steps, guidance 6.0. |
|
||||
| `lingbot-video-moe` | `robbyant/lingbot-video-moe-30b-a3b` | No | One-GPU eager baseline using the CI structured-JSON caption, 384x640, 17 frames, 12 steps, and text-encoder CPU offload. |
|
||||
| `cosmos3-edge-t2i` | `nvidia/Cosmos3-Edge` | No | One-GPU eager T2I baseline at Edge's native 640x640 shape, 35 steps, guidance 7.0. |
|
||||
| `cosmos3-super-t2i-distilled` | `nvidia/Cosmos3-Super-Text2Image-4Step` | No | Four-GPU eager distilled T2I baseline. The checkpoint owns its fixed sigma schedule; the preset does not override the step count. |
|
||||
| `ltx25` | `Lightricks/LTX-2.5-Diffusers` | No | One-stage distilled eager baseline at 960x544, 121 frames, 8 steps, guidance 1.0. |
|
||||
| `ltx25-diffusion-decoder` | `Lightricks/LTX-2.5-Diffusers` | No | Same fixed DiT workload with `--use-diffusion-decoder`; attribute decoder time separately and confirm NATTEN `na3d` is active. |
|
||||
| `ltx2` | `Lightricks/LTX-2` | No | Current-source two-stage LTX-2 preset with 2 GPUs, CFG parallel, 768x512, 121 frames |
|
||||
| `qwen-image` | `Qwen/Qwen-Image` | No | Current-source extra covering the base Qwen-Image native path, separate from the nightly `Qwen-Image-2512` case |
|
||||
| `qwen-edit-2509` | `Qwen/Qwen-Image-Edit-2509` | No | Current-source extra for the pre-2511 edit-plus path; uses the cat image, 1024x1024 |
|
||||
@@ -658,4 +677,5 @@ This skill intentionally stops here. It tells you whether you are looking at:
|
||||
- [ ] one representative `torch.profiler` trace saved
|
||||
- [ ] hotspot classified against `existing-fast-paths.md`
|
||||
- [ ] reference image or video checked for correctness
|
||||
- [ ] task-owned checkpoint cache cleaned and ledger shows zero residual weight files
|
||||
- [ ] any remaining kernel work handed off with perf/profile evidence attached
|
||||
|
||||
+38
-3
@@ -32,6 +32,12 @@ framework-specific optimization workflow.
|
||||
- `python/sglang/kernels/ops/diffusion/layout/usp_relayout_jit.py`
|
||||
- `python/sglang/multimodal_gen/runtime/layers/usp.py`
|
||||
- `python/sglang/multimodal_gen/runtime/models/dits/minimax_h3.py`
|
||||
- `python/sglang/multimodal_gen/runtime/models/dits/longcat_image.py`
|
||||
- `python/sglang/multimodal_gen/runtime/models/dits/sana_video.py`
|
||||
- `python/sglang/multimodal_gen/runtime/models/dits/lingbot_video_moe.py`
|
||||
- `python/sglang/multimodal_gen/runtime/models/decoders/ltx_2_5_diffusion_decoder.py`
|
||||
- `python/sglang/multimodal_gen/runtime/layers/moe.py`
|
||||
- `python/sglang/srt/layers/moe/topk.py`
|
||||
- `python/sglang/kernels/ops/diffusion/modulate/residual_gate_add_jit.py`
|
||||
- `python/sglang/kernels/jit/csrc/diffusion/residual_gate_add.cuh`
|
||||
- `python/sglang/kernels/ops/diffusion/layout/varlen_pack_pad_triton.py`
|
||||
@@ -289,6 +295,28 @@ framework-specific optimization workflow.
|
||||
- Scope: this is a mainline SANA model fast path. Query projection in cross-attention remains separate because it uses denoising hidden states, while K/V share step-invariant encoder hidden states.
|
||||
- Workflow rule: if a SANA trace shows separate self-attention `to_q`, `to_k`, `to_v` GEMMs, or separate cross-attention `to_k` and `to_v` GEMMs, treat that as a regressed existing packed-projection path before proposing a new GEMM fusion.
|
||||
|
||||
**Recent Model Audit Boundaries**
|
||||
|
||||
- LongCat-Image currently has split image/text QKV projections and performs
|
||||
joint-stream `cat`/split inside each single block. Do not misclassify those
|
||||
as a missed existing packed path; they are model-local structural
|
||||
opportunities that need their own weight-loader and parity coverage.
|
||||
- SANA-Video already packs self QKV and cross KV. Its conv/modulation formulas
|
||||
mirror SANA, but it does not yet call SANA's bit-exact bias-SiLU, bias-GLU,
|
||||
residual-gate, LayerNorm-modulation, or one-time contiguous-layout helpers.
|
||||
Reuse or extract those helpers before authoring a video-only kernel.
|
||||
- LingBot Video MoE's router implements sigmoid+bias grouped top-k in
|
||||
`multimodal_gen/runtime/layers/moe.py`. Check parameter and output-order
|
||||
compatibility with `srt/layers/moe/topk.py::biased_grouped_topk` before
|
||||
writing a new router kernel.
|
||||
- LTX-2.5 reuses the mature LTX-2 DiT paths. Treat the optional diffusion
|
||||
decoder separately: confirm NATTEN `na3d` is active, then inspect its
|
||||
per-block 3D RoPE construction and split QKV/SwiGLU projections.
|
||||
- Cosmos3 Edge inherits the existing Cosmos3 attention-prep fusions. Profile
|
||||
the dense squared-ReLU MLP before proposing another Cosmos kernel, and do not
|
||||
repeat the closed experimental Cosmos BCG direction without solving its
|
||||
model-state lifecycle problem.
|
||||
|
||||
**Common Entry Points in Diffusion Models**
|
||||
- AdaLN modulation: `LayerNormScaleShift`, `RMSNormScaleShift`, `ScaleResidual*` in `layernorm.py`.
|
||||
- Bit-exact adaLN modulation / LayerNorm folding: `modulate_scale_shift` and
|
||||
@@ -339,9 +367,10 @@ framework-specific optimization workflow.
|
||||
- Breakable CUDA graph: `runtime/breakable_cuda_graph/runner.py` captures
|
||||
fixed-resolution DiT segments around eager attention/collectives for
|
||||
supported pipelines. It is mutually exclusive with `torch.compile` and
|
||||
Cache-DiT, requires every served resolution in `--warmup-resolutions`, and
|
||||
uses `--bcg-text-buckets` for prompt signatures. Check this path before
|
||||
proposing a second graph-capture mechanism for launch-bound traces.
|
||||
Cache-DiT. The model's default resolution is captured automatically; put
|
||||
every additional served resolution in `--warmup-resolutions`, and use
|
||||
`--bcg-text-buckets` for prompt signatures. Check this path before proposing
|
||||
a second graph-capture mechanism for launch-bound traces.
|
||||
- Dual-stream diffusion models: `use_dual_stream = True` in models such as `hunyuan3d.py` is an existing overlap family.
|
||||
- Workflow rule: if a hotspot is communication-heavy, rule out these in-repo overlap families before proposing a brand new overlap design.
|
||||
|
||||
@@ -361,6 +390,9 @@ relying on any file path, flag, or claim about whether the work has merged.
|
||||
- #20429 Qwen-Image layernorm and `fuse_scale_shift_gate_select01` work.
|
||||
- #20530 MOVA fused RMSNorm + interleaved RoPE.
|
||||
- #29361 LTX2 residual-gate CUDA fast path for `residual + update * gate`.
|
||||
- #34172 LTX2 quality-high fusion; #34305/#34314 Ideogram eager fusions.
|
||||
- #34584 Wan TI2V modulation/RoPE; #34616 FLUX2; #34617 Hunyuan;
|
||||
#34619 GLM; #34620 ERNIE; #34928 SANA; #34932 Cosmos3.
|
||||
- VAE and decode-side acceleration:
|
||||
- #22531 LTX2 parallel VAE support and #20927 batched tiled VAE decode (draft).
|
||||
- Attention, communication, and runtime scheduling:
|
||||
@@ -375,6 +407,9 @@ relying on any file path, flag, or claim about whether the work has merged.
|
||||
- #20447 TeaCache support for GLM-Image, Qwen-Image, and related models.
|
||||
- #19516 Qwen-Image CUDA Graph.
|
||||
- #21912 Z-Image Turbo FP8 full quantization and CUDA Graph.
|
||||
- #34174 automatic default-resolution BCG warmup; #34210 Z-Image BCG
|
||||
correctness; #34929 LTX2.3 BCG. #34618 is a closed Cosmos BCG experiment,
|
||||
not a reusable mainline fast path.
|
||||
|
||||
**Constraints and Fallbacks**
|
||||
- `scale_shift` Triton requires CUDA + contiguous `x`. NPU swaps to native.
|
||||
|
||||
+439
-26
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
End-to-end denoise-stage benchmark presets for SGLang Diffusion.
|
||||
|
||||
@@ -12,6 +13,12 @@ Usage:
|
||||
# Tag the run for later compare_perf.py usage
|
||||
python3 python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/scripts/bench_diffusion_denoise.py --model flux --label tuned
|
||||
|
||||
# Opt in to a compile control (presets are eager by default)
|
||||
python3 python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/scripts/bench_diffusion_denoise.py --model flux --torch-compile
|
||||
|
||||
# Clean an isolated model cache even if the run fails or is interrupted
|
||||
python3 python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/scripts/bench_diffusion_denoise.py --model longcat-image --model-cache-root /task/model-caches --cleanup-model-cache
|
||||
|
||||
# All preset models
|
||||
python3 python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/scripts/bench_diffusion_denoise.py --all
|
||||
|
||||
@@ -33,17 +40,17 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from diffusion_skill_env import ( # noqa: E402
|
||||
from diffusion_skill_env import (
|
||||
ensure_dir,
|
||||
get_assets_dir,
|
||||
get_output_dir,
|
||||
@@ -69,6 +76,15 @@ DIFFUSERS_FALLBACK_SIGNALS = (
|
||||
)
|
||||
CATALOG_TABLE_WIDTH = 140
|
||||
RESULTS_TABLE_WIDTH = 105
|
||||
MODEL_CACHE_MARKER = ".sglang-diffusion-benchmark-cache"
|
||||
MODEL_WEIGHT_SUFFIXES = {
|
||||
".bin",
|
||||
".ckpt",
|
||||
".gguf",
|
||||
".pt",
|
||||
".pth",
|
||||
".safetensors",
|
||||
}
|
||||
NIGHTLY_PRESET_ORDER = (
|
||||
"flux",
|
||||
"flux2",
|
||||
@@ -81,6 +97,58 @@ NIGHTLY_PRESET_ORDER = (
|
||||
"ideogram4-fp8",
|
||||
"cosmos3-super-t2v",
|
||||
"wan-i2v",
|
||||
"minimax-h3-t2va",
|
||||
)
|
||||
|
||||
LINGBOT_VIDEO_PROMPT = json.dumps(
|
||||
{
|
||||
"comprehensive_description": {
|
||||
"scene_content_description": (
|
||||
"A small silver robot arm on a white table slowly reaches "
|
||||
"toward a red cube. The background is a plain, softly lit "
|
||||
"laboratory wall."
|
||||
),
|
||||
"camera_movement_description": (
|
||||
"The camera is static at eye level, medium shot, with the "
|
||||
"robot arm centered and in sharp focus."
|
||||
),
|
||||
},
|
||||
"camera_info": {
|
||||
"color": "Neutral",
|
||||
"frame_size": "Medium",
|
||||
"shot_type_angle": "Eye level",
|
||||
"lens_size": "Medium",
|
||||
"composition": "Center",
|
||||
"lighting": "Soft light",
|
||||
"lighting_type": "Artificial light",
|
||||
},
|
||||
"world_knowledge": [],
|
||||
"prominent_elements": [
|
||||
{
|
||||
"name": "robot arm",
|
||||
"description": "A small silver robot arm with a two-finger gripper.",
|
||||
"actions": [
|
||||
{
|
||||
"timestamp": "[0.0s - 1.0s]",
|
||||
"action": "reaches toward the red cube",
|
||||
}
|
||||
],
|
||||
"location": "center of the frame",
|
||||
"relative_size": "dominant",
|
||||
"shape_and_color": "articulated silver metal arm",
|
||||
"texture": "brushed metal",
|
||||
"appearance_details": "two-finger gripper, visible joints",
|
||||
"relationship": "reaching toward the red cube on the table",
|
||||
"orientation": "upright, base on the table",
|
||||
"pose": "reaching",
|
||||
"expression": "",
|
||||
"clothing": "",
|
||||
"gender": "",
|
||||
"skin_tone_and_texture": "",
|
||||
}
|
||||
],
|
||||
},
|
||||
separators=(",", ":"),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -100,8 +168,7 @@ MODELS = {
|
||||
"--height=1024",
|
||||
"--num-gpus=2",
|
||||
"--tp-size=2",
|
||||
"--dit-layerwise-offload",
|
||||
"false",
|
||||
"--component-residency=dit=resident",
|
||||
],
|
||||
},
|
||||
# 2. Nightly: flux2_dev_t2i_1024
|
||||
@@ -114,8 +181,7 @@ MODELS = {
|
||||
"--height=1024",
|
||||
"--num-gpus=2",
|
||||
"--tp-size=2",
|
||||
"--dit-layerwise-offload",
|
||||
"false",
|
||||
"--component-residency=dit=resident",
|
||||
],
|
||||
},
|
||||
# 3. Nightly: qwen_image_2512_t2i_1024
|
||||
@@ -248,11 +314,12 @@ MODELS = {
|
||||
"--pin-cpu-memory",
|
||||
],
|
||||
},
|
||||
# Source-tracked extras from current registry / GPU test coverage.
|
||||
# 12. Nightly: minimax_h3_t2va_5s
|
||||
# MiniMax-H3 owns its temporal canvas through target.duration_seconds, so
|
||||
# the model-specific sampling fields are passed through --config instead
|
||||
# of generic --width/--height/--num-frames flags.
|
||||
"minimax-h3-t2va": {
|
||||
"nightly_case_id": "minimax_h3_t2va_5s",
|
||||
"path": "MiniMaxAI/MiniMax-H3",
|
||||
"prompt": "At night, while their owner sleeps in a bedroom, three cats march in loudly playing tiny brass instruments, then abruptly file out.",
|
||||
"seed": 1101,
|
||||
@@ -280,6 +347,115 @@ MODELS = {
|
||||
# torch.compile changes numerical output, so never add the global
|
||||
# helper default --enable-torch-compile flag for this preset.
|
||||
"force_eager": True,
|
||||
"nightly_cli_ignored": {
|
||||
"width",
|
||||
"height",
|
||||
"num-frames",
|
||||
"fps",
|
||||
"num-inference-steps",
|
||||
},
|
||||
},
|
||||
# Source-tracked extras from current registry / GPU test coverage.
|
||||
"longcat-image": {
|
||||
"path": "meituan-longcat/LongCat-Image",
|
||||
"prompt": "A red panda reading a book beside a sunlit window.",
|
||||
"extra_args": [
|
||||
"--width=1024",
|
||||
"--height=1024",
|
||||
"--num-inference-steps=50",
|
||||
"--guidance-scale=4.5",
|
||||
"--enable-prompt-rewrite=false",
|
||||
"--performance-mode=manual",
|
||||
],
|
||||
},
|
||||
"sana-video": {
|
||||
"path": "Efficient-Large-Model/SANA-Video_2B_480p_diffusers",
|
||||
"prompt": "A curious raccoon walks through a sunlit forest. motion score: 30.",
|
||||
"extra_args": [
|
||||
"--width=832",
|
||||
"--height=480",
|
||||
"--num-frames=17",
|
||||
"--fps=16",
|
||||
"--num-inference-steps=8",
|
||||
"--guidance-scale=6.0",
|
||||
"--performance-mode=manual",
|
||||
],
|
||||
},
|
||||
"lingbot-video-moe": {
|
||||
"path": "robbyant/lingbot-video-moe-30b-a3b",
|
||||
"prompt": LINGBOT_VIDEO_PROMPT,
|
||||
"seed": 0,
|
||||
"extra_args": [
|
||||
"--width=384",
|
||||
"--height=640",
|
||||
"--num-frames=17",
|
||||
"--fps=16",
|
||||
"--num-inference-steps=12",
|
||||
"--text-encoder-cpu-offload",
|
||||
"--performance-mode=manual",
|
||||
],
|
||||
},
|
||||
"cosmos3-edge-t2i": {
|
||||
"path": "nvidia/Cosmos3-Edge",
|
||||
"prompt": "A warehouse robot folds a blue cloth on a clean workbench.",
|
||||
"seed": 0,
|
||||
"env": {
|
||||
"SGLANG_DISABLE_COSMOS3_GUARDRAILS": "1",
|
||||
},
|
||||
"extra_args": [
|
||||
"--width=640",
|
||||
"--height=640",
|
||||
"--num-frames=1",
|
||||
"--num-inference-steps=35",
|
||||
"--guidance-scale=7.0",
|
||||
"--performance-mode=manual",
|
||||
],
|
||||
},
|
||||
"cosmos3-super-t2i-distilled": {
|
||||
"path": "nvidia/Cosmos3-Super-Text2Image-4Step",
|
||||
"prompt": "A warehouse robot folds a blue cloth on a clean workbench.",
|
||||
"seed": 0,
|
||||
"env": {
|
||||
"SGLANG_DISABLE_COSMOS3_GUARDRAILS": "1",
|
||||
},
|
||||
"extra_args": [
|
||||
"--width=640",
|
||||
"--height=640",
|
||||
"--num-frames=1",
|
||||
"--guidance-scale=1.0",
|
||||
"--num-gpus=4",
|
||||
"--tp-size=4",
|
||||
"--performance-mode=manual",
|
||||
],
|
||||
},
|
||||
"ltx25": {
|
||||
"path": "Lightricks/LTX-2.5-Diffusers",
|
||||
"prompt": "A cat and a dog baking a cake together in a kitchen.",
|
||||
"extra_args": [
|
||||
"--pipeline-class-name=LTX2Pipeline",
|
||||
"--width=960",
|
||||
"--height=544",
|
||||
"--num-frames=121",
|
||||
"--fps=24",
|
||||
"--num-inference-steps=8",
|
||||
"--guidance-scale=1.0",
|
||||
"--performance-mode=manual",
|
||||
],
|
||||
},
|
||||
"ltx25-diffusion-decoder": {
|
||||
"path": "Lightricks/LTX-2.5-Diffusers",
|
||||
"prompt": "A cat and a dog baking a cake together in a kitchen.",
|
||||
"extra_args": [
|
||||
"--pipeline-class-name=LTX2Pipeline",
|
||||
"--width=960",
|
||||
"--height=544",
|
||||
"--num-frames=121",
|
||||
"--fps=24",
|
||||
"--num-inference-steps=8",
|
||||
"--guidance-scale=1.0",
|
||||
"--use-diffusion-decoder",
|
||||
"--performance-mode=manual",
|
||||
],
|
||||
},
|
||||
"ltx2": {
|
||||
"path": "Lightricks/LTX-2",
|
||||
@@ -618,6 +794,123 @@ def model_nightly_case_id(model_key: str) -> str:
|
||||
return MODELS[model_key].get("nightly_case_id", "-")
|
||||
|
||||
|
||||
def _safe_cache_component(value: str) -> str:
|
||||
component = "".join(
|
||||
character if character.isalnum() or character in "-_." else "_"
|
||||
for character in value
|
||||
).strip(".")
|
||||
if not component:
|
||||
raise ValueError(f"Cannot derive a cache directory name from {value!r}")
|
||||
return component
|
||||
|
||||
|
||||
def _prepare_model_cache(cache_root: Path, model_key: str, label: str) -> Path:
|
||||
cache_root = cache_root.expanduser().resolve()
|
||||
unsafe_roots = {Path("/"), Path.home().resolve(), REPO_ROOT.resolve()}
|
||||
if cache_root in unsafe_roots:
|
||||
raise ValueError(
|
||||
"Refusing to use a broad or shared directory as the isolated model "
|
||||
f"cache root: {cache_root}"
|
||||
)
|
||||
|
||||
cache_root.mkdir(parents=True, exist_ok=True)
|
||||
marker = cache_root / MODEL_CACHE_MARKER
|
||||
if not marker.exists():
|
||||
marker.write_text(
|
||||
"Owned by bench_diffusion_denoise.py. Only generated child caches "
|
||||
"may be removed.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
cache_dir = cache_root / (
|
||||
f"{_safe_cache_component(model_key)}-{_safe_cache_component(label)}"
|
||||
)
|
||||
if cache_dir.exists():
|
||||
raise FileExistsError(
|
||||
"The isolated model cache already exists. Refusing to reuse or "
|
||||
f"delete it without inspection: {cache_dir}"
|
||||
)
|
||||
cache_dir.mkdir()
|
||||
return cache_dir
|
||||
|
||||
|
||||
def _model_cache_env(cache_dir: Path) -> dict[str, str]:
|
||||
huggingface_root = cache_dir / "huggingface"
|
||||
huggingface_hub = huggingface_root / "hub"
|
||||
return {
|
||||
"HF_HOME": str(huggingface_root),
|
||||
"HF_ASSETS_CACHE": str(huggingface_root / "assets"),
|
||||
"HF_HUB_CACHE": str(huggingface_hub),
|
||||
"HF_MODULES_CACHE": str(huggingface_root / "modules"),
|
||||
"HF_XET_CACHE": str(huggingface_root / "xet"),
|
||||
"HUGGINGFACE_HUB_CACHE": str(huggingface_hub),
|
||||
"DIFFUSERS_CACHE": str(huggingface_hub),
|
||||
"TRANSFORMERS_CACHE": str(huggingface_hub),
|
||||
"MODELSCOPE_CACHE": str(cache_dir / "modelscope"),
|
||||
"MODELSCOPE_MODULES_CACHE": str(cache_dir / "modelscope" / "modules"),
|
||||
}
|
||||
|
||||
|
||||
def _cache_stats(cache_dir: Path) -> dict[str, int]:
|
||||
file_count = 0
|
||||
weight_file_count = 0
|
||||
total_bytes = 0
|
||||
if not cache_dir.exists():
|
||||
return {
|
||||
"file_count": 0,
|
||||
"weight_file_count": 0,
|
||||
"total_bytes": 0,
|
||||
}
|
||||
|
||||
for entry in cache_dir.rglob("*"):
|
||||
if not (entry.is_file() or entry.is_symlink()):
|
||||
continue
|
||||
file_count += 1
|
||||
total_bytes += entry.lstat().st_size
|
||||
if entry.suffix.lower() in MODEL_WEIGHT_SUFFIXES:
|
||||
weight_file_count += 1
|
||||
return {
|
||||
"file_count": file_count,
|
||||
"weight_file_count": weight_file_count,
|
||||
"total_bytes": total_bytes,
|
||||
}
|
||||
|
||||
|
||||
def _cleanup_model_cache(
|
||||
cache_root: Path,
|
||||
cache_dir: Path,
|
||||
ledger_path: Path,
|
||||
model_key: str,
|
||||
label: str,
|
||||
exit_reason: str,
|
||||
) -> dict[str, object]:
|
||||
cache_root = cache_root.expanduser().resolve()
|
||||
cache_dir = cache_dir.resolve()
|
||||
if not (cache_root / MODEL_CACHE_MARKER).is_file():
|
||||
raise RuntimeError(f"Missing isolated-cache ownership marker: {cache_root}")
|
||||
if cache_dir.parent != cache_root:
|
||||
raise RuntimeError(
|
||||
f"Refusing to remove cache outside the isolated root: {cache_dir}"
|
||||
)
|
||||
|
||||
before = _cache_stats(cache_dir)
|
||||
shutil.rmtree(cache_dir)
|
||||
after = _cache_stats(cache_dir)
|
||||
record: dict[str, object] = {
|
||||
"model": model_key,
|
||||
"label": label,
|
||||
"exit_reason": exit_reason,
|
||||
"cache_dir": str(cache_dir),
|
||||
"cleaned_at_unix_s": time.time(),
|
||||
"before": before,
|
||||
"after": after,
|
||||
}
|
||||
ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with ledger_path.open("a", encoding="utf-8") as ledger:
|
||||
ledger.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
return record
|
||||
|
||||
|
||||
def _parse_cli_args(args: list[str]) -> dict[str, object]:
|
||||
parsed: dict[str, object] = {}
|
||||
i = 0
|
||||
@@ -669,7 +962,7 @@ def _expected_nightly_cli_args(case: dict) -> dict[str, str]:
|
||||
# switch. It is not a valid ``sglang generate`` flag after the
|
||||
# warmup-mode migration, so exclude both spellings from preset drift
|
||||
# validation.
|
||||
if flag in {"enable-torch-compile", "warmup", "warmup-mode"}:
|
||||
if flag in {"warmup", "warmup-mode"}:
|
||||
continue
|
||||
expected[flag] = _normalize_cli_value(value)
|
||||
|
||||
@@ -716,11 +1009,32 @@ def validate_nightly_alignment() -> int:
|
||||
if preset.get("env", {}) != case["frameworks"]["sglang"].get("extra_env", {}):
|
||||
errors.append(f"{model_key}: environment differs")
|
||||
|
||||
if "config_overrides" in preset:
|
||||
expected_config = {
|
||||
key: value
|
||||
for key, value in case.get("sglang_request_extra", {}).items()
|
||||
if value is not None
|
||||
}
|
||||
if "num_inference_steps" in case:
|
||||
expected_config["num_inference_steps"] = case["num_inference_steps"]
|
||||
if preset["config_overrides"] != expected_config:
|
||||
errors.append(
|
||||
f"{model_key}: generated request config differs\n"
|
||||
f" skill={preset['config_overrides']}\n"
|
||||
f" ci={expected_config}"
|
||||
)
|
||||
|
||||
ignored_args = set(preset.get("nightly_cli_ignored", set()))
|
||||
actual_args = {
|
||||
key: _normalize_cli_value(value)
|
||||
for key, value in _parse_cli_args(preset["extra_args"]).items()
|
||||
if key not in ignored_args
|
||||
}
|
||||
expected_args = {
|
||||
key: value
|
||||
for key, value in _expected_nightly_cli_args(case).items()
|
||||
if key not in ignored_args
|
||||
}
|
||||
expected_args = _expected_nightly_cli_args(case)
|
||||
if actual_args != expected_args:
|
||||
errors.append(
|
||||
f"{model_key}: CLI args differ\n"
|
||||
@@ -761,12 +1075,12 @@ def print_model_catalog():
|
||||
|
||||
def build_sglang_cmd(
|
||||
model_key: str,
|
||||
perf_dump_path: Optional[str] = None,
|
||||
perf_dump_path: str | None = None,
|
||||
warmup: bool = True,
|
||||
torch_compile: bool = True,
|
||||
torch_compile: bool = False,
|
||||
seed: int = 42,
|
||||
save_output: bool = True,
|
||||
artifact_dir: Optional[Path] = None,
|
||||
artifact_dir: Path | None = None,
|
||||
) -> list[str]:
|
||||
"""
|
||||
Build the `sglang generate` command for the given model.
|
||||
@@ -818,12 +1132,13 @@ def build_sglang_cmd(
|
||||
return cmd
|
||||
|
||||
|
||||
def run_benchmark_once(
|
||||
def _run_benchmark_once_impl(
|
||||
model_key: str,
|
||||
label: str,
|
||||
output_dir: Path,
|
||||
warmup: bool = True,
|
||||
torch_compile: bool = True,
|
||||
torch_compile: bool = False,
|
||||
model_cache_dir: Path | None = None,
|
||||
) -> dict:
|
||||
"""Run a single benchmark pass and return results dict."""
|
||||
perf_path = output_dir / f"{model_key}_{label}.json"
|
||||
@@ -846,6 +1161,8 @@ def run_benchmark_once(
|
||||
cfg = MODELS[model_key]
|
||||
for key, value in cfg.get("env", {}).items():
|
||||
env.setdefault(key, str(value))
|
||||
if model_cache_dir is not None:
|
||||
env.update(_model_cache_env(model_cache_dir))
|
||||
if env.get("HF_TOKEN") and not env.get("HUGGINGFACE_HUB_TOKEN"):
|
||||
env["HUGGINGFACE_HUB_TOKEN"] = env["HF_TOKEN"]
|
||||
|
||||
@@ -883,10 +1200,20 @@ def run_benchmark_once(
|
||||
)
|
||||
fallback_detected = False
|
||||
assert process.stdout is not None
|
||||
for line in process.stdout:
|
||||
print(line, end="")
|
||||
if any(signal in line.lower() for signal in DIFFUSERS_FALLBACK_SIGNALS):
|
||||
fallback_detected = True
|
||||
try:
|
||||
for line in process.stdout:
|
||||
print(line, end="")
|
||||
if any(signal in line.lower() for signal in DIFFUSERS_FALLBACK_SIGNALS):
|
||||
fallback_detected = True
|
||||
except BaseException:
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
raise
|
||||
returncode = process.wait()
|
||||
elapsed = time.time() - t0
|
||||
|
||||
@@ -925,10 +1252,7 @@ def run_benchmark_once(
|
||||
if (
|
||||
isinstance(step_name, str)
|
||||
and step.get("duration_ms") is not None
|
||||
and (
|
||||
step_name.endswith("DenoisingStage")
|
||||
or step_name.endswith("RefinementStage")
|
||||
)
|
||||
and step_name.endswith(("DenoisingStage", "RefinementStage"))
|
||||
and "BeforeDenoisingStage" not in step_name
|
||||
):
|
||||
denoise_stage_total_ms += float(step["duration_ms"])
|
||||
@@ -957,12 +1281,63 @@ def run_benchmark_once(
|
||||
peak_memory_gb = candidate
|
||||
metrics["peak_memory_gb"] = peak_memory_gb
|
||||
|
||||
except Exception as e:
|
||||
except (AttributeError, OSError, TypeError, ValueError) as e:
|
||||
print(f" Warning: could not parse perf dump: {e}")
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def run_benchmark_once(
|
||||
model_key: str,
|
||||
label: str,
|
||||
output_dir: Path,
|
||||
warmup: bool = True,
|
||||
torch_compile: bool = False,
|
||||
model_cache_root: Path | None = None,
|
||||
cleanup_model_cache: bool = False,
|
||||
cleanup_ledger_path: Path | None = None,
|
||||
) -> dict:
|
||||
"""Run one preset and optionally clean its task-owned model cache."""
|
||||
cache_dir = None
|
||||
exit_reason = "error"
|
||||
if model_cache_root is not None:
|
||||
cache_dir = _prepare_model_cache(model_cache_root, model_key, label)
|
||||
|
||||
try:
|
||||
result = _run_benchmark_once_impl(
|
||||
model_key,
|
||||
label,
|
||||
output_dir,
|
||||
warmup=warmup,
|
||||
torch_compile=torch_compile,
|
||||
model_cache_dir=cache_dir,
|
||||
)
|
||||
exit_reason = "error" if result.get("error") else "success"
|
||||
return result
|
||||
except KeyboardInterrupt:
|
||||
exit_reason = "interrupted"
|
||||
raise
|
||||
finally:
|
||||
if cleanup_model_cache and cache_dir is not None:
|
||||
assert model_cache_root is not None
|
||||
ledger_path = cleanup_ledger_path or output_dir / "cleanup.jsonl"
|
||||
record = _cleanup_model_cache(
|
||||
model_cache_root,
|
||||
cache_dir,
|
||||
ledger_path,
|
||||
model_key,
|
||||
label,
|
||||
exit_reason,
|
||||
)
|
||||
before = record["before"]
|
||||
assert isinstance(before, dict)
|
||||
print(
|
||||
" Cleaned isolated model cache: "
|
||||
f"{before['total_bytes']} bytes, "
|
||||
f"{before['weight_file_count']} weight files; ledger={ledger_path}"
|
||||
)
|
||||
|
||||
|
||||
def print_results_table(results: list[dict]):
|
||||
"""Print a compact table for one or more benchmark runs."""
|
||||
print()
|
||||
@@ -1032,10 +1407,37 @@ def main():
|
||||
help="Directory for perf dump JSON files",
|
||||
)
|
||||
parser.add_argument("--no-warmup", action="store_true", help="Skip warmup")
|
||||
parser.add_argument(
|
||||
compile_group = parser.add_mutually_exclusive_group()
|
||||
compile_group.add_argument(
|
||||
"--torch-compile",
|
||||
action="store_true",
|
||||
help="Opt in to a torch.compile comparison. Presets run eager by default.",
|
||||
)
|
||||
compile_group.add_argument(
|
||||
"--no-torch-compile",
|
||||
action="store_true",
|
||||
help="Keep torch.compile disabled for eager-mode comparisons.",
|
||||
help="Deprecated compatibility flag; eager is already the default.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model-cache-root",
|
||||
type=str,
|
||||
help=(
|
||||
"Create a new isolated Hugging Face/ModelScope cache below this "
|
||||
"directory for each model run."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cleanup-model-cache",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Remove the task-owned model cache in a finally block and append "
|
||||
"a cleanup ledger record. Requires --model-cache-root."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cleanup-ledger",
|
||||
type=str,
|
||||
help="JSONL cleanup ledger path (default: <output-dir>/cleanup.jsonl).",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -1050,7 +1452,15 @@ def main():
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
warmup = not args.no_warmup
|
||||
torch_compile = not args.no_torch_compile
|
||||
torch_compile = args.torch_compile and not args.no_torch_compile
|
||||
if args.cleanup_model_cache and not args.model_cache_root:
|
||||
parser.error("--cleanup-model-cache requires --model-cache-root")
|
||||
model_cache_root = (
|
||||
Path(args.model_cache_root) if args.model_cache_root is not None else None
|
||||
)
|
||||
cleanup_ledger_path = (
|
||||
Path(args.cleanup_ledger) if args.cleanup_ledger is not None else None
|
||||
)
|
||||
|
||||
models_to_run = list(MODELS.keys()) if args.all else [args.model or "flux"]
|
||||
results = []
|
||||
@@ -1063,6 +1473,9 @@ def main():
|
||||
output_dir,
|
||||
warmup=warmup,
|
||||
torch_compile=torch_compile,
|
||||
model_cache_root=model_cache_root,
|
||||
cleanup_model_cache=args.cleanup_model_cache,
|
||||
cleanup_ledger_path=cleanup_ledger_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ Before running any `sglang generate` command below inside the diffusion containe
|
||||
- use `python/sglang/multimodal_gen/.claude/skills/sglang-diffusion-benchmark-profile/scripts/diffusion_skill_env.py` to derive the repo root, verify write access, and choose idle GPU(s)
|
||||
- export `HF_TOKEN` first when the selected model lives in a gated Hugging Face repo such as `black-forest-labs/FLUX.*`
|
||||
- export `FLASHINFER_DISABLE_VERSION_CHECK=1`
|
||||
- when a run downloads weights, use a task-owned cache and delete that model's
|
||||
cache after its eager/compile/BCG/profile group finishes; the benchmark skill
|
||||
provides `--model-cache-root --cleanup-model-cache` plus a cleanup ledger
|
||||
- `cd` to the repo root resolved from `sglang.__file__`
|
||||
|
||||
## Native Backend Gate
|
||||
@@ -33,8 +36,8 @@ These options are intended to preserve output quality. In practice, some paths (
|
||||
| Option | CLI Flag / Env Var | What It Does | Speedup | Limitations / Notes |
|
||||
|---|---|---|---|---|
|
||||
| **Performance Mode** | `--performance-mode auto\|speed\|memory\|manual` (`--mode` alias) | Applies model-aware residency, FSDP/CFG, and compile defaults without overriding explicit flags. `auto` is the safe default; `speed` favors GPU residency; `memory` favors offload; `manual` leaves performance args explicit. | Fastest way to establish a sensible deployment baseline | `speed` may OOM and enables `torch.compile` only when the model deployment config allows it. Explicit offload/FSDP/parallelism/compile flags win. Use `manual` for controlled A/B benchmarks. |
|
||||
| **torch.compile** | `--enable-torch-compile` | Applies `torch.compile` to the DiT forward pass, fusing ops and reducing kernel launch overhead. | ~1.2–1.5x on denoising | First request is slow (compilation). May cause minor precision drifts due to [PyTorch issue #145213](https://github.com/pytorch/pytorch/issues/145213). Pair with `--warmup-mode request` for best results. |
|
||||
| **Breakable CUDA Graph** | `--enable-breakable-cuda-graph --warmup-resolutions <WxH...>` plus optional `--bcg-text-buckets ...` | Captures fixed-resolution DiT segments while leaving attention/collectives eager, reducing launch overhead on supported pipelines. | Large on launch-bound paths; merged SANA and LTX-2 cases show material e2e gains | Mutually exclusive with `torch.compile` and Cache-DiT; BCG takes priority. Every served resolution must be declared for warmup capture. Current support is model-specific (Ideogram4, LTX-2, MiniMax-H3, Qwen-Image, SANA1.5, Z-Image, GLM-Image); benchmark before keeping it. |
|
||||
| **torch.compile** | `--enable-torch-compile` | Applies `torch.compile` to the DiT forward pass. Treat it as a measured comparator, not an assumed upgrade. | Model- and shape-dependent; recent B300 coverage found eager or valid BCG faster or within 1% for every valid compile control | First request is slow and some models time out or drift numerically. Keep eager as the ground truth, use a warmup watchdog, and validate the target model. See the [H200/B300 survey](https://github.com/BBuf/how-to-optim-algorithm-in-cuda/issues/21). |
|
||||
| **Breakable CUDA Graph** | `--enable-breakable-cuda-graph` plus optional `--warmup-resolutions <WxH...>` and `--bcg-text-buckets ...` | Captures fixed-resolution DiT segments while leaving attention/collectives eager, reducing launch overhead on supported pipelines. | Large on launch-bound paths; merged SANA and LTX-2 cases show material e2e gains | Mutually exclusive with `torch.compile` and Cache-DiT; BCG takes priority. The model's default resolution is captured automatically; declare every additional production resolution. Current support is model-specific (Ideogram4, LTX-2/2.3, MiniMax-H3, Qwen-Image, SANA1.5, Z-Image, GLM-Image); benchmark before keeping it. |
|
||||
| **Warmup** | `--warmup-mode request` | Runs dummy forward passes to warm up CUDA caches, JIT, and `torch.compile`. Eliminates cold-start penalty. | Removes first-request latency spike | Adds startup time. Without `--warmup-resolutions`, warmup happens on first request. |
|
||||
| **Warmup Resolutions** | `--warmup-resolutions 256x256 720x720` | Pre-compiles and warms up specific resolutions at server startup (instead of lazily on first request). | Faster first request per resolution | Each resolution adds to startup time. Serving mode only; useful when you know your target resolutions in advance. |
|
||||
| **Multi-GPU (SP)** | `--num-gpus N --ulysses-degree N` | Sequence parallelism across GPUs. Shards sequence tokens (not frames) to minimize padding. | Near-linear scaling with N GPUs | Requires NCCL; inter-GPU bandwidth matters. `ulysses_degree * ring_degree = sp_degree`. For Wan2.2 video, start by benchmarking pure Ulysses before assuming a mixed Ulysses/Ring layout is fastest. |
|
||||
@@ -364,9 +367,13 @@ Use these as first commands to benchmark, not as universal winners.
|
||||
| Wan2.2 TI2V 5B | 1280x720, 81 frames, 1 GPU | `--enable-torch-compile --warmup-mode request` | Keep the input image and motion prompt fixed when comparing sparse attention or Cache-DiT. |
|
||||
| Wan2.1 / FastWan / TurboWan variants | 480p or 720p video, family defaults | `--enable-torch-compile --warmup-mode request`; add `--ulysses-degree` / CFG parallel only after measuring | Current registry includes Wan2.1, FastWan2.1, FastWan2.2 TI2V, TurboWan2.1, TurboWan2.2 I2V, and Wan2.1-Fun InP. Use the compatibility matrix and benchmark presets before choosing topology. |
|
||||
| Cosmos3 Nano / Super | T2I: 1024x1024 with `--num-frames 1`; T2V/I2V: 480p/720p video | `SGLANG_DISABLE_COSMOS3_GUARDRAILS=1` for benchmark isolation; `--enable-torch-compile --warmup-mode request` | One checkpoint serves T2I/T2V/I2V. Mode is request-driven: `num_frames == 1` means T2I, `--image-path` means I2V. |
|
||||
| Cosmos3 Edge / distilled Super | Edge T2I: 640x640, 35 steps, 1 GPU; distilled Super T2I: 640x640, fixed 4-step schedule, 4 GPUs | Start eager with `--performance-mode manual`; use `SGLANG_DISABLE_COSMOS3_GUARDRAILS=1` only for benchmark isolation | Edge is trained for 256p/480p shapes. Distilled checkpoints own their sigma schedule and force guidance 1.0; do not override steps or flow shift. Do not retry the closed experimental Cosmos BCG path without a new lifecycle design. |
|
||||
| Ideogram 4 FP8/NVFP4 | 1024x1024, native preset defaults | `--enable-torch-compile --warmup-mode request` | Do not set `--num-inference-steps` or `--guidance-scale` directly unless you also update the Ideogram preset; sampling params derive them from `preset`. |
|
||||
| ERNIE-Image / GLM-Image / SANA / SD3 | 1024-class image, family defaults | `--enable-torch-compile --warmup-mode request`; disable offload only after checking VRAM | Treat these as current native image families. Start with benchmark/profile presets for ERNIE, GLM, and SANA; use registry/config defaults for SD3 unless you add a new preset. |
|
||||
| LongCat-Image | 1024x1024, 50 steps, guidance 4.5, 1 GPU | `--performance-mode manual --enable-prompt-rewrite false` for a DiT-only eager baseline | Prompt rewriting is enabled by the model defaults and runs a Qwen2.5-VL component. Disable it for kernel A/B, then keep a separate end-to-end recipe with rewriting enabled. |
|
||||
| SANA-Video | 832x480, 17 frames, 8 steps for CI-sized profiling; 81 frames, 50 steps for release quality | `--performance-mode manual` and eager first | Self QKV and cross KV are already packed. Check SANA's shared bit-exact conv/modulation fast paths and one-time contiguous layout before adding a new kernel. |
|
||||
| LTX-2 / LTX-2.3 | 768x512 or HQ 1920x1088, 121 frames | `--pipeline-class-name LTX2TwoStagePipeline --enable-torch-compile --warmup-mode request`; HQ uses `LTX2TwoStageHQPipeline` | Use benchmark/profile presets for nightly alignment, one-stage, high-resolution stress, and HQ. Device mode choices are `original` and `resident`; `resident` is fastest but uses more VRAM. `snapshot` is a deprecated alias for `original`, so do not use it in new commands. |
|
||||
| LTX-2.5 | One-stage distilled: 960x544, 121 frames, 8 steps; two-stage: 1920x1088 | `--pipeline-class-name LTX2Pipeline --performance-mode manual`; add `--use-diffusion-decoder` only for the decoder A/B | Benchmark the DiT and optional diffusion decoder as separate stages. Confirm NATTEN `na3d` is active before comparing decoder latency; a FlexAttention fallback is a different backend. Distilled weights run unguided. |
|
||||
| HunyuanVideo | 848x480 or 720p class video | `--text-encoder-cpu-offload --pin-cpu-memory --enable-torch-compile --warmup-mode request` | Check VAE decode separately. GroupNorm+SiLU is default-eligible in mainline when wrapper guards pass; use `bench_group_norm_silu.py` when VAE residual blocks are hot. |
|
||||
| JoyAI-Image-Edit | 1024-class TI2I, 40 steps, guidance 4.0 | `--backend=sglang --num-gpus 2 --enable-cfg-parallel --ulysses-degree 1 --enable-torch-compile --warmup-mode request --dit-layerwise-offload false --dit-cpu-offload false` | Newly supported image-edit path. Keep the input image, prompt, seed, and output size fixed; 2-GPU CFG parallel is the validated H100 starting point. |
|
||||
| FireRed-Image-Edit 1.0 / 1.1 | 1024x1024 image edit, 40 steps, guidance 4.0 | `--backend=sglang --num-gpus 2 --enable-cfg-parallel --ulysses-degree 1 --enable-torch-compile --warmup-mode request --dit-layerwise-offload false --dit-cpu-offload false` | Uses the native `QwenImageEditPlusPipeline` path. 2-GPU CFG parallel is the validated H100 starting point; benchmark 1.0 and 1.1 separately because checkpoint differences can change denoise latency. |
|
||||
@@ -381,12 +388,20 @@ state and the active source tree before relying on any path, flag, or claim
|
||||
about whether the work has merged:
|
||||
|
||||
- Fusion/kernel: #24025 LTX2 QK norm, #24059 Helios norm modulation, #24117 Z-Image packed QKV, #19488 Wan elementwise cross-block fusion, #19249 Z-Image gate/norm fusion, #20429 Qwen-Image layernorm/modulation, #20530 MOVA RMSNorm+RoPE.
|
||||
- Recent eager/BCG work: #34172 LTX2 quality-high fusion, #34174 automatic
|
||||
default-resolution BCG warmup, #34210 Z-Image BCG correctness, #34305/#34314
|
||||
Ideogram eager fusions, #34584 Wan TI2V modulation/RoPE, #34616 FLUX2,
|
||||
#34617 Hunyuan, #34619 GLM, #34620 ERNIE, #34928 SANA, #34929 LTX2.3,
|
||||
and #34932 Cosmos3. Re-check open/merged state before reusing a path.
|
||||
- VAE/decode: #22531 LTX2 parallel VAE, #20927 batched tiled VAE decode.
|
||||
- Runtime/parallel/cache: #22805 FLUX.2 packed QKV for A2A, #21742 hybrid attention schedule, #24053 USP replicated-prefix fix, #21613 TeaCache refactor, #24227 WanVideo TeaCache fix, #18764 dynamic batching, #24200 disaggregated diffusion.
|
||||
|
||||
## Tips
|
||||
|
||||
- **Benchmarking**: always use `--warmup-mode request` and look for the line ending with `(with warmup excluded)` for accurate timing.
|
||||
- **Benchmarking**: establish eager first (`--performance-mode manual`, compile/BCG/cache off), always use `--warmup-mode request`, and look for the line ending with `(with warmup excluded)` for accurate timing. Add compile or BCG as separate labeled controls.
|
||||
- **Checkpoint cleanup**: finish every variant for one model, then delete only
|
||||
its task-owned cache and verify the cleanup ledger reports zero residual
|
||||
weight files. Never point cleanup at a shared Hugging Face or ModelScope cache.
|
||||
- **Preset vs experiment control**: start with `--performance-mode auto` or
|
||||
`speed` for deployment, but use `--performance-mode manual` and pin the
|
||||
relevant residency/parallelism flags for controlled A/B claims.
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
def _load_benchmark_module(temp_root: Path):
|
||||
multimodal_gen_root = Path(__file__).resolve().parents[2]
|
||||
script_path = (
|
||||
multimodal_gen_root
|
||||
/ ".claude"
|
||||
/ "skills"
|
||||
/ "sglang-diffusion-benchmark-profile"
|
||||
/ "scripts"
|
||||
/ "bench_diffusion_denoise.py"
|
||||
)
|
||||
fake_env = types.ModuleType("diffusion_skill_env")
|
||||
fake_env.ensure_dir = lambda path: (
|
||||
Path(path).mkdir(parents=True, exist_ok=True) or Path(path)
|
||||
)
|
||||
fake_env.get_assets_dir = lambda _root: temp_root / "assets"
|
||||
fake_env.get_output_dir = lambda _kind, _root: temp_root / "outputs"
|
||||
fake_env.get_repo_root = lambda: temp_root / "repo"
|
||||
fake_env.pick_idle_gpus = lambda count: list(range(count))
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"test_bench_diffusion_denoise", script_path
|
||||
)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
with patch.dict(sys.modules, {"diffusion_skill_env": fake_env}):
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class TestDiffusionBenchmarkSkill(unittest.TestCase):
|
||||
def test_nightly_presets_remain_aligned(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
module = _load_benchmark_module(Path(tmpdir))
|
||||
repo_root = Path(__file__).resolve().parents[5]
|
||||
module.NIGHTLY_CONFIG_PATH = (
|
||||
repo_root
|
||||
/ "scripts"
|
||||
/ "ci"
|
||||
/ "utils"
|
||||
/ "diffusion"
|
||||
/ "comparison_configs.json"
|
||||
)
|
||||
|
||||
self.assertEqual(module.validate_nightly_alignment(), 0)
|
||||
|
||||
def test_recent_model_presets_are_eager_by_default(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
module = _load_benchmark_module(Path(tmpdir))
|
||||
|
||||
expected = {
|
||||
"longcat-image",
|
||||
"sana-video",
|
||||
"lingbot-video-moe",
|
||||
"cosmos3-edge-t2i",
|
||||
"cosmos3-super-t2i-distilled",
|
||||
"ltx25",
|
||||
"ltx25-diffusion-decoder",
|
||||
}
|
||||
self.assertTrue(expected.issubset(module.MODELS))
|
||||
|
||||
eager_cmd = module.build_sglang_cmd("longcat-image")
|
||||
self.assertNotIn("--enable-torch-compile", eager_cmd)
|
||||
self.assertIn("--enable-prompt-rewrite=false", eager_cmd)
|
||||
|
||||
compiled_cmd = module.build_sglang_cmd("longcat-image", torch_compile=True)
|
||||
self.assertIn("--enable-torch-compile", compiled_cmd)
|
||||
|
||||
h3_cmd = module.build_sglang_cmd("minimax-h3-t2va", torch_compile=True)
|
||||
self.assertNotIn("--enable-torch-compile", h3_cmd)
|
||||
|
||||
def test_isolated_cache_cleanup_writes_zero_residual_ledger(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
cache_root = temp_root / "model-caches"
|
||||
cache_dir = module._prepare_model_cache(
|
||||
cache_root, "longcat-image", "baseline"
|
||||
)
|
||||
|
||||
weight_path = cache_dir / "huggingface" / "hub" / "model.safetensors"
|
||||
weight_path.parent.mkdir(parents=True)
|
||||
weight_path.write_bytes(b"weights")
|
||||
env = module._model_cache_env(cache_dir)
|
||||
self.assertTrue(env["HF_HOME"].startswith(str(cache_dir)))
|
||||
self.assertTrue(env["HF_XET_CACHE"].startswith(str(cache_dir)))
|
||||
self.assertTrue(env["TRANSFORMERS_CACHE"].startswith(str(cache_dir)))
|
||||
self.assertTrue(env["MODELSCOPE_CACHE"].startswith(str(cache_dir)))
|
||||
|
||||
ledger_path = temp_root / "artifacts" / "cleanup.jsonl"
|
||||
record = module._cleanup_model_cache(
|
||||
cache_root,
|
||||
cache_dir,
|
||||
ledger_path,
|
||||
"longcat-image",
|
||||
"baseline",
|
||||
"success",
|
||||
)
|
||||
|
||||
self.assertFalse(cache_dir.exists())
|
||||
self.assertEqual(record["before"]["weight_file_count"], 1)
|
||||
self.assertEqual(record["after"]["file_count"], 0)
|
||||
ledger = json.loads(ledger_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(ledger["exit_reason"], "success")
|
||||
self.assertEqual(ledger["after"]["weight_file_count"], 0)
|
||||
|
||||
def test_isolated_cache_refuses_to_reuse_existing_run_directory(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
cache_root = temp_root / "model-caches"
|
||||
module._prepare_model_cache(cache_root, "sana-video", "baseline")
|
||||
|
||||
with self.assertRaises(FileExistsError):
|
||||
module._prepare_model_cache(cache_root, "sana-video", "baseline")
|
||||
|
||||
def test_interrupted_run_cleans_isolated_cache_in_finally(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
cache_root = temp_root / "model-caches"
|
||||
output_dir = temp_root / "outputs"
|
||||
output_dir.mkdir()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
module, "_run_benchmark_once_impl", side_effect=KeyboardInterrupt
|
||||
),
|
||||
self.assertRaises(KeyboardInterrupt),
|
||||
):
|
||||
module.run_benchmark_once(
|
||||
"sana-video",
|
||||
"baseline",
|
||||
output_dir,
|
||||
model_cache_root=cache_root,
|
||||
cleanup_model_cache=True,
|
||||
)
|
||||
|
||||
self.assertFalse((cache_root / "sana-video-baseline").exists())
|
||||
ledger = json.loads(
|
||||
(output_dir / "cleanup.jsonl").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(ledger["exit_reason"], "interrupted")
|
||||
self.assertEqual(ledger["after"]["weight_file_count"], 0)
|
||||
|
||||
def test_failed_run_is_recorded_as_error_and_cleaned(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
cache_root = temp_root / "model-caches"
|
||||
output_dir = temp_root / "outputs"
|
||||
output_dir.mkdir()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
module, "_run_benchmark_once_impl", side_effect=RuntimeError("boom")
|
||||
),
|
||||
self.assertRaisesRegex(RuntimeError, "boom"),
|
||||
):
|
||||
module.run_benchmark_once(
|
||||
"sana-video",
|
||||
"baseline",
|
||||
output_dir,
|
||||
model_cache_root=cache_root,
|
||||
cleanup_model_cache=True,
|
||||
)
|
||||
|
||||
self.assertFalse((cache_root / "sana-video-baseline").exists())
|
||||
ledger = json.loads(
|
||||
(output_dir / "cleanup.jsonl").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(ledger["exit_reason"], "error")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user