tsiendragon/qwen-image-finetune/qwen-image-edit-aipc-finetune-05-adaptation

Claude Code and agent skill by tsiendragon/qwen-image-finetune

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Step 5 of 8 of the Qwen-Image-Edit AI PC fine-tuning walkthrough. Use after qwen-image-edit-aipc-finetune-04-config. Apply all required XPU/NF4 source patches to the qwen-image-finetune clone: device_utils.py, residual CUDA/flash-attention fixes, the accelerate XPU config, Pydantic schema patches, the NF4 QLoRA autocast/dtype patches, mode-aware loading, and (only for QwenImageEditPlus models) the Plus trainer load_model. Ends with an import test and an adaptation checker. Only use once step 4 config lint passes.

Install

npx vskill@latest install tsiendragon/qwen-image-finetune/qwen-image-edit-aipc-finetune-05-adaptation

SKILL.md

Step 5 — Framework Adaptation (XPU + NF4 Patches)

Series position: Step 5 of 8.

Scope: this step adapts a clone of tsiendragon/qwen-image-finetune for XPU. The
framework is a custom training loop (not HF Trainer), so it hardcodes torch.cuda.* and
CUDA-only paths. If your clone is already adapted (has device_utils.py and the patches
below), some or all steps may already be done — run the §5.8 import test + checker to confirm
before re-applying anything.

Prerequisites:

  • config.yaml with trainer: set (Step 4) — determines whether the Plus section (§5.7) applies
  • The qwen-image-finetune project directory (Step 1)

Next step: when adaptation_check.py exits 0, proceed to skill qwen-image-edit-aipc-finetune-06-quantize.


Which trainer files to adapt

§5.1–§5.6 are framework-wide and always apply, including to the base trainer (`qwen_image_edit_trainer.py`) — even when your model uses the Plus trainer, because QwenImageEditPlusTrainer inherits the base trainer's training loop (that's where the NF4-A2 autocast lives) and its device-wrapped helpers. So: always do §5.1–§5.6.

Then, only if `detect_trainer.py` selected `QwenImageEditPlus` (Step 4), also do §5.7 — adapting the Plus trainer's own load_model. (The Plus trainer overrides only load_model and the multi-image methods; everything else it inherits from the base trainer you already adapted.)

5.1 Device-agnostic helpers (device_utils.py)

Replace each torch.cuda.* call in the framework with a device-agnostic wrapper that checks XPU first. This confines the XPU adaptation to the framework's own source files and leaves third-party libraries (Triton, bitsandbytes, accelerate) untouched.

In qwen-image-finetune the wrappers live at src/qflux/utils/device_utils.py. Create this file (before any other change if adapting a fresh clone) with exactly this content:

python
# src/qflux/utils/device_utils.py
# enable XPU: device-agnostic helpers — call these instead of torch.cuda.* directly.
import contextlib
import torch
def device_empty_cache() -> None:
if torch.xpu.is_available(): # enable XPU
torch.xpu.empty_cache()
elif torch.cuda.is_available():
torch.cuda.empty_cache()
def device_synchronize() -> None:
if torch.xpu.is_available(): # enable XPU
torch.xpu.synchronize()
elif torch.cuda.is_available():
torch.cuda.synchronize()
def device_manual_seed_all(seed: int) -> None:
if torch.xpu.is_available(): # enable XPU
torch.xpu.manual_seed_all(seed)
else:
torch.cuda.manual_seed_all(seed)
def device_context(device):
"""Device context manager replacing torch.cuda.device().
torch.xpu.device("cpu") raises; this helper returns nullcontext() for
CPU and the correct accelerator context otherwise.
"""
device_str = str(device) if device is not None else "cpu"
if device_str == "cpu":
return contextlib.nullcontext()
if torch.xpu.is_available(): # enable XPU
return torch.xpu.device(device)
return torch.cuda.device(device)

Replace each direct torch.cuda.* call in the framework:

OriginalReplacementImport
torch.cuda.empty_cache()device_empty_cache()from qflux.utils.device_utils import device_empty_cache
torch.cuda.synchronize()device_synchronize()from qflux.utils.device_utils import device_synchronize
torch.cuda.manual_seed_all(s)device_manual_seed_all(s)from qflux.utils.device_utils import device_manual_seed_all
with torch.cuda.device(dev):with device_context(dev):from qflux.utils.device_utils import device_context

Import the helpers from the submodule directly (as in the table); there is no need to re-export them from qflux/utils/__init__.py.

5.2 Find and fix residual patterns

The helpers cover functional calls. A separate scan is needed for string-based device comparisons and CUDA-only library imports, which helpers cannot address:

bash
grep -rn "torch\.cuda\.\|flash_attention_2\|transformer_engine\|device\.type.*cuda\|Qwen/Qwen-Image-Edit" src/qflux/

The scan covers both trainer files (qwen_image_edit_trainer.py and qwen_image_edit_plus_trainer.py) — fix every hit in whichever one(s) your model uses.

PatternFix
flash_attention_2 (in from_pretrained calls or config.json)Replace with "sdpa". Flash Attention 2 is CUDA-only
transformer_engine import / useComment out — CUDA-only library
device.type == "cuda" literal compareAdd an xpu branch (string compare can't be patched)
Hardcoded model id in load_model — load_vae("Qwen/Qwen-Image-Edit", …), load_qwenvl("Qwen/Qwen-Image-Edit", …) (base) or "Qwen/Qwen-Image-Edit-2509" (Plus)Replace with self.config.model.pretrained_model_name_or_path. The hardcoded id makes the trainer fetch that repo from the network instead of using the model you already downloaded locally (Step 1) — wasting time, and loading the wrong model's VAE/text_encoder if you trained on a different checkpoint. Point it at your local model dir.
mp.set_start_method("spawn", force=True)Guard with if not torch.xpu.is_available(): .... Windows defaults to spawn already; force=True raises RuntimeError: context has already been set
torch.cuda.device(device) context managerReplace with device_context(device) from device_utils.py. torch.xpu.device("cpu") raises when device="cpu" (e.g. text_encoder placed on CPU for inference); device_context() returns nullcontext() for CPU
train_epoch() ignores max_train_steps — runs num_epochs × dataset_size steps insteadTwo edits, both needed. (1) Guard the per-batch loop in train_epoch(): if self.global_step >= self.config.train.max_train_steps: return. (2) Break the epoch loop in fit() on the same condition: if self.training_interrupted or self.global_step >= self.config.train.max_train_steps: break. See the note below — edit (1) alone stops training but not the epoch loop.

Example device.type fix in a config validator:

python
if d.type == "cuda":
if not torch.cuda.is_available():
raise ValueError(f"CUDA not available but got device={d}.")
# enable XPU: validate XPU device availability
if d.type == "xpu" and not torch.xpu.is_available():
raise ValueError(f"XPU not available but got device={d}.")
Why the `max_train_steps` fix needs both edits. train_epoch()'s guard saves a final
checkpoint and returns — but it only returns from train_epoch(). fit()'s
for epoch in range(...) loop keeps going, so every remaining epoch re-enters train_epoch(),
trips the guard again, and writes another full resumable checkpoint before returning. With
num_epochs set well above the epoch count max_train_steps actually reaches (the Step 4
recommender's default pairing), that is one redundant checkpoint directory per remaining epoch,
each carrying a complete copy of the optimizer state — enough to fill a disk on a long run. The
symptom is a pile of checkpoint-last-<epoch>-<step>-last directories that all share the same
step number and differ only by epoch. Nothing prunes them: train.checkpoints_total_limit is
declared in the schema but never wired into ProjectConfiguration, so it has no effect.
The copies are byte-identical, so a run that skipped edit (2) still trained correctly — the cost
is disk and save time, not the model. adaptation_check.py reports the missing break as a WARN
for that reason, not an ERROR.
Breaking the epoch loop as well makes the guard write exactly one final checkpoint. fit()'s own
closing save_checkpoint(...) reuses the same epoch and step, hence the same directory name, so it
overwrites rather than adding another.

5.3 accelerate single-card XPU config

qflux.main launches via accelerate launch --config_file <path>. Create an XPU-specific config accelerate_config_xpu.yaml in the project root:

yaml
compute_environment: LOCAL_MACHINE
debug: false
distributed_type: NO
mixed_precision: 'no'
num_machines: 1
num_processes: 1
main_training_function: main
dynamo_backend: 'no'
use_cpu: false
deepspeed_config: {}

Key differences from a typical CUDA multi-GPU config:

FieldCUDA multi-GPUXPU single-cardWhy
distributed_typeMULTI_GPUNOSingle process; no collective ops
mixed_precisionbf16'no'Set 'no' so this file matches what actually runs: base_trainer passes a hardcoded mixed_precision="no" to Accelerator, and an explicit argument overrides whatever the launch config says. Mixed precision is applied by the trainer's own torch.autocast (§5.5.3 NF4-A2)
dynamo_backendinductor'no'This recipe does not use torch.compile; leave the wrapper out of the path
YAML gotcha: dynamo_backend: no unquoted parses as YAML boolean False; accelerate then
calls .upper() on it and raises AttributeError: 'bool' object has no attribute 'upper'.
Always quote 'no' (and same for mixed_precision: 'no').

5.4 Pydantic schema patches

Two edits to src/qflux/data/config.py so the NF4 config the recommender emits validates against the upstream schema.

a) Add the `quantize_type` field to `ModelConfig`. Upstream ModelConfig has quantize: bool but no `quantize_type` field, and it is declared with ConfigDict(extra="forbid") — so any config carrying model.quantize_type: nf4 fails immediately with pydantic ... Extra inputs are not permitted. Add the field:

python
class ModelConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
...
quantize: bool = False
quantize_type: str | None = None # enable XPU: "nf4" selects NF4 QLoRA loading

The NF4 loader (§5.5 / Step 6) reads quantize_type == "nf4" to pass use_nf4=True; without this field the whole NF4 recipe is unreachable.

b) Relax `num_workers` to `>= 0`. Upstream declares num_workers with a > 0 constraint. On Windows under spawn start-method, classes registered via trust_remote_code are not picklable across processes — workers crash on dataset iteration, and num_workers = 0 (the fix) is rejected by the schema. Relax the constraint to >= 0 in the dataset config validator. The recommender defaults to 1, but 0 should be acceptable as an escape hatch. (This is one of two Windows spawn-related fixes — the other is the mp.set_start_method guard in §5.2; they are independent.)

Cache key default (`prompt_empty_drop_keys`). Upstream CacheConfig defaults this to
["prompt_embed", ...] — note the missing s. The cached key is prompt_embeds, so
caption-dropout raises KeyError: 'prompt_embed'. The recommender sidesteps this by **emitting
cache.prompt_empty_drop_keys: ["prompt_embeds", "prompt_embeds_mask"]** in the config, so no
source edit is required as long as you use the generated config. (If you hand-write a config,
set this key yourself.)

5.5 BitsAndBytes XPU backend + NF4 QLoRA required patches

5.5.1 BitsAndBytes XPU backend (informational)

Knowing which bnb operations dispatch to which backend on XPU helps diagnose failures and explains why Step 1 requires Visual Studio + oneAPI. For bitsandbytes >= 0.48.2:

OperationBackendWhen it runs
dequantize_4bitSYCLEvery NF4 forward and backward pass
dequantize_blockwiseSYCLUsed alongside NF4 dequant
gemv_4bitSYCLInference-only single-token path
quantize_4bit, quantize_blockwiseTritonInitial quantization (e.g. Step 6 pre-quantize)
optimizer_update_8bit_blockwise, optimizer_update_32bitTritonEvery bnb.optim.Adam8bit.step() call

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
253
Repository forks
27
7-day trend score
28

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/tsiendragon/qwen-image-finetune/qwen-image-edit-aipc-finetune-05-adaptation/badge)](https://verified-skill.com/skills/tsiendragon/qwen-image-finetune/qwen-image-edit-aipc-finetune-05-adaptation)

Something wrong with this skill or its page? Tell us.