[PATCH v2 21/32] gpu: nova-core: vgpu: add instance create/destroy
From: Zhi Wang
Date: Mon Sep 14 2026 - 04:22:52 EST
A vGPU instance holds the resources used by a VF. The VF must have a
vGPU type assigned before instance creation. The instance lifetime is
intended to follow VFIO device use: create it on open and destroy it
on close.
Add an instance registry to VgpuManager with creation and destruction
operations. Creation reserves channel IDs and a VRAM slot according
to the assigned vGPU type; destruction releases those reservations.
Signed-off-by: Zhi Wang <zhiw@xxxxxxxxxx>
---
drivers/gpu/nova-core/gpu.rs | 12 +-
drivers/gpu/nova-core/vgpu.rs | 23 ++-
drivers/gpu/nova-core/vgpu/commands.rs | 1 -
drivers/gpu/nova-core/vgpu/instance.rs | 225 +++++++++++++++++++++++++
4 files changed, 247 insertions(+), 14 deletions(-)
create mode 100644 drivers/gpu/nova-core/vgpu/instance.rs
diff --git a/drivers/gpu/nova-core/gpu.rs b/drivers/gpu/nova-core/gpu.rs
index ebf29b06a8e1..61adf412bae7 100644
--- a/drivers/gpu/nova-core/gpu.rs
+++ b/drivers/gpu/nova-core/gpu.rs
@@ -54,14 +54,16 @@
},
};
-#[cfg_attr(not(CONFIG_KUNIT = "y"), expect(dead_code))]
mod channel;
mod hal;
mod regs;
use self::channel::TOTAL_CHANNELS;
-pub(crate) use self::channel::ChannelIdPool;
+pub(crate) use self::channel::{
+ ChannelIdPool,
+ ChannelIdReservation, //
+};
macro_rules! define_chipset {
({ $($variant:ident = $value:expr),* $(,)* }) =>
@@ -313,7 +315,7 @@ struct GspResources<'gpu> {
#[pin_data]
pub(crate) struct Gpu<'gpu> {
spec: Spec,
- vgpu: Option<VgpuManager<'gpu>>,
+ vgpu: Option<Pin<KBox<VgpuManager<'gpu>>>>,
/// GSP event interrupt registration.
///
/// Declared before `gsp_resources` so it is dropped first: `free_irq` runs, waiting out any
@@ -444,7 +446,7 @@ pub(crate) fn new<'a>(
let info = &gsp_resources.boot_result.static_info;
match gsp_resources.vgpu_state {
VgpuState::Disabled => None,
- VgpuState::Enabled { .. } => Some(VgpuManager::new(
+ VgpuState::Enabled { .. } => Some(KBox::pin_init(VgpuManager::new(
// SAFETY: `chid_pool` is initialized above at its final pinned address.
// The private manager and its pool borrow cannot escape this `Gpu`.
// Completed field drop order drops the manager before the pool; on failure,
@@ -453,7 +455,7 @@ pub(crate) fn new<'a>(
&info.fifo_engine_list,
info.vmmu_segment_size,
TOTAL_CHANNELS,
- )),
+ ), GFP_KERNEL)?),
}
},
diff --git a/drivers/gpu/nova-core/vgpu.rs b/drivers/gpu/nova-core/vgpu.rs
index 0f9f5be752b5..f3e40e6de6f0 100644
--- a/drivers/gpu/nova-core/vgpu.rs
+++ b/drivers/gpu/nova-core/vgpu.rs
@@ -4,8 +4,10 @@
use kernel::{
device,
+ new_mutex,
pci,
- prelude::*, //
+ prelude::*,
+ sync::Mutex, //
};
use crate::{
@@ -23,6 +25,7 @@
mod commands;
mod fw;
mod hal;
+mod instance;
mod vram;
/// vGPU state detected during GPU construction.
@@ -88,9 +91,13 @@ fn query_state(
}
}
+use self::instance::VgpuInstances;
+
/// Runtime resources for an enabled vGPU boot.
+#[pin_data]
pub(crate) struct VgpuManager<'gpu> {
- #[expect(dead_code)]
+ #[pin]
+ instances: Mutex<VgpuInstances<'gpu>>,
chid_pool: &'gpu ChannelIdPool,
vmmu_segment_size: u64,
total_channels: u32,
@@ -104,22 +111,22 @@ pub(crate) fn new(
fifo_engine_list: &FifoEngineList,
vmmu_segment_size: u64,
total_channels: u32,
- ) -> Self {
- Self {
+ ) -> impl PinInit<Self> + use<'gpu> {
+ let fifo_engine_list = *fifo_engine_list;
+ pin_init!(Self {
+ instances <- new_mutex!(VgpuInstances::new(), "nova-core::vgpu-instances"),
chid_pool,
vmmu_segment_size,
total_channels,
- fifo_engine_list: *fifo_engine_list,
- }
+ fifo_engine_list,
+ })
}
/// Returns the VMMU segment size in bytes, or zero if GSP-RM omitted it.
- #[expect(dead_code)]
const fn vmmu_segment_size(&self) -> u64 {
self.vmmu_segment_size
}
- #[expect(dead_code)]
const fn total_channels(&self) -> u32 {
self.total_channels
}
diff --git a/drivers/gpu/nova-core/vgpu/commands.rs b/drivers/gpu/nova-core/vgpu/commands.rs
index b5fa3575b1d7..bdedd7fc21ad 100644
--- a/drivers/gpu/nova-core/vgpu/commands.rs
+++ b/drivers/gpu/nova-core/vgpu/commands.rs
@@ -43,7 +43,6 @@ pub(super) fn query_assigned_vf_type(cmdq: &Cmdq<'_>, dbdf: Dbdf) -> Result<u32>
}
/// Query and decode the firmware properties of one vGPU type.
-#[expect(dead_code)]
pub(super) fn query_vgpu_properties(cmdq: &Cmdq<'_>, type_id: u32) -> Result<KBox<VgpuProperties>> {
let response = cmdq.send_gmc_and_receive(
GMCAPI_CMD_QUERY_VGPU_PROPERTIES,
diff --git a/drivers/gpu/nova-core/vgpu/instance.rs b/drivers/gpu/nova-core/vgpu/instance.rs
new file mode 100644
index 000000000000..99a8f47e72d1
--- /dev/null
+++ b/drivers/gpu/nova-core/vgpu/instance.rs
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+
+use core::num::NonZeroUsize;
+
+use kernel::{
+ prelude::*,
+ ptr::Alignment,
+ sizes::SizeConstants, //
+};
+
+use crate::gsp::cmdq::Cmdq;
+
+use crate::{
+ gpu::ChannelIdReservation,
+ mm::GpuMm, //
+};
+
+use super::{
+ vram::{
+ VgpuVramLayout,
+ VgpuVramSlot,
+ VgpuVramSlotAllocator, //
+ },
+ VgpuManager, //
+};
+
+use super::commands::{
+ query_vgpu_properties,
+ Dbdf,
+ VgpuProperties, //
+};
+
+/// Guest Function ID. GFID 0 is reserved for the PF; VFs start at 1.
+#[repr(transparent)]
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(super) struct Gfid(pub(super) u32);
+
+/// Resource requirements and device identity for one vGPU type.
+#[expect(dead_code)]
+pub(super) struct VgpuType {
+ vgpu_type_id: u32,
+ bar1_length: u64,
+ max_instance: u32,
+ pci_dev_id: u32,
+ pci_subsys_id: u32,
+ fb_length: u64,
+ gsp_heap_size: u64,
+}
+
+impl VgpuType {
+ fn from_properties(properties: &VgpuProperties) -> Self {
+ Self {
+ vgpu_type_id: properties.type_id,
+ bar1_length: properties.bar1_length,
+ max_instance: properties.max_instance,
+ pci_dev_id: properties.dev_id,
+ pci_subsys_id: properties.subsystem_id,
+ fb_length: properties.fb_length,
+ gsp_heap_size: properties.gsp_heap_size,
+ }
+ }
+}
+
+/// A vGPU instance and the resources reserved for it.
+#[expect(dead_code)]
+pub(super) struct VgpuInstance<'gpu> {
+ pub(super) gfid: Gfid,
+ dbdf: Dbdf,
+ vgpu_type: VgpuType,
+ vm_pid: u32,
+ chids: ChannelIdReservation<'gpu>,
+ vram_slot: VgpuVramSlot,
+}
+
+/// Identity and firmware profile used to allocate an instance.
+pub(super) struct InstanceInfo {
+ gfid: Gfid,
+ dbdf: Dbdf,
+ vgpu_type: VgpuType,
+ vm_pid: u32,
+}
+
+#[expect(dead_code)]
+impl InstanceInfo {
+ pub(super) const fn new(gfid: Gfid, dbdf: Dbdf, vgpu_type: VgpuType, vm_pid: u32) -> Self {
+ Self {
+ gfid,
+ dbdf,
+ vgpu_type,
+ vm_pid,
+ }
+ }
+}
+
+/// Registry of live vGPU instances.
+pub(super) struct VgpuInstances<'gpu> {
+ instances: KVec<VgpuInstance<'gpu>>,
+ vram_slots: Option<VgpuVramSlotAllocator>,
+}
+
+#[expect(dead_code)]
+impl<'gpu> VgpuInstances<'gpu> {
+ pub(super) const fn new() -> Self {
+ Self {
+ instances: KVec::new(),
+ vram_slots: None,
+ }
+ }
+
+ fn alloc_vram_slot(&mut self, mm: &GpuMm<'_>, layout: VgpuVramLayout) -> Result<VgpuVramSlot> {
+ let replace_empty_pool = match self.vram_slots.as_ref() {
+ Some(allocator) if allocator.is_empty() => !allocator.matches_layout(layout)?,
+ _ => false,
+ };
+ if replace_empty_pool {
+ self.vram_slots = None;
+ }
+
+ if let Some(allocator) = self.vram_slots.as_mut() {
+ return allocator.alloc(layout);
+ }
+
+ let mut allocator = VgpuVramSlotAllocator::new(mm, layout)?;
+ let slot = allocator.alloc(layout)?;
+ self.vram_slots = Some(allocator);
+ Ok(slot)
+ }
+
+ fn release_vram_slot(&mut self, slot: VgpuVramSlot) -> Result {
+ self.vram_slots.as_mut().ok_or(EIO)?.release(slot);
+ Ok(())
+ }
+
+ /// Allocate resources and register a new inactive vGPU instance.
+ pub(super) fn allocate_instance(
+ &mut self,
+ mm: &GpuMm<'_>,
+ vgpu: &VgpuManager<'gpu>,
+ info: InstanceInfo,
+ ) -> Result<Gfid> {
+ let InstanceInfo {
+ gfid,
+ dbdf,
+ vgpu_type,
+ vm_pid,
+ } = info;
+
+ if self
+ .instances
+ .iter()
+ .any(|instance| instance.gfid == gfid || instance.dbdf == dbdf)
+ {
+ return Err(EEXIST);
+ }
+ let profile_instances = self
+ .instances
+ .iter()
+ .filter(|instance| instance.vgpu_type.vgpu_type_id == vgpu_type.vgpu_type_id)
+ .count();
+ if vgpu_type.max_instance == 0
+ || profile_instances
+ >= usize::try_from(vgpu_type.max_instance).map_err(|_| EOVERFLOW)?
+ {
+ return Err(ENOSPC);
+ }
+ // Reserve registry capacity before acquiring resources so publishing
+ // the completed instance cannot fail due to memory pressure.
+ self.instances.reserve(1, GFP_KERNEL)?;
+
+ let num_chid = vgpu
+ .total_channels()
+ .checked_div(vgpu_type.max_instance)
+ .filter(|count| *count != 0)
+ .ok_or(EINVAL)?;
+ let chids = vgpu.chid_pool.reserve_ids(
+ NonZeroUsize::new(usize::try_from(num_chid).map_err(|_| EOVERFLOW)?).ok_or(EINVAL)?,
+ Alignment::SZ_1,
+ )?;
+ let layout = VgpuVramLayout {
+ type_id: vgpu_type.vgpu_type_id,
+ max_slots: vgpu_type.max_instance,
+ fb_size: vgpu_type.fb_length,
+ heap_size: vgpu_type.gsp_heap_size,
+ fb_align: vgpu.vmmu_segment_size(),
+ };
+ let vram_slot = self.alloc_vram_slot(mm, layout)?;
+
+ let instance = VgpuInstance {
+ gfid,
+ dbdf,
+ vgpu_type,
+ vm_pid,
+ chids,
+ vram_slot,
+ };
+ match self.instances.push_within_capacity(instance) {
+ Ok(()) => Ok(gfid),
+ Err(error) => {
+ let VgpuInstance { vram_slot, .. } = error.0;
+ self.release_vram_slot(vram_slot)?;
+ Err(EIO)
+ }
+ }
+ }
+
+ /// Remove an instance and release its channel and VRAM reservations.
+ pub(super) fn destroy_instance(&mut self, gfid: Gfid) -> Result {
+ let index = self
+ .instances
+ .iter()
+ .position(|instance| instance.gfid == gfid)
+ .ok_or(ENOENT)?;
+ let instance = self.instances.remove(index).map_err(|_| EIO)?;
+ let VgpuInstance { vram_slot, .. } = instance;
+ self.release_vram_slot(vram_slot)
+ }
+}
+
+/// Query and decode one vGPU type using the typed NVKV schema.
+#[expect(dead_code)]
+pub(super) fn query_vgpu_type(cmdq: &Cmdq<'_>, type_id: u32) -> Result<VgpuType> {
+ let properties = query_vgpu_properties(cmdq, type_id)?;
+ Ok(VgpuType::from_properties(&properties))
+}