From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox-ve-rs 2/4] vfio: add rust-native interface for accessing NVIDIA vGPU info
Date: Tue, 20 Jan 2026 14:13:10 +0100 [thread overview]
Message-ID: <20260120131319.949986-3-c.heiss@proxmox.com> (raw)
In-Reply-To: <20260120131319.949986-1-c.heiss@proxmox.com>
Add a "rusty" interface on top of the raw NVML bindings for retrieving
information about creatable vGPU. Will be used to e.g. show a proper
description for each creatable vGPU type.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
.../examples/nv_list_creatable_vgpus.rs | 15 ++
proxmox-ve-vfio/src/nvidia/mod.rs | 123 ++++++++++
proxmox-ve-vfio/src/nvidia/nvml/mod.rs | 224 ++++++++++++++++++
3 files changed, 362 insertions(+)
create mode 100644 proxmox-ve-vfio/examples/nv_list_creatable_vgpus.rs
diff --git a/proxmox-ve-vfio/examples/nv_list_creatable_vgpus.rs b/proxmox-ve-vfio/examples/nv_list_creatable_vgpus.rs
new file mode 100644
index 0000000..b2f276a
--- /dev/null
+++ b/proxmox-ve-vfio/examples/nv_list_creatable_vgpus.rs
@@ -0,0 +1,15 @@
+use std::env;
+
+use proxmox_ve_vfio::nvidia::creatable_vgpu_types_for_dev;
+
+fn main() {
+ let bus_id = env::args()
+ .nth(1)
+ .expect("vGPU bus id expected as first argument, e.g. 00:01.0");
+
+ let types = creatable_vgpu_types_for_dev(&bus_id).expect("failed to retrieve vGPU info");
+
+ for t in types {
+ println!("{}", t.description());
+ }
+}
diff --git a/proxmox-ve-vfio/src/nvidia/mod.rs b/proxmox-ve-vfio/src/nvidia/mod.rs
index 08a414c..bc2ef17 100644
--- a/proxmox-ve-vfio/src/nvidia/mod.rs
+++ b/proxmox-ve-vfio/src/nvidia/mod.rs
@@ -1,3 +1,126 @@
//! Provides access to the state of NVIDIA (v)GPU devices connected to the system.
+use anyhow::Result;
+use serde::Serialize;
+
mod nvml;
+
+use nvml::bindings::{nvmlDevice_t, nvmlVgpuTypeId_t};
+
+/// A single vGPU type that is either supported and/or currently creatable
+/// for a given GPU.
+#[derive(Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct VgpuTypeInfo {
+ /// Unique vGPU type ID.
+ pub id: u32,
+ /// An alphanumeric string that denotes a particular vGPU, e.g. GRID M60-2Q.
+ pub name: String,
+ /// Class of the vGPU, e.g. Quadro.
+ pub class_name: String,
+ /// Maximum number of vGPU instances creatable of this vGPU type.
+ pub max_instances: u32,
+ /// Maximum number of vGPU instances supported per VM for this vGPU type.
+ pub max_instances_per_vm: u32,
+ /// vGPU framebuffer size in bytes.
+ pub framebuffer_size: u64,
+ /// Number of supported display heads by this vGPU type.
+ pub num_heads: u32,
+ /// Maximum resolution of a single head available across all display heads
+ /// supported by this vGPU type.
+ pub max_resolution: (u32, u32),
+ /// License types and versions required to run this specified vGPU type,
+ /// each in the form "\<license name\>,\<version\>", for example
+ /// "GRID-Virtual-PC,2.0".
+ /// A vGPU type might also be runnable with more than one type of license,
+ /// in which cases each license is separated by a semicolon.
+ pub license: String,
+ /// Static frame limit for this vGPU, if the frame limiter is enabled for
+ /// this vGPU type.
+ pub fps_limit: Option<u32>,
+}
+
+impl VgpuTypeInfo {
+ fn get_with(nvml: &nvml::Nvml, dev: nvmlDevice_t, type_id: nvmlVgpuTypeId_t) -> Result<Self> {
+ let num_heads = nvml.vgpu_type_num_display_heads(type_id)?;
+
+ // Take the best resolution among all available display heads
+ let max_resolution = (0..num_heads)
+ .filter_map(|i| nvml.vgpu_type_max_resolution(type_id, i).ok())
+ .max()
+ .unwrap_or((0, 0));
+
+ Ok(VgpuTypeInfo {
+ id: type_id,
+ name: nvml.vgpu_type_name(type_id)?,
+ class_name: nvml.vgpu_type_class_name(type_id)?,
+ max_instances: nvml.vgpu_type_max_instances(dev, type_id)?,
+ max_instances_per_vm: nvml.vgpu_type_max_instances_per_vm(type_id)?,
+ framebuffer_size: nvml.vgpu_type_framebuffer_size(type_id)?,
+ num_heads,
+ max_resolution,
+ license: nvml.vgpu_type_license(type_id)?,
+ fps_limit: nvml.vgpu_type_frame_rate_limit(type_id)?,
+ })
+ }
+
+ /// Formats the descriptive fields of the vGPU type information as a property string.
+ pub fn description(&self) -> String {
+ let VgpuTypeInfo {
+ class_name,
+ max_instances,
+ max_instances_per_vm,
+ framebuffer_size,
+ num_heads,
+ max_resolution,
+ license,
+ ..
+ } = self;
+
+ let framebuffer_size = framebuffer_size / 1024 / 1024;
+ let (max_res_x, max_res_y) = max_resolution;
+
+ format!(
+ "class={class_name}\
+ ,max-instances={max_instances}\
+ ,max-instances-per-vm={max_instances_per_vm}\
+ ,framebuffer-size={framebuffer_size}MiB\
+ ,num-heads={num_heads}\
+ ,max-resolution={max_res_x}x{max_res_y}\
+ ,license={license}"
+ )
+ }
+}
+
+/// Given a concrete GPU device, enumerates all *creatable* vGPU types for this
+/// device.
+fn enumerate_creatable_vgpu_types_by_dev(
+ nvml: &nvml::Nvml,
+ dev: nvmlDevice_t,
+) -> Result<Vec<VgpuTypeInfo>> {
+ let mut vgpu_info = vec![];
+ let type_ids = nvml.device_get_creatable_vgpus(dev)?;
+
+ for type_id in type_ids {
+ vgpu_info.push(VgpuTypeInfo::get_with(nvml, dev, type_id)?);
+ }
+
+ Ok(vgpu_info)
+}
+
+/// Retrieves a list of *creatable* vGPU types for the specified GPU by bus id.
+///
+/// The `bus_id` must be of format "\<domain\>:\<bus\>:\<device\>.\<function\>", e.g.
+/// "0000:01:01.0".
+/// \<domain\> is optional and can be left out if there is only one.
+///
+/// # See also
+///
+/// [`nvmlDeviceGetHandleByPciBusId_v2()`]: <https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html#group__nvmlDeviceQueries_1gea7484bb9eac412c28e8a73842254c05>
+/// [`struct nvmlPciInto_t`]: <https://docs.nvidia.com/deploy/nvml-api/structnvmlPciInfo__t.html#structnvmlPciInfo__t_1a4d54ad9b596d7cab96ecc34613adbe4>
+pub fn creatable_vgpu_types_for_dev(bus_id: &str) -> Result<Vec<VgpuTypeInfo>> {
+ let nvml = nvml::Nvml::new()?;
+ let handle = nvml.device_handle_by_bus_id(bus_id)?;
+
+ enumerate_creatable_vgpu_types_by_dev(&nvml, handle)
+}
diff --git a/proxmox-ve-vfio/src/nvidia/nvml/mod.rs b/proxmox-ve-vfio/src/nvidia/nvml/mod.rs
index 10ad3c9..1259095 100644
--- a/proxmox-ve-vfio/src/nvidia/nvml/mod.rs
+++ b/proxmox-ve-vfio/src/nvidia/nvml/mod.rs
@@ -3,6 +3,13 @@
//!
//! [NVML]: <https://developer.nvidia.com/management-library-nvml>
+use anyhow::{bail, Result};
+use std::{
+ borrow::Cow,
+ ffi::{c_uint, c_ulonglong, CStr},
+ ptr,
+};
+
#[allow(
dead_code,
non_camel_case_types,
@@ -11,3 +18,220 @@
unused_imports
)]
pub mod bindings;
+
+use bindings::{
+ nvmlDevice_t, nvmlReturn_enum_NVML_ERROR_INSUFFICIENT_SIZE,
+ nvmlReturn_enum_NVML_ERROR_NOT_SUPPORTED, nvmlReturn_enum_NVML_SUCCESS, nvmlReturn_t,
+ nvmlVgpuTypeId_t, NvmlLib, NVML_DEVICE_NAME_BUFFER_SIZE, NVML_GRID_LICENSE_BUFFER_SIZE,
+};
+
+/// SONAME/filename of the native NVML, pin it to SOVERSION 1 explicitly to be sure.
+const NVML_LIB_NAME: &str = "libnvidia-ml.so.1";
+
+pub struct Nvml(NvmlLib);
+
+impl Nvml {
+ pub fn new() -> Result<Self> {
+ let lib = unsafe {
+ let lib = Self(NvmlLib::new(NVML_LIB_NAME)?);
+ lib.to_err(lib.0.nvmlInit_v2())?;
+ lib
+ };
+
+ Ok(lib)
+ }
+
+ pub fn device_handle_by_bus_id(&self, bus_id: &str) -> Result<nvmlDevice_t> {
+ let mut handle: nvmlDevice_t = ptr::null_mut();
+ unsafe {
+ self.to_err(
+ self.0
+ .nvmlDeviceGetHandleByPciBusId_v2(bus_id.as_ptr() as *const i8, &mut handle),
+ )?;
+ }
+
+ Ok(handle)
+ }
+
+ /// Retrieves a list of vGPU types supported by the given device.
+ ///
+ /// # See also
+ ///
+ /// <https://docs.nvidia.com/deploy/nvml-api/group__nvmlVgpu.html#group__nvmlVgpu>
+ pub fn device_get_creatable_vgpus(&self, dev: nvmlDevice_t) -> Result<Vec<nvmlVgpuTypeId_t>> {
+ let mut count: c_uint = 0;
+ let mut ids = vec![];
+
+ unsafe {
+ // First retrieve the number of supported vGPUs by passing count == 0,
+ // which will set `count` to the actual number.
+ let result = self
+ .0
+ .nvmlDeviceGetCreatableVgpus(dev, &mut count, ids.as_mut_ptr());
+
+ #[allow(non_upper_case_globals)]
+ if !matches!(
+ result,
+ nvmlReturn_enum_NVML_SUCCESS | nvmlReturn_enum_NVML_ERROR_INSUFFICIENT_SIZE
+ ) {
+ self.to_err(result)?;
+ }
+
+ ids.resize(count as usize, 0);
+ self.to_err(
+ self.0
+ .nvmlDeviceGetCreatableVgpus(dev, &mut count, ids.as_mut_ptr()),
+ )?;
+ }
+
+ Ok(ids)
+ }
+
+ pub fn vgpu_type_class_name(&self, type_id: nvmlVgpuTypeId_t) -> Result<String> {
+ let mut buffer: Vec<u8> = vec![0; NVML_DEVICE_NAME_BUFFER_SIZE as usize];
+ let mut buffer_size = buffer.len() as u32;
+
+ unsafe {
+ self.to_err(self.0.nvmlVgpuTypeGetClass(
+ type_id,
+ buffer.as_mut_ptr() as *mut i8,
+ &mut buffer_size,
+ ))?;
+ }
+
+ slice_to_string(&buffer)
+ }
+
+ pub fn vgpu_type_license(&self, type_id: nvmlVgpuTypeId_t) -> Result<String> {
+ let mut buffer: Vec<u8> = vec![0; NVML_GRID_LICENSE_BUFFER_SIZE as usize];
+
+ unsafe {
+ self.to_err(self.0.nvmlVgpuTypeGetLicense(
+ type_id,
+ buffer.as_mut_ptr() as *mut i8,
+ buffer.len() as u32,
+ ))?;
+ }
+
+ slice_to_string(&buffer)
+ }
+
+ pub fn vgpu_type_name(&self, type_id: nvmlVgpuTypeId_t) -> Result<String> {
+ let mut buffer: Vec<u8> = vec![0; NVML_DEVICE_NAME_BUFFER_SIZE as usize];
+ let mut buffer_size = buffer.len() as u32;
+
+ unsafe {
+ self.to_err(self.0.nvmlVgpuTypeGetName(
+ type_id,
+ buffer.as_mut_ptr() as *mut i8,
+ &mut buffer_size,
+ ))?;
+ }
+
+ slice_to_string(&buffer)
+ }
+
+ pub fn vgpu_type_max_instances(
+ &self,
+ dev: nvmlDevice_t,
+ type_id: nvmlVgpuTypeId_t,
+ ) -> Result<u32> {
+ let mut count: c_uint = 0;
+ unsafe {
+ self.to_err(self.0.nvmlVgpuTypeGetMaxInstances(dev, type_id, &mut count))?;
+ }
+
+ Ok(count)
+ }
+
+ pub fn vgpu_type_max_instances_per_vm(&self, type_id: nvmlVgpuTypeId_t) -> Result<u32> {
+ let mut count: c_uint = 0;
+ unsafe {
+ self.to_err(self.0.nvmlVgpuTypeGetMaxInstancesPerVm(type_id, &mut count))?;
+ }
+
+ Ok(count)
+ }
+
+ pub fn vgpu_type_framebuffer_size(&self, type_id: nvmlVgpuTypeId_t) -> Result<u64> {
+ let mut size: c_ulonglong = 0;
+ unsafe {
+ self.to_err(self.0.nvmlVgpuTypeGetFramebufferSize(type_id, &mut size))?;
+ }
+
+ Ok(size)
+ }
+
+ pub fn vgpu_type_num_display_heads(&self, type_id: nvmlVgpuTypeId_t) -> Result<u32> {
+ let mut num: c_uint = 0;
+ unsafe {
+ self.to_err(self.0.nvmlVgpuTypeGetNumDisplayHeads(type_id, &mut num))?;
+ }
+
+ Ok(num)
+ }
+
+ pub fn vgpu_type_max_resolution(
+ &self,
+ type_id: nvmlVgpuTypeId_t,
+ head: u32,
+ ) -> Result<(u32, u32)> {
+ let (mut x, mut y): (c_uint, c_uint) = (0, 0);
+ unsafe {
+ self.to_err(
+ self.0
+ .nvmlVgpuTypeGetResolution(type_id, head, &mut x, &mut y),
+ )?;
+ }
+
+ Ok((x, y))
+ }
+
+ pub fn vgpu_type_frame_rate_limit(&self, type_id: nvmlVgpuTypeId_t) -> Result<Option<u32>> {
+ let mut limit: c_uint = 0;
+ let result = unsafe { self.0.nvmlVgpuTypeGetFrameRateLimit(type_id, &mut limit) };
+
+ if !Self::err_is_unsupported(result) {
+ Ok(None)
+ } else {
+ self.to_err(result)?;
+ Ok(Some(limit))
+ }
+ }
+
+ fn to_err(&self, result: nvmlReturn_t) -> Result<()> {
+ if result == nvmlReturn_enum_NVML_SUCCESS {
+ Ok(())
+ } else {
+ bail!("{}", self.error_str(result))
+ }
+ }
+
+ fn err_is_unsupported(result: nvmlReturn_t) -> bool {
+ result == nvmlReturn_enum_NVML_ERROR_NOT_SUPPORTED
+ }
+
+ fn error_str(&self, err_code: nvmlReturn_t) -> Cow<'_, str> {
+ let cstr = unsafe {
+ let raw = self.0.nvmlErrorString(err_code);
+ CStr::from_ptr(raw)
+ };
+
+ cstr.to_string_lossy()
+ }
+}
+
+impl Drop for Nvml {
+ fn drop(&mut self) {
+ if let Ok(sym) = self.0.nvmlShutdown.as_ref() {
+ // Although nvmlShutdown() provides a return code (or error) indicating
+ // whether the operation was successful, at this point there isn't
+ // really anything we can do if it throws an error.
+ unsafe { sym() };
+ }
+ }
+}
+
+fn slice_to_string(s: &[u8]) -> Result<String> {
+ Ok(CStr::from_bytes_until_nul(s)?.to_str()?.into())
+}
--
2.52.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next prev parent reply other threads:[~2026-01-20 13:13 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-01-20 13:13 [pve-devel] [PATCH proxmox-{ve, perl}-rs/common 0/4] use native libnvidia-ml library for " Christoph Heiss
2026-01-20 13:13 ` [pve-devel] [PATCH proxmox-ve-rs 1/4] vfio: add crate for interacting with vfio host devices Christoph Heiss
2026-01-20 13:13 ` Christoph Heiss [this message]
2026-01-20 13:13 ` [pve-devel] [PATCH proxmox-perl-rs 3/4] pve: add bindings for proxmox-ve-vfio Christoph Heiss
2026-01-20 13:13 ` [pve-devel] [PATCH common 4/4] sysfs: use new PVE::RS::VFIO::Nvidia module to retrieve vGPU info Christoph Heiss
2026-01-20 15:00 ` Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260120131319.949986-3-c.heiss@proxmox.com \
--to=c.heiss@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox