Target configuration
| Component | Value |
|---|---|
| Laptop | Lenovo Legion 7i Pro |
| CPU | Intel Core Ultra 9 (24 cores) |
| RAM | 64 GB |
| GPU | NVIDIA GeForce RTX 5090 Laptop — 24 GB VRAM, Blackwell, sm_120 |
| Storage | 2 × 2 TB NVMe SSD |
| Host OS | Windows 11 (build 26200+) |
| Guest | WSL2, Ubuntu 24.04 LTS |
| Python | 3.12.3 (Ubuntu system Python) |
| PyTorch | 2.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:
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 Policy → Prefer 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.
| Setting | Where | Target |
|---|---|---|
| Windows power mode | Settings → System → Power & battery | Best Performance |
| Legion thermal mode | Fn+Q, or Legion Space / Lenovo Vantage | Performance |
| Graphics mode | Legion Space → Display | Discrete / MUX for long runs |
| Power supply | physical | Barrel 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
wsl --install -d Ubuntu-24.04
wsl --update
wsl --shutdown
Confirm:
wsl --version
Reference known-good versions:
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:
[wsl2]
processors=16
memory=48GB
swap=32GB
localhostForwarding=true
memory=48GBleaves 16 GB for Windows. Do not allocate all 64.swap=32GBmatters for GGUF conversion and weight merging, which spike RAM.processors=16of 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
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:
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:
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
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:
export HF_HOME=$HOME/.cache/huggingface
Check space with df -h /. Budget roughly:
| Item | Size |
|---|---|
| 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 checkpoints | 1–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:
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
| Env | Contents | Purpose |
|---|---|---|
~/ai/scratch_env | torch (latest) + numpy, tokenizers, datasets, tqdm | From-scratch training, own code. No version pins. |
~/ai/unsloth_env | torch (Unsloth-pinned) + full Unsloth stack | Fine-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
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:
- torch 2.13 does not ship cu128 wheels. Available builds are cu129, cu130, cu132.
- cu128 had genuine Blackwell problems. RTX 5090 users hit
CUBLAS_STATUS_EXECUTION_FAILEDon 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
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.
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")
source ~/ai/scratch_env/bin/activate
python3 gpu_check.py
Known-good baseline (recorded 2026-08-25)
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:
| Observation | Likely cause |
|---|---|
| ~103.9 TFLOP/s | Normal. Baseline. |
| Rises to ~100 | Performance mode / discrete graphics took effect. |
| Drops well below 70 | Thermal throttling, or the power profile reset. |
| Drops below 30 | Sysmem 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.