Post

PCIe GPU Passthrough on a Dell Wyse 5070 Extended (AMD Embedded Radeon E9173)

A little while back I picked up two decommissioned Dell Wyse 5070s from work — the kind of thin clients that were never meant to do anything more exciting than run a remote desktop session. Turns out these two are the “Extended” chassis variant, and someone had them ordered from the factory with the optional dedicated GPU: an AMD Embedded Radeon E9173 module. No riser hacking required — the card was already sitting there. Naturally, that meant one of them was getting turned into a GPU-passthrough box, and after a frustrating detour through a very specific AMD reset quirk, it actually worked.

I didn’t write anything down while I was doing it (rookie mistake), so this post is reconstructed from my shell history after the fact. It’s as much a note to future-me as it is a guide, so treat the steps below as a solid starting point rather than gospel for your exact setup.

This post started as a reconstruction from shell history, then got filled in with the real BIOS options, grub config, and VM config pulled straight off the box — so what follows matches what’s actually running on wyse2.

The build

A Dell Wyse 5070 Extended — the chassis variant with room for a factory add-in card — running Proxmox VE, with the dedicated GPU option Dell offered for it already installed. The goal: pass that GPU straight through to a VM instead of letting the Proxmox host touch it.

  • Host CPU: Intel (the kernel cmdline uses intel_iommu=on, so this is the Intel Celeron/Pentium variant of the 5070, not the AMD one)
  • GPU: an AMD Embedded Radeon E9173 — the factory-fit option for the Extended chassis. It’s a GCN 4.0 “Lexa” chip with 2GB GDDR5 over a PCIe 3.0 x8 link, 35W TDP, no external power connector needed. PCI ID 1002:699f matches this Lexa-family silicon, and its audio function 1002:aae0 rides along on the same card.
  • Hypervisor: Proxmox VE

Verify this: if you’re following along on different hardware, swap in your actual card’s PCI ID — 699f is specific to the Lexa/E9173 family.

Before you touch the OS: BIOS settings

None of this shows up in bash history, but it has to be right before any of the software steps below will work. Enter setup with F2 at boot, then head to the Virtualization support screen. Dell’s own manual for the 5070 lists exactly two options there: a “Virtualization” toggle for Intel Virtualization Technology (enabled by default), and a separate “VT for Direct I/O” option that lets a hypervisor use those extra Direct I/O capabilities — which is off by default.

  • Virtualization → Enabled (this is Intel VT-x, and it’s already on out of the box)
  • VT for Direct I/O → Enabled (this is VT-d — the one you actually have to flip yourself)

That second one is the setting the whole rest of this post depends on: no VT-d, no DMAR ACPI table, no IOMMU groups, no passthrough.

Step 1 — Turn on IOMMU at the kernel level

1
nano /etc/default/grub

Here’s the actual line from /etc/default/grub:

1
GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt initcall_blacklist=sysfb_init pcie_acs_override=downstream,multifunction"

Three flags doing three different jobs:

  • intel_iommu=on iommu=pt — the baseline: turns on VT-d and puts the IOMMU in passthrough mode for everything that isn’t explicitly isolated.
  • initcall_blacklist=sysfb_init — stops the kernel’s generic sysfb (simple-framebuffer) driver from registering a platform device against the GPU’s UEFI framebuffer during early boot. Without this, sysfb/simpledrm can grab the E9173 before vfio-pci gets a chance to, and no amount of blacklisting amdgpu fixes that — it’s a different driver entirely.
  • pcie_acs_override=downstream,multifunction — forces the kernel to treat each downstream port and multifunction device as its own isolation boundary. This one is a strong signal about the hardware: it means the 5070’s chipset was not exposing clean per-device IOMMU groups on its own, and the GPU’s group needed to be split apart from something else sharing it. See below.
1
2
update-grub
reboot

Confirm it actually took effect:

1
2
3
cat /proc/cmdline                    # should show intel_iommu=on iommu=pt
ls /sys/firmware/acpi/tables/DMAR    # must exist — proves VT-d is on in BIOS
ls /sys/kernel/iommu_groups/         # must list numbered groups

If DMAR is missing, the BIOS setting didn’t take — go back and check VT-d is actually enabled, not just the kernel flag.

Step 2 — Find the GPU and its IOMMU group

1
lspci -nn | grep 01:00

This is where 1002:699f (the E9173 GPU function) and 1002:aae0 (its DisplayPort audio function) come from — both live on the same bus address and need to move together.

1
2
GRP=$(readlink /sys/bus/pci/devices/0000:01:00.0/iommu_group)
ls /sys/kernel/iommu_groups/${GRP##*/}/devices/

On wyse2 that comes back with exactly one device:

1
2
3
4
root@wyse2:~# GRP=$(readlink /sys/bus/pci/devices/0000:01:00.0/iommu_group)
ls /sys/kernel/iommu_groups/${GRP##*/}/devices/
0000:01:00.0
root@wyse2:~#

That’s actually notable: normally, sibling functions of the same physical card — here, the GPU at .0 and its HDMI/DP audio at .1 — land in the same IOMMU group by default, because the PCIe spec has no way to prove two functions behind the same device are isolated from each other without ACS support. This board’s chipset doesn’t implement that, which is exactly what the multifunction half of pcie_acs_override=downstream,multifunction from Step 1 is for — it forces the kernel to treat sibling functions as separately isolated. The result: 0000:01:00.0 gets its own single-device group, and 0000:01:00.1 (the audio function) would show up in its own separate one-device group too if you checked it the same way.

Worth knowing: ACS override is a well-known “trust it and move on” hack — the isolation is now assumed by the kernel, not proven by hardware. Fine for a homelab box you control end to end; not something to reach for on a shared or untrusted host.

Since the GPU and its audio ended up in different groups, they still need to move as a pair for passthrough to make sense — which is exactly why the VM config in Step 6 targets the whole slot (hostpci0: 0000:01:00,..., with no .0/.1 suffix) rather than a single function: Proxmox/QEMU reads that as “every function at this address,” regardless of which IOMMU group each one individually landed in.

Step 3 — Bind the GPU to vfio-pci before anything else can claim it

1
2
3
4
echo "options vfio-pci ids=1002:699f,1002:aae0 disable_vga=1" > /etc/modprobe.d/vfio.conf
printf 'blacklist amdgpu\nblacklist radeon\n' >> /etc/modprobe.d/blacklist.conf
update-initramfs -u -k all
reboot
1
lspci -nnk -s 01:00

Check the “Kernel driver in use” line — it should say vfio-pci, not amdgpu. Here’s what a clean bind looks like on wyse2 (one of the two boxes):

1
2
3
4
5
6
7
8
9
root@wyse2:~# lspci -nnk -s 01:00
01:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Lexa PRO [Radeon 540/540X/550/550X / RX 540X/550/550X] [1002:699f] (rev 81)
        Subsystem: Advanced Micro Devices, Inc. [AMD/ATI] Device [1002:ea73]
        Kernel driver in use: vfio-pci
        Kernel modules: amdgpu
01:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Baffin HDMI/DP Audio [Radeon RX 550 640SP / RX 560/560X] [1002:aae0]
        Subsystem: Advanced Micro Devices, Inc. [AMD/ATI] Baffin HDMI/DP Audio [Radeon RX 550 640SP / RX 560/560X] [1002:aae0]
        Kernel driver in use: vfio-pci
        Kernel modules: snd_hda_intel

Both functions show Kernel driver in use: vfio-pci, with the host driver (amdgpu / snd_hda_intel) demoted to just “available module” — that’s exactly the state you want before handing the card to a VM.

Step 4 — Belt and suspenders: force load order with softdep

Blacklisting is sometimes not enough on its own (module autoloading can race during initramfs), so:

1
2
3
4
5
6
7
8
9
10
11
12
13
cat >> /etc/modules <<'EOF'
vfio
vfio_iommu_type1
vfio_pci
EOF

cat >> /etc/modprobe.d/vfio.conf <<'EOF'
softdep amdgpu pre: vfio-pci
softdep snd_hda_intel pre: vfio-pci
EOF

update-initramfs -u -k all
reboot
1
lspci -nnk -s 01:00   # re-confirm vfio-pci owns the card

Side quest: general Proxmox housekeeping

Not part of passthrough, but the same session also ran the standard community-scripts Proxmox post-install tuning script, and reclaimed the thin-pool storage into the root filesystem:

1
2
3
4
5
6
7
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pve-install.sh)"
reboot

pvesm remove local-lvm
lvremove /dev/pve/data
lvextend -l +100%FREE /dev/pve/root
resize2fs /dev/mapper/pve-root

Worth its own short post if you want to document it — I’ve left it out of the passthrough walkthrough below since it’s unrelated to the GPU.

Step 5 — The AMD reset bug, and why vendor-reset is non-negotiable

Here’s the quirk you half-remembered: many older AMD GPUs (the whole Polaris/Lexa generation — which is exactly what a 699f card is — plus early Vega) never got a working PCIe Function Level Reset implemented in firmware. In practice that means:

  • First VM boot after a host reboot: GPU passthrough works fine.
  • Stop or reset the VM and start it again: the GPU is left in a broken state and either the VM hangs on boot or the guest never sees a working GPU — until you power-cycle the whole Proxmox host.

That matches the repeated qm start / qm restart / qm reboot / qm list entries in the history around this point — that’s the reset bug in action, not random flakiness.

gnif/vendor-reset is a small kernel module that ships hardware-specific reset sequences for exactly these GPUs, so the card comes back cleanly every time the VM restarts.

1
2
3
4
5
6
apt install pve-headers-$(uname -r)
apt install git dkms pve-headers
git clone https://github.com/gnif/vendor-reset.git
cd vendor-reset/
dkms install .
echo "vendor-reset" >> /etc/modules

Secure Boot + DKMS = you need to enroll a MOK key

Because this is a self-built, unsigned kernel module, a Secure-Boot-enabled host will refuse to load it until you enroll the DKMS signing key:

1
mokutil --import /var/lib/dkms/mok.pub

Note: mokutil --import prompts for a one-time password right there in the terminal — you choose it yourself on the spot. Write it down, because you need it again on the next reboot, at the blue MOK Management screen (enroll → confirm → type that password), and it’s not a screen you can paste into. The command shows up twice in the log, which usually means the first enrollment didn’t stick — often because the reboot afterward got skipped, or the key changed after a DKMS rebuild. If lsmod | grep vendor_reset comes up empty after rebooting, redoing this is the first thing to try.

1
2
update-grub
reboot

At boot, go through the MOK enrollment screen, then confirm the module is actually loaded:

1
2
lsmod | grep vendor_reset
lspci -nnk -s 01:00     # driver should still read vfio-pci

Step 6 — Attach the GPU to the VM

Checking /etc/pve/qemu-server/101.conf confirms how this landed — either set through the web UI or a qm set run outside this shell session:

1
2
3
4
5
6
7
8
9
10
11
12
13
agent: 1
balloon: 0
bios: ovmf
boot: order=scsi0;net0
cores: 4
cpu: x86-64-v2-AES
efidisk0: local:101/vm-101-disk-1.qcow2,efitype=4m,ms-cert=2023k,pre-enrolled-keys=1,size=528K
hostpci0: 0000:01:00,pcie=1,x-vga=1
machine: pc-q35-11.0
memory: 5120
ostype: win11
scsi0: local:101/vm-101-disk-0.qcow2,discard=on,iothread=1,size=90G
scsihw: virtio-scsi-single

The bits that matter for passthrough:

  • hostpci0: 0000:01:00,pcie=1,x-vga=1 — the whole 01:00 device (both the GPU and audio function) passed through, pcie=1 so it attaches as a real PCIe device instead of legacy PCI, x-vga=1 marking it as the VM’s primary display.
  • machine: pc-q35-11.0 — Q35 chipset, required for PCIe passthrough (the older i440fx machine type doesn’t support it properly).
  • bios: ovmf + efidisk0: ...,pre-enrolled-keys=1 — UEFI firmware with Microsoft’s Secure Boot keys pre-enrolled at disk creation, paired with ostype: win11 — this is a Windows 11 guest, which requires Secure Boot to install and boot at all.

The repeated qm start 101 / qm restart 101 / qm reboot 101 attempts right before the vendor-reset install, and the clean qm start 101 after it, is a good before/after signature of the reset bug getting fixed.

Step 7 — Guest Secure Boot

1
qm enroll-efi-keys 101

The config already shows efidisk0: ...,pre-enrolled-keys=1, which normally enrolls Microsoft’s default Secure Boot certificates automatically when the EFI disk is created. Running qm enroll-efi-keys on top of that four times in a row is harmless (it’s idempotent) — it more likely means the OVMF NVRAM needed a manual nudge at some point while troubleshooting, possibly after the EFI disk got recreated. Either way, this is the command to reach for if a Windows 11 guest ever complains that Secure Boot isn’t satisfied.

Lessons learned

  • pcie_acs_override and initcall_blacklist=sysfb_init are easy to forget about. They don’t show up in most “enable IOMMU” tutorials because plenty of boards don’t need them — but on hardware that doesn’t implement ACS on its downstream ports or between a card’s own sibling functions (like this one, where the GPU and its audio function only ended up isolated because of the multifunction override), or with a UEFI GOP framebuffer that sysfb/simpledrm grabs before vfio-pci can, passthrough silently fails without them, and the fix isn’t obvious from the symptom.
  • Blacklist and softdep. Blacklisting amdgpu/radeon stops the host driver from loading later, but a softdep ... pre: vfio-pci line is what actually wins the race during early boot.
  • Old AMD cards + passthrough = plan for vendor-reset from day one. Anything Polaris/Lexa/early-Vega will hang on VM restart without it — it’s not an edge case, it’s the default behavior.
  • Secure Boot + DKMS means remembering a MOK password. Write down the one you set when mokutil --import asks for it; you’ll need it at the next boot, in a screen that doesn’t take pasted input.
  • Always re-check lspci -nnk -s 01:00 after every reboot during setup — it’s the fastest way to see whether vfio-pci, amdgpu, or vendor-reset currently owns the card.

Full command reference

Cleaned-up, step-ordered command log ```bash # --- IOMMU --- nano /etc/default/grub # GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt initcall_blacklist=sysfb_init pcie_acs_override=downstream,multifunction" update-grub reboot cat /proc/cmdline ls /sys/firmware/acpi/tables/DMAR ls /sys/kernel/iommu_groups/ # --- Identify GPU --- lspci -nn | grep 01:00 GRP=$(readlink /sys/bus/pci/devices/0000:01:00.0/iommu_group) ls /sys/kernel/iommu_groups/${GRP##*/}/devices/ # --- Bind to vfio-pci --- echo "options vfio-pci ids=1002:699f,1002:aae0 disable_vga=1" > /etc/modprobe.d/vfio.conf printf 'blacklist amdgpu\nblacklist radeon\n' >> /etc/modprobe.d/blacklist.conf update-initramfs -u -k all reboot lspci -nnk -s 01:00 # --- Force load order --- cat >> /etc/modules <<'EOF' vfio vfio_iommu_type1 vfio_pci EOF cat >> /etc/modprobe.d/vfio.conf <<'EOF' softdep amdgpu pre: vfio-pci softdep snd_hda_intel pre: vfio-pci EOF update-initramfs -u -k all reboot lspci -nnk -s 01:00 # --- vendor-reset for the AMD reset bug --- apt install pve-headers-$(uname -r) apt install git dkms pve-headers git clone https://github.com/gnif/vendor-reset.git cd vendor-reset/ dkms install . echo "vendor-reset" >> /etc/modules mokutil --import /var/lib/dkms/mok.pub update-grub reboot # enroll the MOK key at the blue screen lsmod | grep vendor_reset lspci -nnk -s 01:00 # --- Attach to VM and start --- qm set 101 -hostpci0 0000:01:00,pcie=1,x-vga=1 # or via web UI qm start 101 # --- Guest Secure Boot (if OVMF + Secure Boot in the VM) --- qm enroll-efi-keys 101 qm start 101 ```

If you’re chasing the same AMD reset bug, the gnif/vendor-reset GitHub repo has an up-to-date list of which chips are supported — worth checking before you assume DKMS alone will fix it.

This post is licensed under CC BY 4.0 by the author.