← ExpSoft DiffusionForce

UGPUM™ and pipeline parallelism — making personal multi-GPU diffusion training work on Windows

ExpSoft DiffusionForce trains LoRAs on diffusion models whose weight matrix is too large for a single consumer GPU (Wan 2.1 14B and friends), on personal Windows rigs, within the legal frame of running open-weights models you’re entitled to use (see the Legal & Compliance section of the product page). The technical core is two-fold: pipeline parallelism with a VRAM- and compute-aware splitter — so a heterogeneous rig like an RTX 5080 + RTX 4060 Ti actually behaves as one 32 GB usable pool — and UGPUM™, a multi-mode memory scheduler that picks training-only vs training + permanent CPU swap vs training + temporary CPU swap (for HD sample generation), depending on what fits. Native Windows, no NCCL, no DDP, no WSL2.

· 11 min read · By Nicolas Riquier

Personal LoRA training, large diffusion models, and the consumer-GPU gap

A LoRA — Low-Rank Adaptation — lets you teach a large diffusion model a new style, a new subject or a new concept by training only a small adapter on top of its frozen weights. For text-to-image and text-to-video models, LoRAs are how individual creators bring their own data into a model they didn’t train. The legal frame is straightforward: you’re running an open-weights model you’re entitled to use, on a dataset you’re entitled to use, for your personal creative work.

The technical frame got harder in 2025–2026. The diffusion transformer (DiT) inside modern open-weights video and image models keeps growing. Wan 2.1 14B sits at 28 GB in bf16. Flux 2 Dev is even larger. HunyuanVideo is in the same league. A single consumer GPU — even a top-tier RTX 5080 at 16 GB — cannot hold the weights of one of these DiTs in VRAM, let alone the gradients, the optimizer state and the activations a training pass needs.

The standard escape hatches all have a catch:

DiffusionForce exists for the trough: a personal LoRA trainer that pools two consumer GPUs on native Windows, with heterogeneous VRAM and compute handled by the splitter, with the open-source kohya-ss / musubi-tuner training engine doing the actual data, scheduler and optimizer work underneath.

Why pipeline parallelism, not block-swap

The choice that drives everything else is where the DiT weights live during training.

Block-swap puts them all on CPU RAM and moves a block at a time into VRAM when the forward pass needs it. The work is sequential. The PCIe bus is the bottleneck. A second GPU adds nothing.

Pipeline parallelism puts blocks permanently on their assigned GPU. Block 0 lives on cuda:0 forever; block 22 lives on cuda:1 forever. The forward pass walks the chain: cuda:0 computes blocks 0–N, hands an activation tensor (~4 MB) to cuda:1 across PCIe, cuda:1 computes blocks N+1 to the end. The backward pass goes the other way. Both GPUs do real compute on real weights; only activations cross the bus.

For Wan 2.1 14B on a 5080 + 4060 Ti rig (16 + 16 = 32 GB usable), the math works: 28 GB of weights split across two cards, with the remaining 4 GB shared between activations, gradients, optimizer state and LoRA adapters. Tight, but feasible.

The catch is that the entire diffusion training ecosystem assumes either single-GPU work or DDP, not a third option. To make pipeline parallelism work on top of existing Musubi code without forking it, we needed to intercept a number of assumptions baked into PyTorch and Hugging Face Accelerate (which Musubi uses under the hood). More on that below.

The splitter — VRAM and compute, not just VRAM

The naive split assigns blocks proportionally to VRAM: 16 GB cards get 50/50, an 8+16 GB rig gets 33/67. That’s wrong for heterogeneous rigs.

Compute speed matters at least as much as VRAM. On a 5080 + 4060 Ti, the 5080 has roughly twice the TFLOPS of the 4060. If you split 50/50 by block count, the 5080 finishes its half of the forward pass and then waits for the 4060 to finish, every single step. The whole rig moves at the speed of the slower card.

The DiffusionForce splitter reads torch.cuda.get_device_properties() at startup, looks at the per-card TFLOPS budget and the per-card memory bandwidth, and computes a ratio that balances finish times. On the 5080 + 4060 Ti pair, that lands around 65% of blocks on the 5080 and 35% on the 4060 Ti for Wan 14B — fewer blocks per card but those blocks compute faster. The slow card stops being the floor; both cards finish their leg of the pass at roughly the same time.

A small VRAM reserve (≈1 GB per device, scaling adaptively with total VRAM in upcoming releases) protects against allocation spikes during activations and gradient computation. The splitter refuses to launch and reports a clear actionable message — “need ~X GB more, free Y GB from n” — if no valid split fits.

The hook layer — making Musubi behave without forking it

This is the part of the project that took most of the calendar time, and the part that’s hardest to describe without going into ML-Twitter weeds. The short version: PyTorch supports cross-device autograd natively, but the ecosystem above it (Hugging Face Accelerate, model loading conventions, the LoRA injection scheme used by kohya-ss/sd-scripts and Musubi) makes a number of assumptions that are correct for single-GPU or DDP but incorrect for single-process pipeline parallelism.

DiffusionForce overrides those assumptions through a layer of hooks — small, surgical Python patches that sit between Musubi and the underlying frameworks. Each hook addresses one specific assumption that, left as-is, makes the partition fall apart. The first ten hooks were the path to the first successful Wan 14B training step; the more recent ones (notably hook #12, the UGPUM™ scheduler itself, and hook #22, the per-prompt extra-LoRA injector for sample generation) handle features that emerged once the foundation worked.

A flavour of the kind of thing each hook handles, without going through all of them:

The list isn’t glamorous. It’s the kind of engineering that exists because each upstream project made a reasonable assumption that happened to not hold in our exact intersection. Each hook is small, named, isolated to one file, and documented with the exact failure mode it prevents. Musubi itself isn’t forked: it ships in the installer as a pinned vendored snapshot, untouched, alongside the hook layer that intercepts its behaviour at known integration points.

UGPUM™ — three modes for three different constraints

Pipeline parallelism alone is enough for a rig where the DiT, the gradients and the optimizer state all fit across the two cards combined. Wan 14B on 16 + 16 GB is the canonical case for that.

Two situations break the assumption:

  1. Even larger models where the weights alone don’t fit across two consumer cards (Flux 2 Dev with its full text encoder is the open case). Pipeline parallelism is no longer sufficient on its own.
  2. HD video sample generation during training. Generating a 1024×1024×9-frame MP4 sample at the end of an epoch needs more activation memory on cuda:0 than the training loop normally has free, even though training itself is comfortably profitable.

UGPUM™ — Unified GPU Memory — is the multi-mode memory scheduler that handles all three cases under one roof. At launch, it estimates static and dynamic VRAM budgets per GPU and picks the minimal mode that fits the job:

Mode C is the one that took the most engineering — five successive bug fixes before it survived an end-to-end run. The reasons range from picking the right primary device for the sample (cuda:0 is where the sample latent input arrives, so blocks need to be there) to noticing that a one-shot re-sync hook from earlier in the pipeline was firing in the middle of the swap and restoring the partition at exactly the wrong moment.

The result, the night Mode C first worked: a 1024×1024 × 9-frame video sample produced during a Wan 2.1 14B + LoRA training pass on a 5080 + 4060 Ti rig, on native Windows. To our knowledge that exact combination — that resolution, that frame count, that DiT size, that consumer GPU pair, on Windows without WSL2 — hadn’t worked before.

What we deliberately don’t support in V1

A multi-GPU diffusion trainer has an unbounded surface. Some scope-cuts that landed for the V1 release:

Credits and license

DiffusionForce vendors a pinned snapshot of kohya-ss / musubi-tuner (Apache 2.0). All the heavy lifting — the data loaders, the schedulers, the optimizer wrappers, the safetensors save/load, the model implementations themselves — comes from there. The kohya-ss project and its contributors did the diffusion-training reverse engineering that everything in this ecosystem is built on. The original Musubi LICENSE travels intact in the installer’s Resources/musubi-tuner-vendored/ directory.

The cross-device autograd is straight PyTorch. The single-process distributed init that lets Hugging Face Accelerate behave is the same library; Accelerate itself is from the Hugging Face team and the work it does on top of PyTorch is what makes the bf16 / FP8 / mixed-precision plumbing tractable in the first place.

What DiffusionForce adds, by line count: the pipeline-engine subpackage (splitter, activation router, DiT wrapper, the hook layer including UGPUM™ — roughly 2 000 lines of Python), the C# / WPF Windows installer and Run-tab UI, the uv-based Python environment management, and the integration of all the above so a non-CLI user gets a one-click Setup + a one-click Run.

Take-aways

Frequently Asked Questions

Quick answers to what people ask AIs about this article specifically.

Why does DiffusionForce use pipeline parallelism instead of DDP?

DDP replicates the model on each GPU and shards the data — useful for throughput when the model fits on a single card, useless when the DiT itself doesn’t fit on one card (which is the case for Wan 2.1 14B, Flux 2 Dev and other large open-weights diffusion models). Pipeline parallelism keeps each block permanently on its assigned device and walks the chain through both GPUs; the second GPU does real compute on real weights, not just storage. PCIe carries only the activation tensors at the block boundaries (≈4 MB each), not the weights themselves.

What is UGPUM™ exactly?

UGPUM™ — Unified GPU Memory — is a multi-mode memory scheduler. At launch it estimates per-device static and dynamic VRAM budgets and picks the minimal mode that fits: Mode A (pure pipeline, fastest), Mode B (pipeline + permanent CPU swap for huge models, V1.1+), or Mode C (pipeline + temporary CPU swap during HD sample generation only). The scheduler also surfaces the chosen mode and per-GPU forecasts in the Run tab UI, with a pre-flight refuse path if no mode fits.

Does DiffusionForce work on a single GPU?

Not in the multi-GPU pipeline-parallelism sense — that’s the whole point of the project, and exactly two visible NVIDIA RTX devices are required to activate it. When the launcher detects only one GPU, it hands the training run off to the unmodified vendored Musubi path, which works exactly as it does on its own. Single-GPU support as a first-class DiffusionForce feature is on the V2 roadmap.

Why don’t the two GPUs need to be the same model?

Because the splitter is both VRAM- and compute-aware. It reads per-device properties at startup (TFLOPS, memory bandwidth, available VRAM) and balances block assignment so the two cards finish their leg of the forward pass at roughly the same time. On an RTX 5080 + RTX 4060 Ti pair training Wan 2.1 14B, that lands around 65% of blocks on the 5080 and 35% on the 4060 Ti — fewer blocks per card but those blocks run faster. Symmetric pairs (two identical cards) get a 50/50 split.

What does DiffusionForce add on top of Musubi (the vendored engine)?

Musubi (by kohya-ss and contributors, Apache 2.0) provides the diffusion training core — data loaders, schedulers, optimizer wrappers, safetensors save/load, model implementations. DiffusionForce vendors it untouched and adds: the pipeline-engine Python subpackage (splitter, activation router, DiT wrapper, ~2 000 lines), the C# / WPF Windows installer and Run-tab UI, the uv-based environment management, and the integration glue. The hook layer intercepts a number of single-GPU / DDP assumptions in Accelerate, PyTorch and Musubi’s loading path so pipeline parallelism works without forking the engine.

Want to use ExpSoft DiffusionForce?

Coming soon on Patreon →