What the EasyInstaller is — and what it deliberately is not
OneTrainer is a remarkable piece of open-source work. It supports a stack of architectures broader than any single project I know of in this space — Stable Diffusion 1.5/2.x, SDXL, SD 3 / 3.5, Flux.1 / Flux.2 (both Klein 4B and 9B), PixArt, Sana, HiDream, Chroma, Hunyuan Video, Wuerstchen, Qwen-Image, Z-Image, Stable Cascade — and lets you train LoRAs, full fine-tunes, embeddings, or run Dreambooth-style pipelines, plus captioning, plus model conversion. It is gratuit, open-source under AGPL-3.0, and Nerogar maintains it actively. Every piece of credit for the training capabilities belongs to Nerogar and the OneTrainer contributors.
What the EasyInstaller does, very explicitly, is sit beside OneTrainer rather than around it:
- The EasyInstaller is original ExpSoft C# / WPF code. It does not embed OneTrainer source.
- At Setup time it
git clone’s OneTrainer from the upstream repository (Nerogar’s GitHub) into aproject/sub-folder. The clone is intact — the AGPL-3.0 source lives there, alongside its LICENSE file, ready to be inspected or replaced with a different commit. - At Run time, the EasyInstaller launches OneTrainer’s own Python GUI (
train_ui.py) as a sub-process. The user interacts directly with OneTrainer’s UI — the EasyInstaller is a launcher and a configurator, not a re-skin.
This is the right legal posture for an AGPL-3.0 dependency. The EasyInstaller is a separate work in a different language, distributed as an independent program; OneTrainer is installed at the user’s request from its canonical source. No derivative-work confusion.
Why Miniconda instead of an embeddable Python
OneTrainer’s GUI is built on customtkinter, which depends on Tkinter, which depends on Tcl/Tk. The official Python embeddable distribution from python.org does not ship Tkinter; you’d have to source Tcl/Tk separately, place the runtime where Python expects to find it, and pin the versions so the launchers don’t drift. It works, but it’s fragile across Windows updates.
Miniconda (Anaconda Inc, BSD-3-Clause) ships Tkinter natively as part of the Python 3.10 build. The installer for Miniconda3-py310 is about 110 MB and runs silently with a one-line command, dropping a complete Python 3.10 with Tk into runtime/miniconda/ alongside the EasyInstaller exe. A small .condarc tells conda to keep environments and packages under the install folder rather than under %USERPROFILE%\.conda, so the install stays self-contained.
The price is the extra 60 MB compared to an embeddable Python. The benefit is years of headaches you don’t have to debug.
The triton-windows story — patched out of requirements-cuda.txt
OneTrainer’s default requirements-cuda.txt includes triton-windows, an experimental Windows port of the Triton kernel compiler. On Linux, Triton is the right tool — it’s what makes torch.compile() fast on attention kernels. On Windows, the port has open issues — community reports point to “CUDA invalid memory access” crashes during long training runs on the Klein and Flux families, with the failure landing mid-training rather than at startup. Nobody has been able to reproduce them deterministically enough to fix at the source, and the maintainers have been transparent about it.
The EasyInstaller takes a deliberately conservative approach: at Setup Step 4, after cloning OneTrainer, a PatchRequirements() pass rewrites requirements-cuda.txt:
- Lines matching
triton-windowsortriton==are dropped. - The PyTorch wheel index URLs (
download.pytorch.org/whl/cuXXX) are rewritten to match the build variant:cu124for the RTX 20xx/30xx/40xx build,cu128for the RTX 50xx Blackwell build,cpufor the CPU-only build.
The seven preset profiles (more on those below) all have compile = false set explicitly, so even if Triton ever sneaks back in via a transitive dependency, OneTrainer won’t try to use it. The cost of running OneTrainer without Triton on Windows is a ~10-15% slowdown on the forward pass of large attention blocks. The benefit is that training runs of several hours don’t halt halfway through. For personal-use training on a single workstation, that trade is worth taking until upstream Triton on Windows stabilises.
Three build targets — one C# source, one Python source
The EasyInstaller ships in three flavours, each as a separate ZIP:
- Release-40xx — for RTX 20xx, 30xx, 40xx (Turing, Ampere, Ada). Pulls
torch+cu124. - Release-50xx — for RTX 50xx (Blackwell). Pulls
torch+cu128, which is the wheel set the Blackwell architecture currently needs. - Release-CPU — for development and testing without a GPU. Pulls
torch+cpu(much smaller wheel).
The split lives in the C# csproj as three separate configurations with DefineConstants set per build (GPU_50XX, CPU_ONLY, or default for 40xx). The TorchCudaIndex and TorchIndexUrl constants in SetupService.cs are #if-gated so the right wheel set is pinned at compile time. At runtime, the GPU detection step compares nvidia-smi’s reported card series against the build variant and surfaces a friendly warning if they don’t match — “you’re running the 40xx build on a 50xx card; please download the 50xx ZIP instead.” Better to catch the mismatch up front than to let it manifest as an obscure CUDA error after a 90-minute install.
Why three builds rather than one mega-installer that probes the GPU and downloads the right wheels? Two reasons. First, a single installer would have to either download all wheel sets (4-7 GB) or detect-then-fetch, which means failing in the middle of Setup on a slow connection. Second, the wheel sets are mutually exclusive — switching between them after install requires pip uninstall followed by pip install, which is a lot of disk thrash for what is conceptually a different build. Splitting at the ZIP level keeps each installer small and unambiguous.
Profile regeneration on every launch — the #.json convention preserved
OneTrainer’s convention for its training_presets/ folder is well-defined: files starting with # are Nerogar’s official defaults (#.json, #flux_dev_lora.json, and so on). User profiles can have any other name. The EasyInstaller adds seven preset profiles for Flux.2 Klein training, all prefixed with ExpSoft_ so they’re unambiguously distinct from both the Nerogar defaults and any custom profiles a user might create:
| Profile | Model | Resolution | Batch | VRAM | CPU offload |
|---|---|---|---|---|---|
ExpSoft_Klein_9B_LoRA_16GB | Klein 9B | 512 | 2 | 16 GB | none |
ExpSoft_Klein_9B_LoRA_8GB | Klein 9B | 512 | 1 | 8 GB | 0.7 |
ExpSoft_Klein_9B_LoRA_1024 | Klein 9B | 1024 | 1 | 16 GB | 0.5 |
ExpSoft_Klein_4B_LoRA_16GB | Klein 4B | 512 | 4 | 16 GB | none |
ExpSoft_Klein_4B_LoRA_8GB | Klein 4B | 512 | 2 | 8 GB | 0.5 |
ExpSoft_Klein_4B_LoRA_1024 | Klein 4B | 1024 | 2 | 16 GB | 0.3 |
ExpSoft_Klein_4B_LoRA_1536 | Klein 4B | 1536 | 1 | 16 GB | 0.6 |
Common settings across the seven: learning rate 3e-5 (a sweet spot for Klein LoRA), 100 epochs, LoRA rank 16, LoRA alpha 1.0, bf16 train and output, logit-normal timestep distribution (recommended for Flux.2), LoRA layer filter on the transformer blocks, INT-W8A8 quantisation on the transformer for VRAM savings, FP8 on the text encoders, FP32 on the VAE, and compile = false everywhere (the deliberate Triton-out posture from the previous section). CPU offload moves a fraction of the layers from GPU to system RAM during forward and backward, at the cost of a 20-40% slowdown — the right trade for Klein 9B on 8 GB, or Klein 9B at 1024 on 16 GB.
The non-obvious decision is that these seven profiles are regenerated from scratch on every launch of the EasyInstaller, rather than being saved once at Setup time. Three reasons:
- Drive-letter changes. If the install folder is on
F:today andG:tomorrow (USB-stick scenario, or a drive reorganisation), the absolute paths embedded in the profile JSON (workspace, output, samples, concepts, latent cache) need to follow the move. Regenerating beats string-replacing. - OneTrainer schema migrations. OneTrainer’s profile JSON has a
__versionfield. When Nerogar bumps the schema, the upstream#.jsonfile intraining_presets/gets the new version. Regenerating the ExpSoft profiles every launch — and reading__versiondynamically from#.json— means our profiles always match OneTrainer’s current expectations. No “schema version mismatch” errors at load time. - Strict scope.
ProfileServiceonly touches files matchingExpSoft_*.json.#-prefixed Nerogar defaults are read but never written. Any other user-created profiles are completely untouched. The user’s own work is never at risk.
Portable venv — the AutoPatchVenvPaths step
Once python -m venv project/venv/ is run, a number of files in the venv contain absolute paths to the Python interpreter that created it: pyvenv.cfg, Scripts/activate, Scripts/pip.exe (the launcher embeds a path), every .pth file in site-packages/, every .egg-link, every direct_url.json in .dist-info/ folders, and the conda meta JSONs that store each package’s prefix. If the install folder moves (different drive letter, different parent path), all of those break in subtly different ways.
PathService.AutoPatchVenvPaths() runs at the start of every launch. It reads pyvenv.cfg to discover the venv’s recorded root path, compares it to the current AppContext.BaseDirectory, and if they differ, walks the venv directory tree replacing the old root with the new one in every text file (an extension allow-list keeps it away from .exe / .dll / .pyd binaries). The patch is idempotent — running it on an already-correct venv is a no-op.
End result: drop the install folder on a USB stick, plug it into a different machine with a different drive letter, double-click the EasyInstaller exe, and OneTrainer launches. No reinstall.
The Samples tab — live previews from the training run
OneTrainer’s training loop periodically generates sample images at user-defined intervals, dropping them into workspace/samples/<prompt-folder>/ as PNGs. By default they’re invisible unless you go looking. The EasyInstaller adds a sixth tab — Samples — that watches that folder via a FileSystemWatcher with IncludeSubdirectories, and presents new samples in a WrapPanel gallery as they arrive, newest first.
A few small implementation details that pay back:
- The thumbnails use
BitmapImagedecoded from aFileStream(not from aUriSource), so the file handle is closed immediately after decode. The user can move, rename or delete the sample file without WPF holding a lock on it. DecodePixelWidth = 256on the decode side. On a long training run that produces 500 samples, that’s the difference between a few hundred MB of thumbnail RAM and a few GB.Task.Delay(500)between theFileSystemWatcherevent and the actual decode. The event fires before the file finishes writing; the brief delay lets the writer close it cleanly.- Click a thumbnail and a full-screen lightbox opens with a translucent overlay; click anywhere to close. Two lines of XAML, big quality-of-life delta.
RAM watchdog — protecting the host from Klein 9B
Klein 9B training, especially with CPU offload at 0.7, can consume 18 GB or more of system RAM (offloaded layers + optimiser state + gradients + dataset latent cache). On a 32 GB machine that leaves comfortable head-room; on a 16 GB machine it swaps and the entire system grinds to a halt.
The EasyInstaller runs a small background task during training that calls Win32 GlobalMemoryStatusEx every three seconds. If available physical RAM drops below 2 GB, the watchdog logs a warning and sends a graceful cancel signal to the Python sub-process. A user-friendly shutdown beats the alternative — Windows’ own OOM-killer taking out random processes, including potentially the IDE the user has unsaved work in.
Credits and license
The work that makes this useful belongs to upstream:
- OneTrainer — Nerogar, AGPL-3.0. The training engine, the GUI, the support for two-dozen architectures, the active maintenance. All of that is upstream work. The EasyInstaller is a launcher, not a fork.
- PyTorch — Meta + community, BSD-3-Clause.
- Miniconda — Anaconda Inc, BSD-3-Clause (the Python distribution); CPython itself is PSF License.
- Portable Git for Windows — GPL-2.0.
- customtkinter — MIT. OneTrainer’s GUI toolkit.
- transformers + diffusers + huggingface_hub — HuggingFace, Apache 2.0.
- Microsoft.Web.WebView2 — Microsoft, distribution permitted.
Diffusion model weights (Flux, SDXL, Klein, etc.) each have their own license — Klein 9B and 4B are gated on HuggingFace and require accepting the Black Forest Labs terms individually; SDXL is OpenRAIL++; PixArt is Apache 2.0; and so on. The EasyInstaller doesn’t bundle any model weights — the user downloads them with their own HF token through the Settings tab.
Take-aways
- For AGPL-3.0 upstream dependencies, an independent launcher that
git clone’s the upstream source at Setup time and runs it as a sub-process is the cleanest legal posture. The upstream stays itself; the wrapper stays separate. - Patching out a known-problematic Windows-specific dependency at requirements time is more honest than “try to make it work and hope.” Document the trade-off in the disclaimer and move on.
- Build matrices (cu124 / cu128 / cpu) at the C# project level let you ship pre-pinned PyTorch wheels per GPU generation. Run-time auto-detection sounds clever but means failing late.
- Regenerate config files at every launch when they encode absolute paths and upstream schema versions. Don’t persist what you can derive.