import type { Metadata } from "next" import { getTranslations, getMessages, setRequestLocale } from "next-intl/server" import { Link } from "@/i18n/navigation" import Image from "next/image" import { ExternalLink } from "lucide-react" import { DocHeader } from "@/components/ui/doc-header" import { Callout } from "@/components/ui/callout" import { Prerequisites } from "@/components/ui/prerequisites" import { Steps } from "@/components/ui/steps" import CopyableCode from "@/components/CopyableCode" export async function generateMetadata({ params, }: { params: Promise<{ locale: string }> }): Promise { const { locale } = await params const t = await getTranslations({ locale, namespace: "docs.hardware.gpuVmPassthrough.meta" }) return { title: t("title"), description: t("description"), } } type StringItem = string type RelatedItem = { label: string; href: string; tail?: string } export default async function GpuVmPassthroughPage({ params, }: { params: Promise<{ locale: string }> }) { const { locale } = await params setRequestLocale(locale) const t = await getTranslations({ locale, namespace: "docs.hardware.gpuVmPassthrough" }) const messages = (await getMessages({ locale })) as unknown as { docs: { hardware: { gpuVmPassthrough: { walkthrough: { preflight: { items: StringItem[] } switchMode: { items: StringItem[] } hostApply: { items: StringItem[] } } related: { items: RelatedItem[] } } } } } const preflightItems = messages.docs.hardware.gpuVmPassthrough.walkthrough.preflight.items const switchModeItems = messages.docs.hardware.gpuVmPassthrough.walkthrough.switchMode.items const hostApplyItems = messages.docs.hardware.gpuVmPassthrough.walkthrough.hostApply.items const relatedItems = messages.docs.hardware.gpuVmPassthrough.related.items const code = (chunks: React.ReactNode) => {chunks} const strong = (chunks: React.ReactNode) => {chunks} const em = (chunks: React.ReactNode) => {chunks} const pveLink = (chunks: React.ReactNode) => ( {chunks} ) const lxcLink = (chunks: React.ReactNode) => ( {chunks} ) const nvidiaLink = (chunks: React.ReactNode) => ( {chunks} ) const postLink = (chunks: React.ReactNode) => ( {chunks} ) const vendorResetLink = (chunks: React.ReactNode) => ( {chunks} ) const sriovLink = (chunks: React.ReactNode) => ( {chunks} ) return (
{t.rich("intro.body", { code, em, pveLink })}

{t("who.heading")}

{t.rich("who.body", { strong, em, lxcLink })}

{t.rich("prereqs.gpu", { strong, code })}, check: t("prereqs.gpuCheck") }, { label: <>{t.rich("prereqs.iommu", { strong })} }, { label: <>{t.rich("prereqs.q35", { strong, code })}, check: t("prereqs.q35Check") }, { label: <>{t.rich("prereqs.moreGpus", { strong, em })} }, { label: <>{t.rich("prereqs.nvidiaInstalled", { nvidiaLink, code, strong })} }, ]} /> {t.rich("pickOne.body", { code })}
  • {t.rich("pickOne.vmItem", { strong })}
  • {t.rich("pickOne.lxcItem", { strong, lxcLink })}

{t("running.heading")}

{t.rich("running.body", { strong })}

{t("running.imageAlt")}

{t("howRuns.heading")}

{t("howRuns.body")}

{`┌─────────────────────────────────────────────┐
│  PHASE 1 — Gather info, validate, confirm   │
│  (nothing touched yet)                      │
└──────────────────┬──────────────────────────┘
                   ▼
      ┌────────────┴────────────┐
      ▼                         ▼
  lspci detects             IOMMU enabled?
  GPUs (Intel/AMD/           ├─ No → offer to add
  NVIDIA)                    │      intel_iommu=on /
      │                      │      amd_iommu=on
      ▼                      └─ Yes → continue
  User selects GPU
      │
      ▼
  Pre-flight checks
  ├─ Not in SR-IOV
  ├─ Not D3cold (AMD)
  ├─ Has FLR or equivalent reset
  ├─ Warn if single-GPU host
  └─ Resolve IOMMU group
      │
      ▼
  Audio companion
      ├─ Has .1 sibling?  (dGPU: NVIDIA/AMD HDMI)
      │      → auto-include (never used by host)
      └─ No .1 sibling?   (Intel iGPU, split audio)
             → checklist of host audio controllers,
               default = none (user opts in)
      │
      ▼
  User selects VM
      │
      ▼
  VM is q35? ── No → abort
      │
     Yes
      ▼
  GPU already assigned elsewhere?
      │
      ├─ To LXC     → offer to remove it from LXC
      ├─ To other VM → offer to remove it there
      │               + clean up orphan audio
      │                 (skips audio whose
      │                 display sibling stays)
      └─ Free        → continue
      │
      ▼
  Show confirmation summary
  (GPU + IOMMU siblings + audio + target VM)
                   │
     ┌─────── Cancel   OR   Confirm ────┐
     ▼                                  ▼
 exit, nothing            ┌─────────────┴──────────────┐
 was changed              │  PHASE 2 — Apply changes   │
                          └─────────────┬──────────────┘
                                        ▼
                          Host:
                          ├─ /etc/modules (vfio_*)
                          ├─ /etc/modprobe.d/vfio.conf (ids=...)
                          ├─ /etc/modprobe.d/blacklist.conf
                          ├─ kernel cmdline (IOMMU if missing)
                          ├─ NVIDIA: disable udev rule + hard blacklist
                          ├─ AMD: dump ROM → /usr/share/kvm/*.bin
                          └─ update-initramfs -u -k all

                          VM config (qm set ):
                          ├─ hostpci0 = GPU (x-vga=1 unless Intel iGPU)
                          ├─ hostpci1..n = IOMMU group siblings
                          ├─ hostpci = audio function(s)
                          ├─ vga = std
                          └─ NVIDIA: cpu=host,hidden=1
                                     args=... hv_vendor_id=NV43FIX
                                        │
                                        ▼
                        ┌───────────────┴───────────────┐
                        │  PHASE 3 — Summary + reboot   │
                        └───────────────────────────────┘
                        Show what changed. If host config
                        touched → prompt reboot.`}
      

{t("walkthrough.heading")}

{t.rich("walkthrough.detect.body", { code })}

{t.rich("walkthrough.detect.tipBody", { postLink })} {t("walkthrough.detect.imageAlt")}

{t("walkthrough.preflight.intro")}

    {preflightItems.map((_, idx) => (
  • {t.rich(`walkthrough.preflight.items.${idx}`, { strong, code, em })}
  • ))}
  • {t.rich("walkthrough.preflight.audioIntro", { strong })}
    • {t.rich("walkthrough.preflight.audioDgpu", { strong, code })}
    • {t.rich("walkthrough.preflight.audioIgpu", { strong, code })}

{t("walkthrough.pickVm.body")}

{t("walkthrough.pickVm.imageAlt")}

{t("walkthrough.switchMode.intro")}

    {switchModeItems.map((_, idx) => (
  • {t.rich(`walkthrough.switchMode.items.${idx}`, { strong, code })}
  • ))}
{t("walkthrough.switchMode.imageAlt")} {t.rich("walkthrough.switchMode.smartBody", { strong, code })}

{t("walkthrough.audioPick.body")}

{t("walkthrough.audioPick.imageAlt")} {t("walkthrough.audioPick.warnBody")}

{t("walkthrough.summary.body")}

{t("walkthrough.summary.imageAlt")}

{t("walkthrough.hostApply.intro")}

    {hostApplyItems.map((_, idx) => (
  • {t.rich(`walkthrough.hostApply.items.${idx}`, { strong, code })}
  • ))}

{t.rich("walkthrough.vmApply.body", { code })}

{t.rich("walkthrough.vmApply.after1", { code, strong })}

{t.rich("walkthrough.vmApply.after2", { code })}

{t("walkthrough.reboot.body")}

{t("walkthrough.reboot.imageAlt")}

{t("vendors.heading")}

{t("vendors.nvidiaHeading")}

{t.rich("vendors.nvidiaBody", { em, code })}

{t("vendors.nvidiaMultiHeading")}

{t.rich("vendors.nvidiaMultiBody", { strong, code })}

{t("vendors.amdHeading")}

{t.rich("vendors.amdBody", { em, vendorResetLink })}

{t("vendors.intelHeading")}

{t.rich("vendors.intelBody", { code, sriovLink })}

{t("verification.heading")}

# Expect: "Kernel driver in use: vfio-pci" # Start the VM and watch for successful binding qm start journalctl -u qemu-server@.service -f # Inside the guest, drivers install normally and the GPU works as if it were physical. # NVIDIA: verify with nvidia-smi inside the VM. # AMD: verify with Windows Device Manager / DXDiag. # Intel: verify display output / intel_gpu_top inside the VM.`} className="my-4" />

{t("troubleshoot.heading")}

{t.rich("troubleshoot.code43Body", { code })} {t.rich("troubleshoot.amdResetBody", { code, em })} {t.rich("troubleshoot.stuckBootBody", { code })} {t.rich("troubleshoot.darkBody", { code })} .conf # Unblacklist the host driver rm -f /etc/modprobe.d/blacklist.conf /etc/modprobe.d/vfio.conf # (if NVIDIA was involved) mv /etc/udev/rules.d/70-nvidia.rules.proxmenux-disabled /etc/udev/rules.d/70-nvidia.rules 2>/dev/null update-initramfs -u -k all reboot`} className="my-4" /> {t.rich("troubleshoot.logBody", { code })}

{t("revert.heading")}

{t("revert.intro")}

--delete hostpci0 # (repeat for hostpci1, hostpci2, ... if multiple were added) # ─── OPTIONAL — not required ────────────────────────────────────── # The steps above already free the VM from the GPU. The lines below # are only needed if you also want the host to use the GPU again # (e.g. for LXC sharing or host-side transcoding). Skip this block # if you simply want to stop passing the GPU to the VM. # ────────────────────────────────────────────────────────────────── # Release the GPU back to the host driver: rm -f /etc/modprobe.d/vfio.conf rm -f /etc/modprobe.d/blacklist.conf # careful — this file may have other blacklists # NVIDIA only — re-enable the udev rule + unpin the hard blacklist mv /etc/udev/rules.d/70-nvidia.rules.proxmenux-disabled /etc/udev/rules.d/70-nvidia.rules 2>/dev/null rm -f /etc/modprobe.d/nvidia-blacklist.conf update-initramfs -u -k all reboot`} className="my-4" />

{t("related.heading")}

    {relatedItems.map((item) => (
  • {item.label} {item.tail}
  • ))}
) }