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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.