ML Training Environment Setup — RTX 5090 Laptop

Complete setup record: Windows 11 → WSL2 → verified CUDA training environment on a Lenovo Legion 7i Pro. Written as a rebuild guide — follow top to bottom on a fresh machine.

WSL2 CUDA / Blackwell PyTorch

Target configuration

ComponentValue
LaptopLenovo Legion 7i Pro
CPUIntel Core Ultra 9 (24 cores)
RAM64 GB
GPUNVIDIA GeForce RTX 5090 Laptop — 24 GB VRAM, Blackwell, sm_120
Storage2 × 2 TB NVMe SSD
Host OSWindows 11 (build 26200+)
GuestWSL2, Ubuntu 24.04 LTS
Python3.12.3 (Ubuntu system Python)
PyTorch2.13.0+cu130

The one fact that governs everything

The RTX 5090 is compute capability sm_120. Most prebuilt CUDA wheels are compiled for sm_90 and below. They will install without complaint, report torch.cuda.is_available() == True, and then fail on the first real kernel with cudaErrorNoKernelImageForDevice.

Every CUDA package you install must be verified to contain sm_120 kernels. That is what the verification script in Part 4 proves.

Part 1 — Windows 11 host

Everything in this section is done on Windows, not in WSL.

1.1 NVIDIA driver

Install the latest Game Ready or Studio driver from NVIDIA (or Lenovo Vantage). This is the only GPU driver in the entire stack. WSL2 receives it by passthrough.

Verify in PowerShell:

PowerShell
nvidia-smi

Expect a WDDM driver model and KMD Version / CUDA UMD Version fields — those identify Windows-native output.

CUDA UMD Version: 13.3 is a ceiling, not a requirement. A 13.3 driver runs cu126, cu128, cu129, cu130, and cu132 wheels. It is not the CUDA version you install.

1.2 Disable CUDA sysmem fallback ← do not skip

By default the Windows driver spills CUDA allocations into system RAM when VRAM is exhausted, instead of raising an error. For gaming this prevents crashes. For training it is a silent performance catastrophe: the run does not fail, it just starts shuttling tensors over PCIe every step and collapses to a fraction of its speed, with no error anywhere.

With 64 GB of RAM behind a 24 GB card, there is a lot of room to fall back into.

NVIDIA Control Panel → Manage 3D Settings → Global Settings → CUDA - Sysmem Fallback PolicyPrefer No Sysmem Fallback

You want a loud CUDA out of memory. That is actionable — you lower the batch size. Slow-but-running is not actionable, because you will not notice it.

1.3 Legion power and thermal configuration

Three independent settings, all of which must be right. Getting one wrong caps your throughput.

SettingWhereTarget
Windows power modeSettings → System → Power & batteryBest Performance
Legion thermal modeFn+Q, or Legion Space / Lenovo VantagePerformance
Graphics modeLegion Space → DisplayDiscrete / MUX for long runs
Power supplyphysicalBarrel charger, not USB-C PD

Notes:

  • Fn+Q is not the Windows power plan. They are separate systems. The power-button LED indicates the Legion mode (blue = Quiet, white = Balanced, red = Performance). Setting Windows to High Performance while the Legion sits in Balanced leaves most of your headroom on the table.
  • Hybrid vs Discrete graphics. Hybrid mode routes display output through the Intel iGPU and can hold the dGPU at reduced sustained power. Discrete/MUX mode gives the 5090 full headroom but costs battery life and requires a reboot. Worth switching for multi-hour training runs.
  • USB-C PD is not enough. An Ultra 9 plus a 5090 under sustained load exceeds what PD typically negotiates. Use the barrel charger.
  • Thermals. Multi-hour training is a different load profile from gaming bursts. Elevate the chassis, keep intakes clear. Throughput that degrades over the first ~20 minutes then plateaus is throttling, not a bug.

Verify the effect with nvidia-smi and read the Pwr:Usage/Cap field. The Legion 7i Pro chassis is rated well above 110W for this GPU — if the cap does not move after setting Performance mode, check graphics mode and charger next.

1.4 Install WSL2

PowerShell
wsl --install -d Ubuntu-24.04
wsl --update
wsl --shutdown

Confirm:

PowerShell
wsl --version

Reference known-good versions:

text
WSL version: 2.6.3.0
Kernel version: 6.6.87.2-1
WSLg version: 1.0.71
Windows version: 10.0.26200.9168

1.5 Configure WSL resources

Create C:\Users\<YourName>\.wslconfig:

INI
[wsl2]
processors=16
memory=48GB
swap=32GB
localhostForwarding=true
  • memory=48GB leaves 16 GB for Windows. Do not allocate all 64.
  • swap=32GB matters for GGUF conversion and weight merging, which spike RAM.
  • processors=16 of 24. The Ultra 9 is hybrid (P-cores + E-cores, no hyperthreading). Barely affects training — the GPU is the bottleneck — but helps tokenization and GGUF conversion, which are CPU-bound.

Apply with wsl --shutdown, then reopen.

Part 2 — Ubuntu guest

2.1 Verify GPU passthrough BEFORE anything else

bash
nvidia-smi
ls /usr/lib/wsl/lib/

nvidia-smi should print the GPU without a Driver-Model column (that column is Windows-only). The directory must contain at minimum:

text
libcuda.so.1   libnvidia-ml.so.1   nvidia-smi   libdxcore.so

If that directory is missing or empty, passthrough is broken. Fix it from Windows with wsl --update && wsl --shutdown. Do not attempt to fix it from inside Ubuntu.

2.2 Never install NVIDIA drivers inside WSL

Do not run any of these:

bash
sudo apt install nvidia-driver-*        # NO
sudo apt install nvidia-cuda-toolkit    # NO
sudo sh cuda_*_linux.run                # NO

The driver is projected in from Windows. Installing a Linux driver overwrites the passthrough stubs in /usr/lib/wsl/lib/ and breaks GPU access entirely. This is the single most common way people destroy a working WSL GPU setup.

Ubuntu 24.04's nvidia-cuda-toolkit package is CUDA 12.0 — it predates Blackwell and its nvcc rejects sm_120 outright with nvcc fatal: Unsupported gpu architecture 'compute_120'.

You do not need the CUDA toolkit at all. PyTorch pip wheels bundle their own CUDA runtime libraries.

2.3 System packages

bash
sudo apt update
sudo apt install -y python3-venv python3-dev build-essential git cmake ninja-build

python3-dev and build-essential are required because parts of the training stack (xformers, occasionally triton) may need to compile from source for Blackwell.

Ubuntu 24.04 is an externally-managed Python environment — global pip install fails with error: externally-managed-environment. Use venvs. Always.

2.4 Filesystem rules

All work lives in the WSL ext4 filesystem (~/ai/, ~/projects/). Never under /mnt/c/ or /mnt/d/.

The /mnt/ bridge is a 9p network filesystem. It is catastrophically slow for the many-small-file access patterns that pip installs and dataset tokenization generate. This one mistake can turn a 20-minute job into three hours.

Set the Hugging Face cache inside ext4. Add to ~/.bashrc:

bash
export HF_HOME=$HOME/.cache/huggingface

Check space with df -h /. Budget roughly:

ItemSize
PyTorch + bundled CUDA libs~8 GB
Unsloth + deps (triton, bitsandbytes, xformers)~3 GB
3B model, 4-bit~2 GB
8B model, 4-bit~6 GB
Tokenized corpus (e.g. TinyStories)~5 GB
llama.cpp build for GGUF export~2 GB
Training checkpoints1–10 GB

100 GB free is comfortable; 50 GB is the floor. Note the WSL virtual disk grows but never shrinks automatically — reclaiming space later requires a manual compact.

2.5 VS Code

Install the WSL extension (ms-vscode-remote.remote-wsl) on the Windows side. Then always open projects from inside WSL:

bash
cd ~/ai && code .

Windows-side extensions do not carry over — VS Code installs a separate set into the WSL host and will prompt you. Editing WSL files through a Windows-side window is slow and error-prone.

Part 3 — Python environments

3.1 Use two separate venvs

EnvContentsPurpose
~/ai/scratch_envtorch (latest) + numpy, tokenizers, datasets, tqdmFrom-scratch training, own code. No version pins.
~/ai/unsloth_envtorch (Unsloth-pinned) + full Unsloth stackFine-tuning.

Unsloth pins specific torch/triton/xformers combinations, and those pins lag PyTorch releases by weeks or months. Sharing one environment means every Unsloth update risks breaking your own code and vice versa. ~8 GB each — cheap insurance.

3.2 Install PyTorch — the --index-url is mandatory

bash
python3 -m venv ~/ai/scratch_env
source ~/ai/scratch_env/bin/activate
pip install --upgrade pip

pip install torch --index-url https://download.pytorch.org/whl/cu130

Omitting --index-url is the primary failure mode. Plain pip install torch pulls the default PyPI wheel, which does not contain sm_120 kernels. It installs cleanly and fails at runtime.

If pip errors on a missing cuda-bindings dependency (a known gap on that index), use --extra-index-url instead so pip can fall back to PyPI for stragglers.

3.3 Why cu130 and not cu128

Earlier Blackwell guides recommended cu128. That is now outdated on two counts:

  1. torch 2.13 does not ship cu128 wheels. Available builds are cu129, cu130, cu132.
  2. cu128 had genuine Blackwell problems. RTX 5090 users hit CUBLAS_STATUS_EXECUTION_FAILED on the first matmul with torch 2.10.0+cu128 due to missing sm_120 cuBLAS kernels. Moving to cu130 resolved it.

cu130 matches the CUDA 13.x generation the 610-series driver is built around and is what Unsloth's current docs use.

3.4 Supporting packages

bash
pip install numpy tokenizers datasets tqdm

Without numpy, torch emits Failed to initialize NumPy — harmless in itself, but the data pipeline needs it.

Part 4 — Verification

This is the last step of the setup, and the only one that actually proves the environment works. nvidia-smi and torch.cuda.is_available() both pass on a broken install, so the script below launches real kernels and validates their results.

The verification script

Save this as gpu_check.py inside the venv you want to test, then run it with that venv activated.

Python
import time
import torch

FAIL = []

def check(label, ok, detail=""):
    print(f"  [{'PASS' if ok else 'FAIL'}] {label}{' — ' + detail if detail else ''}")
    if not ok:
        FAIL.append(label)

print(f"\ntorch {torch.__version__}")
print(f"built against CUDA {torch.version.cuda}\n")

# --- 1. basic visibility -----------------------------------------------------
check("CUDA available", torch.cuda.is_available())
if not torch.cuda.is_available():
    raise SystemExit("\nStop here. PyTorch cannot see the GPU at all.")

name = torch.cuda.get_device_name(0)
cap = torch.cuda.get_device_capability(0)
total = torch.cuda.get_device_properties(0).total_memory / 1e9
print(f"  device: {name}")
check("compute capability is sm_120 (Blackwell)", cap == (12, 0), f"got sm_{cap[0]}{cap[1]}")
check("VRAM >= 20 GB", total >= 20, f"{total:.1f} GB")

# --- 2. does this torch build even TARGET your GPU? --------------------------
# This is the check that catches the classic Blackwell trap. A wheel compiled
# only up to sm_90 will still report cuda.is_available() == True.
arch_list = torch.cuda.get_arch_list()
print(f"  wheel compiled for: {', '.join(arch_list)}")
check("wheel includes sm_120", any("120" in a for a in arch_list))

# --- 3. real kernels, real results -------------------------------------------
print()
try:
    a = torch.randn(4096, 4096, device="cuda")
    b = torch.randn(4096, 4096, device="cuda")
    c = a @ b
    torch.cuda.synchronize()
    ref = (a.cpu() @ b.cpu())
    err = (c.cpu() - ref).abs().max().item()
    check("fp32 matmul executes and is numerically correct", err < 1e-2, f"max err {err:.2e}")
except Exception as e:
    check("fp32 matmul", False, str(e)[:120])

try:
    x = torch.randn(2048, 2048, device="cuda", dtype=torch.bfloat16)
    y = (x @ x)
    torch.cuda.synchronize()
    check("bf16 matmul executes", torch.isfinite(y).all().item())
except Exception as e:
    check("bf16 matmul", False, str(e)[:120])

try:
    q = torch.randn(2, 8, 512, 64, device="cuda", dtype=torch.bfloat16)
    o = torch.nn.functional.scaled_dot_product_attention(q, q, q, is_causal=True)
    torch.cuda.synchronize()
    check("flash attention kernel executes", torch.isfinite(o).all().item())
except Exception as e:
    check("flash attention", False, str(e)[:120])

# --- 4. throughput — the sysmem-fallback detector ----------------------------
# If the driver is quietly spilling to system RAM, this number collapses.
print()
n = 8192
a = torch.randn(n, n, device="cuda", dtype=torch.bfloat16)
b = torch.randn(n, n, device="cuda", dtype=torch.bfloat16)
for _ in range(3):
    a @ b
torch.cuda.synchronize()

t0 = time.time()
iters = 30
for _ in range(iters):
    a @ b
torch.cuda.synchronize()
dt = time.time() - t0

tflops = (2 * n ** 3 * iters) / dt / 1e12
print(f"  bf16 matmul throughput: {tflops:.1f} TFLOP/s")
if tflops < 30:
    print("  !! Very low. Suspect sysmem fallback, a power cap, or thermal throttling.")
elif tflops < 90:
    print("  ~  Workable, but below what this card can do. Check the power cap.")
else:
    print("  OK — the card is running properly.")

# --- 5. can you actually fill VRAM? ------------------------------------------
print()
try:
    hog = torch.empty(int(16e9 // 2), dtype=torch.float16, device="cuda")  # ~16 GB
    torch.cuda.synchronize()
    del hog
    torch.cuda.empty_cache()
    check("can allocate 16 GB on device", True)
except RuntimeError as e:
    check("can allocate 16 GB on device", False, str(e)[:120])

print("\n" + "=" * 60)
if FAIL:
    print("NOT READY. Failed: " + ", ".join(FAIL))
else:
    print("READY. Install the training stack.")
print("=" * 60 + "\n")
bash
source ~/ai/scratch_env/bin/activate
python3 gpu_check.py

Known-good baseline (recorded 2026-08-25)

text
torch 2.13.0+cu130
built against CUDA 13.0

  [PASS] CUDA available
  device: NVIDIA GeForce RTX 5090 Laptop GPU
  [PASS] compute capability is sm_120 (Blackwell) — got sm_120
  [PASS] VRAM >= 20 GB — 25.7 GB
  wheel compiled for: sm_75, sm_80, sm_86, sm_90, sm_100, sm_120
  [PASS] wheel includes sm_120

  [PASS] fp32 matmul executes and is numerically correct — max err 4.43e-04
  [PASS] bf16 matmul executes
  [PASS] flash attention kernel executes

  bf16 matmul throughput: 103.9 TFLOP/s
  OK — the card is running properly.

  [PASS] can allocate 16 GB on device

The line that matters most is wheel compiled for:. If sm_120 is absent, stop — nothing downstream will work correctly.

Reading the throughput number

~103.9 TFLOP/s is a healthy result for a laptop 5090 at a 110W cap. The mobile GB203 has fewer SMs and lower clocks than the desktop part; roughly half its peak is expected. Do not chase desktop numbers.

Its value is diagnostic, not aspirational. Re-run the script whenever the machine feels slow:

ObservationLikely cause
~103.9 TFLOP/sNormal. Baseline.
Rises to ~100Performance mode / discrete graphics took effect.
Drops well below 70Thermal throttling, or the power profile reset.
Drops below 30Sysmem fallback re-enabled, or a driver update reset it.

Real training throughput lands around 30–40 TFLOP/s once attention, optimizer steps, and data loading are included.