How to use libvmaf_cuda on Windows: an easy-to-follow guide (WSL2 + Docker + NVIDIA)
**TL;DR:** Running GPU-accelerated VMAF on Windows is surprisingly painful. After spending hours fighting broken builds and undocumented errors, the only reliable path I found is **WSL2 + Docker Desktop + NVIDIA Container Toolkit + a CUDA-enabled FFmpeg**. This guide walks you through the whole setup using the easyVmaf project, so you can skip the trial-and-error I went through.
This guide uses **easyVmaf** , a project that ships a `Dockerfile.cuda` specifically built for this purpose. We'll run it inside WSL2, with Docker Desktop and the NVIDIA Container Toolkit handling the GPU passthrough.
**The result:** VMAF running at ~15x real-time speed on an RTX 3060 Mobile. A 46-minute video gets analyzed in about 3 minutes. On CPU, the same task would take like an hour.
> ⚠️ **AMD and Intel GPUs will not work with this guide.** `libvmaf_cuda` is NVIDIA-exclusive.
## Table of contents
* The Problem
* Prerequisites
* Part 1 — Setting up the environment
* 1.1 Install WSL2
* 1.2 Accessing Windows drives from WSL
* 1.3 Verify GPU access from WSL
* 1.4 Install Docker Desktop
* 1.5 Install the NVIDIA Container Toolkit
* Part 2 — Building the easyVmaf image
* 2.1 Clone the repository
* 2.2 Critical fix: pin nv-codec-headers
* 2.3 Build the image
* 2.4 Verify the libvmaf_cuda filter
* Part 3 — Running VMAF with GPU acceleration
* 3.1 capabilities=video
* 3.2 Full analysis command
* Troubleshooting
* Conclusion
* References
* Let's Connect
## The Problem
If you've ever tried to calculate VMAF on Windows, you already know that:
* The standard `libvmaf` filter works, but it runs on the **CPU** (slow).
* `libvmaf_cuda` **does not exist in any prebuilt Windows binary** (not in gyan.dev, not in BtbN, not anywhere _that I know_).
* Most information online is scattered, outdated, or simply missing.
## Prerequisites
Before starting, make sure you have:
Component | Minimum requirement
---|---
**Windows** | Windows 10 (version 2004+) or Windows 11
**NVIDIA GPU** | Any CUDA-capable GPU (I'm using an RTX 3060 Mobile)
**NVIDIA drivers** | Version 525+ (for CUDA 12.x) — download here
**Disk space** | ~30 GB free (WSL + Docker + images)
**RAM** | 16 GB recommended (WSL2 is memory-hungry)
> ⚠️ **Do not install NVIDIA drivers inside WSL.** WSL automatically uses the drivers from Windows. Installing Linux drivers inside WSL will break GPU passthrough.
## Part 1 — Setting up the environment
This part covers everything needed to get a working Linux + Docker + GPU stack: WSL2, Docker Desktop, and the NVIDIA Container Toolkit.
### 1.1 Install WSL2
Open **PowerShell as Administrator** and run
wsl --install
This command will:
* download and install WSL2 kernel
* install Ubuntu distro by default
**Restart Windows** when prompted.
After the restart, open PowerShell again and verify:
wsl --list --verbose
Expected output:
NAME STATE VERSION
* Ubuntu Running 2
Make sure `VERSION` is `2`. If it says `1`, upgrade with:
wsl --set-version Ubuntu 2
wsl --set-default-version 2
#### Ubuntu update
Open your Ubuntu terminal either by typing in PowerShell
ubuntu
or
wsl
Once in Ubuntu terminal enter:
sudo apt update && sudo apt upgrade -y
### 1.2 Accessing Windows drives from WSL
If you're new to Linux, one of the first things to understand is that WSL doesn't use `C:\`, `D:\`, etc. Instead, it mounts every Windows drive under `/mnt/`.
Windows path | WSL path
---|---
`C:\Users\YourName\Videos` | `/mnt/c/Users/YourName/Videos`
`D:\Movies` | `/mnt/d/Movies`
`E:\Backups\2026` | `/mnt/e/Backups/2026`
### 1.3 Verify GPU access from WSL
if you already have installed NVIDIA drivers on Windows, run:
nvidia-smi
You should see the `nvidia-smi` table with your GPU listed. If it doesn't work, your Windows drivers are outdated or WSL isn't configured properly.
Wed Sep 16 09:30:35 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 615.71.08 KMD Version: 616.92 CUDA UMD Version: 13.4 |
+-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA GeForce RTX 3060 ... On | 00000000:01:00.0 On | N/A |
| N/A 55C P8 14W / 115W | 1085MiB / 6144MiB | 6% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| No running processes found |
+-----------------------------------------------------------------------------------------+
### 1.4 Install Docker Desktop
Download Docker Desktop from: https://www.docker.com/products/docker-desktop/
During installation, make sure to check **"Use WSL 2 instead of Hyper-V"**.
#### Configure WSL2 integration
Once installed, open Docker Desktop and go to Settings
**General tab:**
* Check **Use the WSL 2 based engine**
* Click **Apply & Restart**
**Resources → WSL Integration:**
* Check **Enable integration with my default WSL distro**
* Turn on the Ubuntu toggle
**Resources → Advanced (optional):**
* Uncheck **"Enable Resource Saver"**
> If "Resource Saver" is enabled, Docker will suspend WSL2 after inactivity, and you'll have to restart it manually.
#### Verify Docker works in WSL
In your Ubuntu terminal:
docker --version
Expected output:
Docker version 27.3.1, build ce12230
> ⚠️ If you get `var/run/docker.sock: connect: permission denied.` jump to the Troubleshooting section.
### 1.5 Install the NVIDIA Container Toolkit
This is the component that allows Docker containers to access the GPU.
#### Install
In your WSL Ubuntu terminal:
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg && \
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt update
sudo apt install -y nvidia-container-toolkit
#### Configure the Docker runtime
sudo nvidia-ctk runtime configure --runtime=docker
Expected output:
INFO[0000] Config file does not exist; using empty config
INFO[0000] Wrote updated config to /etc/docker/daemon.json
INFO[0000] It is recommended that docker daemon be restarted.
#### Restart Docker
Restart **Docker Desktop** from Windows (either via the restart icon or by quitting and reopening it).
#### Verify Docker can see the GPU
Once Docker Desktop is running again, execute in your WSL terminal:
docker run --rm --gpus all nvidia/cuda:12.3.2-base-ubuntu22.04 nvidia-smi
You should see the `nvidia-smi` table with your GPU listed.
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 615.71.08 KMD Version: 616.92 CUDA UMD Version: 13.4 |
+-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA GeForce RTX 3060 ... On | 00000000:01:00.0 On | N/A |
| N/A 55C P8 14W / 115W | 1085MiB / 6144MiB | 6% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| No running processes found |
+-----------------------------------------------------------------------------------------+
## Part 2 — Building the easyVmaf image
We'll use the easyVmaf project, which ships a `Dockerfile.cuda` configured for GPU-accelerated VMAF.
### 2.1 Clone the repository
cd ~
git clone --depth 1 https://github.com/gdavila/easyVmaf.git
cd easyVmaf
### 2.2 Critical fix: pin nv-codec-headers
> 💡 **This step is undocumented anywhere else.** I figured it out after spending hours debugging the compilation error with the help of AI. Skip it and your build will fail.
The original `Dockerfile.cuda` clones the **latest** version of `nv-codec-headers`, which is **incompatible with FFmpeg 8.1**. You'll get this error during the build:
libavcodec/nvenc.c:2529:42: error: 'NV_ENC_CLOCK_TIMESTAMP_SET'
has no member named 'countingType'; did you mean 'countingTypeLSB'?
In recent versions, NVIDIA renamed `countingType` to `countingTypeLSB/countingTypeMSB`, but FFmpeg 8.1 still uses `countingType`.
**The fix:** Pin `nv-codec-headers` to version `n12.1.14.0`, which still uses `countingType` **and** already includes the modern CUDA functions (`cuStreamCreateWithPriority`, `cuMemHostAlloc`, etc.) that `libvmaf_cuda` needs.
Edit the Dockerfile:
nano Dockerfile.cuda
* Press `CTRL+W`, type `nv-codec-headers`, press `ENTER`. You'll land on this line:
RUN git clone --depth 1 https://git.videolan.org/git/ffmpeg/nv-codec-headers.git && \
cd nv-codec-headers && \
make install
Change it by adding `n12.1.14.0` before --depth:
RUN git clone --branch n12.1.14.0 --depth 1 https://git.videolan.org/git/ffmpeg/nv-codec-headers.git && \
cd nv-codec-headers && \
make install
Save with `CTRL+X`, then `Y`, then `ENTER`.
### 2.3 Build the image
docker build -f Dockerfile.cuda -t easyvmaf:cuda .
### 2.4 Verify the `libvmaf_cuda` filter
> ⚠️ The `easyvmaf:cuda` image defines `easyVmaf` (its own CLI) as the `ENTRYPOINT`. To run raw `ffmpeg`, you must override the entrypoint.
>
docker run --rm --gpus all --entrypoint ffmpeg easyvmaf:cuda -filters | grep -E "libvmaf|scale_cuda"
Expected output:
.. libvmaf VV->V Calculate the VMAF between two video streams.
.. libvmaf_cuda VV->V Calculate the VMAF between two video streams.
.. scale_cuda V->V GPU accelerated video resizer
**If you see`libvmaf_cuda`, you're done with the setup.**
## Part 3 — Running VMAF with GPU acceleration
### 3.1 capabilities=video
By default, `--gpus all` alone only grants the compute and utility capabilities. It does not mount the video decode/encode libraries `libnvcuvid.so.1` (NVDEC) and `libnvidia-encode.so.1` (NVENC). Since we tell FFmpeg to decode with `-hwaccel cuda`, it needs those libraries. Without them, FFmpeg fails with:
Cannot load libnvcuvid.so.1
Failed loading nvcuvid.
Failed setup for format cuda: hwaccel initialisation returned error.
**The fix:** explicitly request the `video` capability:
--gpus all,capabilities=video
Now Docker mounts `libcuda.so.1` (CUDA compute), `libnvcuvid.so.1` (NVDEC), and `libnvidia-encode.so.1` (NVENC). FFmpeg can then decode, filter, and analyze entirely on the GPU.
### 3.2 Full analysis command
docker run --gpus all,capabilities=video --rm --entrypoint ffmpeg \
-v "/path/to/your/videos":/videos \
easyvmaf:cuda \
-hwaccel cuda -hwaccel_output_format cuda \
-i "/videos/distorted.mkv" \
-hwaccel cuda -hwaccel_output_format cuda \
-i "/videos/reference.mkv" \
-filter_complex "[0:v]scale_cuda=format=yuv420p[dis];[1:v]scale_cuda=format=yuv420p[ref];[dis][ref]libvmaf_cuda=log_fmt=json:log_path=/videos/vmaf_full.json" \
-f null -
#### Real-world result
Here's the output from a test on a 46-minute video file:
[Parsed_libvmaf_cuda_2 @ 0x760b44004f80] VMAF score: 93.558906
speed=14.9x elapsed=0:03:05.05
[out#0/null @ 0x5ef225e22140] video:27438KiB audio:2072848KiB subtitle:0KiB
frame=66265 fps=350 q=-0.0 Lsize=N/A time=00:46:03.79 bitrate=N/A speed=14.6x elapsed=0:03:09.43
**3 minutes for a 46-minute video at ~15x real-time speed.** That's the whole point of using `libvmaf_cuda`.
## Troubleshooting
### `var/run/docker.sock: connect: permission denied`
docker: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Head "http://%2Fvar%2Frun%2Fdocker.sock/_ping": dial unix /var/run/docker.sock: connect: permission denied.
Your user doesn't belong to the `docker` group, which owns `/var/run/docker.sock`.
fix:
enter this command in your WSL Ubuntu terminal
sudo usermod -aG docker $USER
This adds your user to the `docker` group. Then **close and reopen WSL** (group changes only apply to new sessions), and verify:
groups
Expected output:
youruser adm cdrom sudo dip plugdev users docker
### `Cannot load libnvcuvid.so.1`
Missing `,capabilities=video` in the `--gpus` flag. See section 3.1.
### `NV_ENC_CLOCK_TIMESTAMP_SET has no member named 'countingType'`
You didn't pin `nv-codec-headers` to `n12.1.14.0`. See section 2.2.
### WSL2 hangs after inactivity
Edit `C:\Users\YOUR_USER\.wslconfig`:
[wsl2]
vmIdleTimeout=-1
Then run `wsl --shutdown` in PowerShell. Also disable "Resource Saver" in Docker Desktop (see section 1.3).
## Conclusion
Setting up `libvmaf_cuda` on Windows was a long journey. At the start, I couldn't find much information about it — most guides either stop at "use `libvmaf` on CPU" or assume you're on Linux. Even though this isn't 100% native to Windows (it runs through WSL2 + Docker), it's a solid alternative that is absolutely worth the effort.
Once it's working, you get VMAF analysis at **15x real-time speed** , which completely changes what's practical for video quality workflows.
## References
* easyVmaf — the project powering this guide
* NVIDIA Container Toolkit docs
* WSL2 GPU support
* Netflix VMAF
## Let's Connect
If you found this post useful and you're looking for a Full Stack Developer or a technical writer, feel free to reach out!
* GitHub
* **Telegram** : @rtaglia
Thanks for reading! 🙌