상태: 초안 (2026-10-17), 소스 + 읽기 전용 실측. 기준은 KubeVirt
v1.9.0(커밋79d34c17)입니다. GPU 를 붙인 VM 은 꺼져 있어 그 도메인 XML 은 코드로 그린 예상입니다(미실측). 실측은 KubeVirt CR, 노드의 allocatable, virt-handler 로그를 읽은 것입니다.
KubeVirt 소스 코드 읽기 7: 장치 패스스루
목차
1. 직관: 세 군데가 같은 장치를 알아야 한다
PCI 장치를 VM 에 넘기려면 세 군데가 같은 사실을 알아야 합니다.
- 호스트 커널: 그 장치를 원래 드라이버에서 떼어
vfio-pci에 묶어야 합니다. 그래야 사용자 공간(QEMU)이 IOMMU 의 보호 아래 장치를 직접 다룰 수 있습니다. - 쿠버네티스: 장치가 「노드의 자원」 으로 보여야 스케줄러가 그 노드에 파드를 앉히고, kubelet 이 파드에 장치 파일을 넣어 줍니다. 이 다리가 장치 플러그인입니다.
- libvirt: 도메인 XML 에
<hostdev>가 있어야 QEMU 가 그 PCI 주소를 게스트에 붙입니다.
KubeVirt 는 1을 하지 않습니다(관리자 몫). 2와 3을 잇는 일을 구성요소 넷이 나눠 합니다.
2. 그림: 일곱 걸음
3. 코드 경로
① 허용 목록 — KubeVirt CR
KubeVirt CR 의 spec.configuration.permittedHostDevices.pciHostDevices[] 에 pciVendorSelector(벤더:장치 16진수)와 resourceName 을
적습니다. 기능 게이트 HostDevices 도 켜야 합니다. 여기 없는 장치는 VMI 가 이름을 적어도 쓸 수 없습니다. virt-controller 가 파드를
그리기 전에 validatePermittedHostDevices()
(renderresources.go:493)로 확인합니다.
② 장치 관리자가 sysfs 를 훑는다 — virt-handler
virt-handler 안의 DeviceController.Run()(device_controller.go:481)은
KubeVirt 설정이 바뀔 때마다 refreshPermittedDevices() 가 불리도록 콜백을 겁니다
(:497). 이 함수가
updatePermittedHostDevicePlugins()(:208)를
거쳐 discoverPermittedHostPCIDevices()(pci_device.go:258)를
부릅니다. 이 함수가 /sys/bus/pci/devices 를 돌며 하는 일은 셋입니다.
- 장치의
벤더:장치가 허용 목록에 있는지. - 드라이버가
vfio-pci인지(:278). 아니면shouldPermitNvidia()(nvidia.go:23)가 한 번 더 봅니다. NVIDIA 드라이버에 묶인 장치라도 vGPU 프로필을 가진 가상 기능(VF)이면 받아 주고, 그 밖에는 로그를 남기고 버립니다. - 장치의 IOMMU 그룹 번호와 NUMA 노드를 읽습니다.
③ 장치 플러그인 — kubelet 에 자원 등록
자원 이름마다 PCIDevicePlugin 이 하나씩 떠서(Start, pci_device.go:62)
kubelet 의 장치 플러그인 소켓에 등록합니다. kubelet 에게 내미는 장치 ID 는 PCI 주소가 아니라 IOMMU 그룹 번호입니다
(constructDPIdevices, :132). VFIO 는 그룹
단위로 장치를 넘기므로, 같은 그룹의 장치를 서로 다른 VM 에 줄 수 없기 때문입니다. 등록이 되면 노드의 status.allocatable 에
nvidia.com/<GPU 이름>: 1 처럼 나타납니다. 같은 장치 관리자가 devices.kubevirt.io/kvm·tun·vhost-net 같은 일반 장치도 같은 방식으로 내놓습니다.
④ 파드가 자원을 요청 — virt-controller
VMI 에 spec.domain.devices.gpus[].deviceName: nvidia.com/<GPU 이름> 이 있으면, 3편의 템플릿이 자원 렌더러 옵션
WithGPUsDevicePlugins()(renderresources.go:149)로
compute 컨테이너의 limits 에 그 자원을 1개씩 넣습니다. 메모리 오버헤드에는 1Gi 가 더해집니다
(hypervisorbackend.go:130). VFIO 는 DMA 를 위해
게스트 RAM 전체를 잠그기 때문이라고 주석에 적혀 있습니다. 판단 기준은 vmitrait.HasVFIO()
(vmitrait.go:33) — 호스트 장치·GPU·SR-IOV 중 하나라도 있으면입니다.
⑤ Allocate — 장치 파일과 주소를 넘긴다
스케줄러가 그 자원이 남은 노드에 파드를 앉히면, kubelet 이 장치 플러그인의 Allocate()
(pci_device.go:152)를 부릅니다. 답은 두 가지입니다.
- 장치 파일:
/dev/vfio/vfio와/dev/vfio/<그룹 번호>(formatVFIODeviceSpecs, common.go:275), iommufd 를 쓰는 커널이면 장치별 cdev 도 넣습니다. - 환경 변수:
PCI_RESOURCE_접두사(types.go:3672)에 자원 이름을 대문자로 바꾸고/·.를_로 바꿔 붙인 이름(ResourceNameToEnvVar, util.go:89), 값은 쉼표로 이은 PCI 주소입니다. 예:PCI_RESOURCE_NVIDIA_COM_<GPU>=0000:01:00.0.
이 환경 변수가 노드(handler)와 파드(launcher)를 잇는 유일한 끈입니다. launcher 는 sysfs 를 보지 않고 이 값만 믿습니다.
⑥ launcher 가 <hostdev> 를 만든다
5편의 generateConverterContext() 가 getGPUDevices()(manager.go:1138)를
부르고, gpu.CreateHostDevices()(gpu/hostdev.go:39)가
주소 풀을 만듭니다. 풀은 ⑤의 환경 변수를 읽어 채웁니다(AddressPool.load,
addresspool.go:46). GPU 마다 주소를
하나씩 꺼내 createPCIHostDevice()(hostdev.go:141)가
api.HostDevice 를 만들고, converter 의 HostDeviceDomainConfigurator
(compute/host_device.go:42)가 도메인에
붙입니다. 같은 GPU 목록이 vGPU(mediated device)면 MDEV_PCI_RESOURCE_… 변수와 CreateMDEVHostDevices 쪽으로 갑니다. v1.9.0 에는 장치 플러그인
대신 DRA(Dynamic Resource Allocation)로 받는 길(hostdevice/dra/)도 기능 게이트 뒤에 들어와 있습니다.
⑦ 예상 XML (미실측)
코드대로라면 GPU 하나(이름 gpu1)의 도메인 XML 은 이런 모양입니다.
<hostdev mode='subsystem' type='pci' managed='no'>
<source>
<address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
</source>
<alias name='ua-gpu-gpu1'/>
</hostdev>
managed='no' 가 중요합니다. managed='yes' 면 libvirt 가 VM 을 켤 때 장치를 호스트 드라이버에서 떼어 vfio-pci 에 묶어 주지만, 파드 안의 비루트
libvirt 에는 그럴 권한이 없습니다. 그래서 묶는 일은 호스트에서 미리 해 두어야 하고(1절의 1), KubeVirt 는 이미 묶인 장치만 셉니다(②).
별칭은 사용자 별칭 접두사 ua- 에 GPU 접두사 gpu- 와 이름이 붙습니다. 또 이런 장치가 있는 VMI 는 virt-handler 가 라이브 마이그레이션
불가로 표시합니다(vmiContainsNonMigratablePCIHostDevices, vm.go:1304).
4. 실측: 허용 목록은 켜졌는데 자원이 0
작은 클러스터의 GPU 노드 하나를 VM 전용으로 쓰려고 설정해 둔 상태를 읽었습니다(2026-10-17).
-
① KubeVirt CR 에 기능 게이트
HostDevices, 그리고pciHostDevices하나(pciVendorSelector: 10DE:xxxx,resourceName: nvidia.com/<GPU 이름>)가 있었습니다. 이 GPU 를 요청하는 VM 하나는runStrategy: Halted로 꺼져 있었습니다. -
③ 그런데 그 노드의 allocatable 은
nvidia.com/<GPU 이름>: 0이었습니다. 같은 노드에 컨테이너용 GPU 자원(nvidia.com/gpu, 다른 장치 플러그인이 내놓는 것)은 1 로 보였습니다. 같은 노드에서 KubeVirt 의 일반 장치(devices.kubevirt.io/kvm·tun·vhost-net)는 각각 1k 였습니다. -
② 그 노드 virt-handler 의 로그에 이 줄이 되풀이되고 있었습니다(
pos: nvidia.go:33).Skipping non-vfio-pci device without vGPU profile 0000:01:00.0 (10de:xxxx, driver: nvidia)장치의 드라이버가
vfio-pci가 아니라 호스트의nvidia였고, vGPU 프로필도 없어 ②에서 걸러졌습니다. 그래서 ③의 장치 수가 0 입니다. 이 상태에서 VM 을 켜면 파드가 자원 부족으로 Pending 에 머물 것입니다(켜 보지는 않았습니다). 해결은 KubeVirt 가 아니라 호스트에서 해당 GPU 를vfio-pci에 묶는 것이고(커널 인자 또는 드라이버 바인딩), 컨테이너용 GPU 장치 플러그인이 같은 GPU 를 내놓지 않게 하는 일도 함께 해야 합니다. -
이 줄은 2026-10-16 하루(UTC)에만 641번, 그 virt-handler 가 다시 뜬 뒤로 7,889번 찍혀 있었습니다. 평균 2분 남짓마다 장치를 다시 훑고 있다는 뜻입니다. 무엇이 그렇게 자주 ②를 부르는지는 이번에 따라가지 않았습니다.
-
⑤의 흔적. 같은 virt-handler 로그에서, 다른 VM 의 파드가 만들어진 순간(2026-10-14T14:38:34Z) 일반 장치 플러그인의
Generic Allocate: resourceName: kvm·tun·vhost-net세 줄(generic_device.go:240)이 1ms 간격으로 찍혀 있었습니다. kubelet 이 파드를 앉히며 장치 플러그인의 Allocate 를 부르는 순간이 이렇게 보입니다.
5. 한계
- GPU 를 붙인 VM 을 켜지 않았으므로 ⑤의 환경 변수, ⑥·⑦의 XML, 게스트 안의
lspci는 모두 미실측입니다. 호스트에서 장치를 vfio-pci 로 옮기는 일은 노드 설정 변경이라 이 글의 범위(읽기)를 넘습니다. - IOMMU 그룹에 장치가 여럿인 경우(같은 그룹의 오디오 기능 등)를 어떻게 다루는지는 코드만 읽었습니다.
- vGPU·SR-IOV·DRA 길은 이름만 짚었습니다.
티티 한마디. 「허용 목록에 넣었는데 왜 안 보이지?」 싶으면 virt-handler 로그에서
Skipping을 찾아보세요. KubeVirt 는 이미 vfio-pci 에 묶인 장치만 세요. 묶는 건 호스트 몫이에요!
참고
모두 2026-10-17 에 열람했습니다.
device-manager: https://github.com/kubevirt/kubevirt/tree/v1.9.0/pkg/virt-handler/device-managerhostdevice: https://github.com/kubevirt/kubevirt/tree/v1.9.0/pkg/virt-launcher/virtwrap/device/hostdevice- KubeVirt 사용자 안내서, 호스트 장치 할당: https://kubevirt.io/user-guide/compute/host-devices/
- 쿠버네티스 장치 플러그인: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/
- 리눅스 커널 VFIO 문서: https://docs.kernel.org/driver-api/vfio.html
- libvirt 도메인 XML, 호스트 장치: https://libvirt.org/formatdomain.html#host-device-assignment