* [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring
@ 2026-09-08 13:15 Li Wang
2026-09-09 6:10 ` Greg Kroah-Hartman
2026-10-27 9:08 ` [RFC PATCH v2 0/1] misc: fgds: introduce GPU-File direct I/O via dma-buf Li Wang
0 siblings, 2 replies; 14+ messages in thread
From: Li Wang @ 2026-09-08 13:15 UTC (permalink / raw)
To: Arnd Bergmann, Greg Kroah-Hartman
Cc: Sumit Semwal, Christian König, linux-media, dri-devel,
linaro-mm-sig, linux-kernel, Mengmeng Zhao, Li Wang
From: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
Inspired by the paper published in SC'25 [1], we implemented a character
device named fgds that provides two ioctl interfaces:
`REG_BUFFER/UNREG_BUFFER`. It enables applications to perform direct I/O
between GPU memory and NVMe via POSIX and io_uring APIs. This is
particularly useful for LLM workloads, such as model loading, KV cache
offloading, and checkpointing. The fgds device corresponds one-to-one with
the PCIe GPU on the machine. The usage is straightforward: an application
simply opens the corresponding fgds device, calls ioctl on the returned fd
with REG_BUFFER, taking the target GPU memory buffer address (represented
as a dma-buf fd), and the buffer length as inputs, and then invokes mmap on
the fgds device fd, using the return value of ioctl as the input. The mmap
call returns a CPU virtual address (call it cpu_vaddr). Afterward,
cpu_vaddr can be passed directly to pread/pwrite, or
io_uring_prep_read/io_uring_prep_write to perform direct I/O between files
on NVMe and GPU memory. A minimal working example can be found in [2].
The underlying mechanism is that, with the support of fgds device,
cpu_vaddr is made to point directly to the GPU memory buffer corresponding
to the dma-buf fd. This solution is loosely coupled with the GPU vendor's
driver; the GPU vendor only needs to support exporting the allocated GPU
memory buffer through the standard Linux kernel dma-buf framework, which
the vast majority of mainstream GPUs already support. This allows both
applications and the fgds device to work seamlessly with GPUs from
different vendors without any modifications. Furthermore, applications no
longer need to call vendor-specific proprietary APIs (such as NVIDIA's
cuFile API) or install vendor-specific kernel modules (such as NVIDIA's
nvidia-fs.ko) for different GPU vendors. We have tested fgds on GPU cards
from NVIDIA, AMD, and several other vendors, and it works well.
Besides the benefits in ease of use and compatibility, another key
advantage of this solution is higher performance. [2] presents the
performance comparison results between fgds and NVIDIA GDS. Because fgds
eliminates the overhead of phony buffers incurred by NVIDIA GDS, it
achieves significantly higher performance. For example, for reads, fgds
outperforms GDS by 11% to 109%; for writes, fgds outperforms GDS by 10%
to 71%.
To further accelerate the read and write operations of large files or
massive data volumes—which are very common in LLM scenarios—we have
implemented library functions `fgds_read` and `fgds_write`. Under the hood,
these interfaces split large data into chunks and submit them
asynchronously and in parallel via io_uring, thereby further boosting I/O
performance, with read performance improved by up to 115% and write
performance by up to 40%. In addition, we also provide the `fgds_register`
library interface to encapsulate the `open`, `ioctl' and `mmap` operations.
Readers who are interested can refer to [2].
In addition, we have added the LMCache backend, enabling vLLM to offload KV
cache via LMCache using fgds, which accelerates inference performance. We
also added PyTorch APIs, compatible with the PyTorch GDS API, to improve
the performance of reading and writing checkpoints during LLM training.
We look forward to community feedback and are fully committed to iterating
on this series to work towards upstreaming.
[1] https://dl.acm.org/doi/10.1145/3712285.3759862
[2] https://github.com/Storage-and-OS-for-AI/fgds
Signed-off-by: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
Signed-off-by: Li Wang <liwang@kylinos.cn>
---
drivers/misc/Kconfig | 9 +
drivers/misc/Makefile | 1 +
drivers/misc/fgds.c | 989 ++++++++++++++++++++++++++++++++++++++
include/uapi/linux/fgds.h | 54 +++
4 files changed, 1053 insertions(+)
create mode 100644 drivers/misc/fgds.c
create mode 100644 include/uapi/linux/fgds.h
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 7364931dad3a..2f3a5a5fd0bf 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -568,6 +568,15 @@ config MCHP_LAN966X_PCI
- lan966x-miim (MDIO_MSCC_MIIM)
- lan966x-switch (LAN966X_SWITCH)
+config FGDS
+ tristate "GPU-NVMe direct I/O control driver"
+ depends on PCI && DMA_SHARED_BUFFER && ZONE_DEVICE
+ help
+ Say Y here if you want to support GPU-NVME direct I/O
+ via POSIX/io_uring interfaces.
+
+ If unsure, say N.
+
source "drivers/misc/c2port/Kconfig"
source "drivers/misc/eeprom/Kconfig"
source "drivers/misc/cb710/Kconfig"
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index e8d8d5d88c0d..04985abe1678 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -71,3 +71,4 @@ obj-y += keba/
obj-y += amd-sbi/
obj-$(CONFIG_MISC_RP1) += rp1/
obj-$(CONFIG_INTEL_SSEI) += issei/
+obj-$(CONFIG_FGDS) += fgds.o
diff --git a/drivers/misc/fgds.c b/drivers/misc/fgds.c
new file mode 100644
index 000000000000..3aa4945f701b
--- /dev/null
+++ b/drivers/misc/fgds.c
@@ -0,0 +1,989 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Fast GPU Direct Storage via dma-buf.
+ *
+ * Copyright (C) 2026 KylinSoft. Co., Ltd. All rights reserved.
+ *
+ * Maps GPU memory into user space to enable direct NVME-to-GPU DMA
+ * pread/pwrite syscalls. BAR pages are remapped into ZONE_DEVICE via
+ * devm_memremap_pages() and populated using dma-buf backing pages.
+ */
+#define pr_fmt(fmt) "fgds: " fmt
+
+#include <linux/cdev.h>
+#include <linux/device.h>
+#include <linux/dma-buf.h>
+#include <linux/dma-mapping.h>
+#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/idr.h>
+#include <linux/iommu.h>
+#include <linux/kernel.h>
+#include <linux/kref.h>
+#include <linux/list.h>
+#include <linux/memremap.h>
+#include <linux/mm.h>
+#include <linux/module.h>
+#include <linux/overflow.h>
+#include <linux/pci.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/uaccess.h>
+#include <linux/xarray.h>
+
+#include <linux/fgds.h>
+
+/*
+ * Upper bound for chrdev minor allocation and IDA range
+ */
+#define FGDS_MAX_MINORS 512
+#define FGDS_MIN_GPU_BAR_SIZE (64UL * 1024 * 1024)
+
+struct fgds_dev {
+ struct list_head node; /* Entry in fgds_dev_list */
+ struct pci_dev *pdev; /* Underlying GPU PCI device */
+ int idx; /* Minor number from fgds_ida */
+ phys_addr_t bar_paddr; /* GPU PCIe BAR physical base address */
+ resource_size_t bar_size; /* GPU PCIe BAR size */
+ void __iomem *pci_mem_va; /* Remapped kernel virtual address */
+ struct dev_pagemap *pgmap; /* ZONE_DEVICE page map for the BAR */
+ struct device device;
+ struct cdev cdev;
+};
+
+/*
+ * Contiguous GPU memory extent mapped from dma-buf scatterlist
+ */
+struct fgds_extent {
+ u64 vma_offset; /* Start offset within VMA (bytes) */
+ dma_addr_t dma_addr; /* Bus address within BAR window */
+ u64 len; /* Extent length(bytes) */
+};
+
+/*
+ * Registered dma-buf range. Reference counted by per-file registry and
+ * each active VMA mapped from it.
+ */
+struct fgds_buffer {
+ struct kref ref;
+ struct fgds_dev *fdev;
+ u64 idx; /* Token passed to mmap(2) */
+ u64 size; /* Range size(page aligned) */
+ u64 dmabuf_offset; /* Offset inside the dma-buf */
+ struct dma_buf *dbuf;
+ struct dma_buf_attachment *attach;
+ struct sg_table *sgt;
+ struct fgds_extent *extents; /* Merged physical extents */
+ u32 nr_extents;
+ bool invalidated; /* Buffer moved by exporter */
+};
+
+/* Per-open session state. */
+struct fgds_file_ctx {
+ struct fgds_dev *fdev;
+ struct mutex lock; /* Protects @buffers */
+ struct xarray buffers; /* idx -> struct fgds_buffer */
+};
+
+static dev_t fgds_chr_devt;
+static struct class *fgds_chr_class;
+
+static DEFINE_IDA(fgds_ida);
+static LIST_HEAD(fgds_dev_list);
+static u32 fgds_dev_count;
+
+struct fgds_bdf_entry {
+ u16 domain;
+ u8 bus;
+ u8 slot;
+ u8 func;
+ struct list_head list;
+};
+
+static LIST_HEAD(fgds_whitelist);
+static char *devices = "all";
+
+module_param(devices, charp, 0444);
+MODULE_PARM_DESC(devices, "all | comma-separated BDF whitelist (e.g. 0000:1e:00.0,0000:1f:00.0)");
+
+static int __init fgds_parse_bdf_list(const char *str, struct list_head *head)
+{
+ char *dup, *token, *cur;
+
+ if (!str || !strcmp(str, "all") || !strlen(str))
+ return 0;
+
+ dup = kstrdup(str, GFP_KERNEL);
+ if (!dup)
+ return -ENOMEM;
+
+ cur = dup;
+ while ((token = strsep(&cur, ",")) != NULL) {
+ unsigned int dom, b, s, f;
+ struct fgds_bdf_entry *entry;
+
+ if (sscanf(token, "%x:%x:%x.%x", &dom, &b, &s, &f) != 4)
+ continue;
+
+ entry = kzalloc_obj(*entry, GFP_KERNEL);
+ if (!entry) {
+ kfree(dup);
+ return -ENOMEM;
+ }
+ entry->domain = dom;
+ entry->bus = b;
+ entry->slot = s;
+ entry->func = f;
+ list_add_tail(&entry->list, head);
+ }
+ kfree(dup);
+ return 0;
+}
+
+static bool fgds_match_bdf(struct pci_dev *pdev, struct list_head *head)
+{
+ struct fgds_bdf_entry *entry;
+ u16 dom = pci_domain_nr(pdev->bus);
+ u8 b = pdev->bus->number;
+ u8 s = PCI_SLOT(pdev->devfn);
+ u8 f = PCI_FUNC(pdev->devfn);
+
+ list_for_each_entry(entry, head, list) {
+ if (entry->domain == dom && entry->bus == b &&
+ entry->slot == s && entry->func == f)
+ return true;
+ }
+ return false;
+}
+
+static bool fgds_should_bind(struct pci_dev *pdev)
+{
+ if (!devices || !strcmp(devices, "all"))
+ return true;
+ return fgds_match_bdf(pdev, &fgds_whitelist);
+}
+
+/*
+ * BAR-based mapping requires device physical addresses. When using
+ * IOMMU, DMA addresses are IOVAs, which cannot be mapped directly.
+ */
+static int fgds_check_gpu_iommu(struct pci_dev *pdev)
+{
+ struct iommu_domain *domain;
+
+ domain = iommu_get_domain_for_dev(&pdev->dev);
+ if (domain && domain->type != IOMMU_DOMAIN_IDENTITY) {
+ pr_warn("%s: reject attaching a translating IOMMU domain (requires iommu=pt or off\n",
+ dev_name(&pdev->dev));
+ return -EPERM;
+ }
+ return 0;
+}
+
+/* Prefetchable memory BARs are the only ones that can back GPU memory. */
+static bool fgds_bar_is_prefetch_mem(struct pci_dev *pdev, int bar)
+{
+ unsigned long flags = pci_resource_flags(pdev, bar);
+
+ return (flags & (IORESOURCE_MEM | IORESOURCE_PREFETCH)) ==
+ (IORESOURCE_MEM | IORESOURCE_PREFETCH);
+}
+
+/*
+ * Check if the device exposes a large prefetchable memory BAR
+ */
+static bool fgds_has_large_memory_bar(struct pci_dev *pdev)
+{
+ int i;
+
+ for (i = 0; i < PCI_STD_NUM_BARS; i++) {
+ if (!fgds_bar_is_prefetch_mem(pdev, i))
+ continue;
+ if (pci_resource_len(pdev, i) >= FGDS_MIN_GPU_BAR_SIZE)
+ return true;
+ }
+ return false;
+}
+
+/* Map the GPU PCIe BAR into ZONE_DEVICE kernel virtual memory. */
+static int fgds_devm_memremap(struct fgds_dev *gdev)
+{
+ struct dev_pagemap *pgmap;
+ int ret;
+ void *addr;
+
+ gdev->pgmap = devm_kzalloc(&gdev->pdev->dev, sizeof(struct dev_pagemap),
+ GFP_KERNEL);
+ if (!gdev->pgmap)
+ return -ENOMEM;
+
+ pgmap = gdev->pgmap;
+ pgmap->range.start = gdev->bar_paddr;
+ pgmap->range.end = gdev->bar_paddr + gdev->bar_size - 1;
+ pgmap->nr_range = 1;
+ pgmap->type = MEMORY_DEVICE_GENERIC;
+
+ addr = devm_memremap_pages(&gdev->pdev->dev, pgmap);
+ if (IS_ERR(addr)) {
+ ret = PTR_ERR(addr);
+ pr_err("%s: cannot map BAR [%#llx, +0x%llx] as device memory (%d)\n",
+ dev_name(&gdev->pdev->dev),
+ (u64)gdev->bar_paddr, (u64)gdev->bar_size, ret);
+ devm_kfree(&gdev->pdev->dev, gdev->pgmap);
+ gdev->pgmap = NULL;
+ return ret;
+ }
+
+ gdev->pci_mem_va = addr;
+
+ pr_info("%s: BAR [%#llx, %#llx] remapped to kernel VA %#lx\n",
+ dev_name(&gdev->pdev->dev), (u64)gdev->bar_paddr,
+ (u64)(gdev->bar_paddr + gdev->bar_size - 1),
+ (uintptr_t)gdev->pci_mem_va);
+ return 0;
+}
+
+/*
+ * Dynamic dma-buf attachment callbacks. Peer2peer ability hints exporters
+ * to retain buffers in device VRAM. Buffer movement invalidates existing
+ * VMA mappings and requires user-space re-registration.
+ */
+static void fgds_invalidate_mappings(struct dma_buf_attachment *attach)
+{
+ struct fgds_buffer *buf = attach->importer_priv;
+
+ pr_warn_ratelimited("dma-buf moved while mapped; re-register required\n");
+ if (buf)
+ WRITE_ONCE(buf->invalidated, true);
+}
+
+static const struct dma_buf_attach_ops fgds_attach_ops = {
+ .allow_peer2peer = true,
+ .invalidate_mappings = fgds_invalidate_mappings,
+};
+
+static void fgds_buffer_free(struct kref *kref)
+{
+ struct fgds_buffer *buf = container_of(kref, struct fgds_buffer, ref);
+
+ if (buf->sgt)
+ dma_buf_unmap_attachment(buf->attach, buf->sgt,
+ DMA_BIDIRECTIONAL);
+ if (buf->attach)
+ dma_buf_detach(buf->dbuf, buf->attach);
+ if (buf->dbuf)
+ dma_buf_put(buf->dbuf);
+
+ kvfree(buf->extents);
+ kfree(buf);
+ module_put(THIS_MODULE);
+}
+
+static inline void fgds_buffer_get(struct fgds_buffer *buf)
+{
+ kref_get(&buf->ref);
+}
+
+static inline void fgds_buffer_put(struct fgds_buffer *buf)
+{
+ kref_put(&buf->ref, fgds_buffer_free);
+}
+
+/*
+ * Collect scatterlist segments into a coalesced extent array within the
+ * BAR
+ */
+static int fgds_fill_extents(struct fgds_buffer *buf)
+{
+ struct fgds_dev *gdev = buf->fdev;
+ struct scatterlist *sg;
+ dma_addr_t addr, seg_end;
+ unsigned int len;
+ u64 cur_vma_offset = 0;
+ u64 skip = buf->dmabuf_offset;
+ int i, idx = 0;
+
+ buf->extents = kvmalloc_array(buf->sgt->nents, sizeof(*buf->extents),
+ GFP_KERNEL | __GFP_NOWARN);
+ if (!buf->extents)
+ return -ENOMEM;
+
+ pr_info_ratelimited("exporter %s attached to %s, dbuf size 0x%zx, offset 0x%llx, nents %u\n",
+ buf->dbuf->exp_name, dev_name(buf->attach->dev),
+ buf->dbuf->size, buf->dmabuf_offset, buf->sgt->nents);
+
+ for_each_sgtable_dma_sg(buf->sgt, sg, i) {
+ addr = sg_dma_address(sg);
+ len = sg_dma_len(sg);
+ if (!len)
+ len = sg->length;
+
+ if (!addr) {
+ pr_err("invalid dma address at sg[%d]\n", i);
+ goto err_free;
+ }
+
+ /* Verify the segment falls within the BAR window
+ */
+ if (check_add_overflow(addr, (dma_addr_t)len, &seg_end) ||
+ addr < gdev->bar_paddr ||
+ seg_end > gdev->bar_paddr + gdev->bar_size) {
+ pr_err("segment sg[%d] [0x%llx, +0x%x] outside %s BAR window [0x%llx, 0x%llx]\n",
+ i, (u64)addr, len,
+ dev_name(&gdev->pdev->dev),
+ (u64)gdev->bar_paddr,
+ (u64)(gdev->bar_paddr + gdev->bar_size - 1));
+ goto err_free;
+ }
+
+ if ((addr & (PAGE_SIZE - 1)) || (len & (PAGE_SIZE - 1))) {
+ pr_err("unaligned sg[%d] addr 0x%llx len %u\n",
+ i, (u64)addr, len);
+ goto err_free;
+ }
+ if (skip) {
+ if (skip >= len) {
+ skip -= len;
+ continue;
+ }
+ addr += skip;
+ len -= (unsigned int)skip;
+ skip = 0;
+ }
+ if (cur_vma_offset + len > buf->size)
+ len = (unsigned int)(buf->size - cur_vma_offset);
+
+ /*
+ * Merge segments which is contiguous in both bus address and
+ * VMA offset
+ */
+ if (idx > 0 &&
+ (buf->extents[idx - 1].dma_addr + buf->extents[idx - 1].len == addr) &&
+ (buf->extents[idx - 1].vma_offset + buf->extents[idx - 1].len ==
+ cur_vma_offset)) {
+ buf->extents[idx - 1].len += len;
+ cur_vma_offset += len;
+ if (cur_vma_offset >= buf->size)
+ break;
+ continue;
+ }
+
+ buf->extents[idx].vma_offset = cur_vma_offset;
+ buf->extents[idx].dma_addr = addr;
+ buf->extents[idx].len = len;
+ cur_vma_offset += len;
+ idx++;
+ if (cur_vma_offset >= buf->size)
+ break;
+ }
+
+ if (skip || cur_vma_offset < buf->size) {
+ pr_err("coverage mismatch (actual=0x%llx, req=0x%llx)\n",
+ cur_vma_offset, buf->size);
+ goto err_free;
+ }
+
+ pr_info_ratelimited("%u extents, BAR window [0x%llx, 0x%llx], first 0x%llx last 0x%llx\n",
+ idx, (u64)gdev->bar_paddr,
+ (u64)(gdev->bar_paddr + gdev->bar_size - 1),
+ (u64)buf->extents[0].dma_addr,
+ (u64)(buf->extents[idx - 1].dma_addr +
+ buf->extents[idx - 1].len - 1));
+
+ /* Shrink extent array to the actual merged count */
+ if (idx > 0 && idx < buf->sgt->nents) {
+ struct fgds_extent *e;
+
+ e = kvmalloc_array(idx, sizeof(*e), GFP_KERNEL | __GFP_NOWARN);
+ if (e) {
+ memcpy(e, buf->extents, idx * sizeof(*e));
+ kvfree(buf->extents);
+ buf->extents = e;
+ }
+ }
+
+ buf->nr_extents = idx;
+ return 0;
+
+err_free:
+ kvfree(buf->extents);
+ buf->extents = NULL;
+ return -EIO;
+}
+
+static int fgds_buffer_import_dmabuf(struct fgds_buffer *buf, s32 dmabuf_fd)
+{
+ struct dma_buf *dbuf;
+ int ret;
+ u64 end;
+
+ dbuf = dma_buf_get(dmabuf_fd);
+ if (IS_ERR(dbuf))
+ return PTR_ERR(dbuf);
+
+ if (check_add_overflow(buf->dmabuf_offset, buf->size, &end) ||
+ end > dbuf->size) {
+ pr_err("request [0x%llx, +0x%llx) exceeds buffer size 0x%zx\n",
+ buf->dmabuf_offset, buf->size, dbuf->size);
+ dma_buf_put(dbuf);
+ return -EINVAL;
+ }
+ buf->dbuf = dbuf;
+
+ /* Attach using GPU PCI device to satisfy dma_mask requirements */
+ buf->attach = dma_buf_dynamic_attach(dbuf, &buf->fdev->pdev->dev,
+ &fgds_attach_ops, buf);
+ if (IS_ERR(buf->attach)) {
+ ret = PTR_ERR(buf->attach);
+ buf->attach = NULL;
+ goto err_put_dbuf;
+ }
+
+ buf->sgt = dma_buf_map_attachment(buf->attach, DMA_BIDIRECTIONAL);
+ if (IS_ERR(buf->sgt)) {
+ ret = PTR_ERR(buf->sgt);
+ buf->sgt = NULL;
+ goto err_detach;
+ }
+
+ ret = fgds_fill_extents(buf);
+ if (ret)
+ goto err_unmap;
+
+ return 0;
+
+err_unmap:
+ dma_buf_unmap_attachment(buf->attach, buf->sgt, DMA_BIDIRECTIONAL);
+ buf->sgt = NULL;
+err_detach:
+ dma_buf_detach(buf->dbuf, buf->attach);
+ buf->attach = NULL;
+err_put_dbuf:
+ dma_buf_put(buf->dbuf);
+ buf->dbuf = NULL;
+ return ret;
+}
+
+static void fgds_vma_open(struct vm_area_struct *vma)
+{
+ struct fgds_buffer *buf = vma->vm_private_data;
+
+ if (buf)
+ fgds_buffer_get(buf);
+}
+
+static void fgds_vma_close(struct vm_area_struct *vma)
+{
+ struct fgds_buffer *buf = vma->vm_private_data;
+
+ if (buf)
+ fgds_buffer_put(buf);
+}
+
+/* Binary search extents by VMA byte offset */
+static struct fgds_extent *fgds_lookup_extent(struct fgds_buffer *buf,
+ u64 offset)
+{
+ struct fgds_extent *ext;
+ int low, high;
+
+ if (unlikely(!buf->nr_extents))
+ return NULL;
+
+ if (likely(buf->nr_extents == 1)) {
+ ext = &buf->extents[0];
+ if (offset < ext->vma_offset + ext->len)
+ return ext;
+ return NULL;
+ }
+
+ low = 0;
+ high = buf->nr_extents - 1;
+ while (low <= high) {
+ int mid = low + (high - low) / 2;
+
+ ext = &buf->extents[mid];
+
+ if (offset < ext->vma_offset)
+ high = mid - 1;
+ else if (offset >= ext->vma_offset + ext->len)
+ low = mid + 1;
+ else
+ return ext;
+ }
+ return NULL;
+}
+
+static vm_fault_t fgds_vma_fault(struct vm_fault *vmf)
+{
+ struct vm_area_struct *vma = vmf->vma;
+ struct fgds_buffer *buf = vma->vm_private_data;
+ u64 offset;
+ struct fgds_extent *ext;
+ phys_addr_t phys;
+ unsigned long pfn;
+ struct page *page;
+
+ if (!buf || unlikely(READ_ONCE(buf->invalidated)))
+ return VM_FAULT_SIGBUS;
+
+ offset = vmf->address - vma->vm_start;
+ if (offset >= buf->size)
+ return VM_FAULT_SIGBUS;
+
+ ext = fgds_lookup_extent(buf, offset);
+ if (!ext)
+ return VM_FAULT_SIGBUS;
+
+ phys = ext->dma_addr + (offset - ext->vma_offset);
+ pfn = phys >> PAGE_SHIFT;
+
+ if (unlikely(!pfn_valid(pfn)))
+ return VM_FAULT_SIGBUS;
+ page = pfn_to_page(pfn);
+ if (unlikely(!is_zone_device_page(page)))
+ return VM_FAULT_SIGBUS;
+
+ return vmf_insert_page(vma, vmf->address, page);
+}
+
+/*
+ * .page_mkwrite is omitted: vmf_insert_page() sets writable PTEs directly
+ * from vma->vm_page_prot for MAP_SHARED mappings, avoiding do_wp_page().
+ */
+static const struct vm_operations_struct fgds_vm_ops = {
+ .open = fgds_vma_open,
+ .close = fgds_vma_close,
+ .fault = fgds_vma_fault,
+};
+
+static int fgds_mmap(struct file *filp, struct vm_area_struct *vma)
+{
+ struct fgds_file_ctx *ctx = filp->private_data;
+ struct fgds_buffer *buf;
+ int ret;
+
+ mutex_lock(&ctx->lock);
+ buf = xa_load(&ctx->buffers, vma->vm_pgoff);
+ if (buf)
+ fgds_buffer_get(buf);
+ mutex_unlock(&ctx->lock);
+
+ if (!buf)
+ return -EINVAL;
+
+ if (READ_ONCE(buf->invalidated)) {
+ pr_err("mmap failed: buffer invalidated by GPU driver\n");
+ ret = -EIO;
+ goto err_put;
+ }
+ if (vma->vm_end - vma->vm_start != buf->size) {
+ ret = -EINVAL;
+ goto err_put;
+ }
+
+ vm_flags_clear(vma, VM_PFNMAP | VM_IO);
+ vm_flags_set(vma, VM_MIXEDMAP | VM_DONTEXPAND | VM_DONTDUMP |
+ current->mm->def_flags);
+ vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
+ vma->vm_ops = &fgds_vm_ops;
+ vma->vm_private_data = buf;
+
+ return 0;
+err_put:
+ fgds_buffer_put(buf);
+ return ret;
+}
+
+static int fgds_ioctl_reg_buffer(struct fgds_file_ctx *ctx,
+ struct fgds_ioctl_reg_buffer __user *argp)
+{
+ struct fgds_ioctl_reg_buffer arg;
+ struct fgds_buffer *buf = NULL;
+ u32 id;
+ int ret;
+ u64 end;
+
+ if (copy_from_user(&arg, argp, sizeof(arg))) {
+ ret = -EFAULT;
+ goto err;
+ }
+
+ if (arg.flags || arg.dmabuf_fd < 0 ||
+ (arg.dmabuf_offset & (PAGE_SIZE - 1)) ||
+ !arg.size || (arg.size & (PAGE_SIZE - 1))) {
+ ret = -EINVAL;
+ goto err;
+ }
+
+ if (check_add_overflow(arg.dmabuf_offset, arg.size, &end)) {
+ ret = -EOVERFLOW;
+ goto err;
+ }
+
+ if (arg.size > ctx->fdev->bar_size) {
+ pr_debug("%s: size 0x%llx exceeds BAR window 0x%llx\n",
+ dev_name(&ctx->fdev->pdev->dev), (u64)arg.size,
+ (u64)ctx->fdev->bar_size);
+ ret = -EINVAL;
+ goto err;
+ }
+
+ buf = kzalloc(sizeof(*buf), GFP_KERNEL);
+ if (!buf) {
+ ret = -ENOMEM;
+ goto err;
+ }
+
+ kref_init(&buf->ref);
+ /*
+ * Pin the module for as long as the buffer exists: a VMA outlives
+ * the file descriptor it was mapped from, so after close(2) nothing
+ * keeps the module alive while .fault and .close still run from it,
+ * and module exit would free the fgds_dev backing those pages.
+ *
+ * __module_get() rather than try_module_get() because the get
+ * cannot fail here: the caller's open file already holds a module
+ * reference, so this ioctl cannot overlap module removal.
+ */
+ __module_get(THIS_MODULE);
+ buf->fdev = ctx->fdev;
+ buf->size = arg.size;
+ buf->dmabuf_offset = arg.dmabuf_offset;
+
+ ret = fgds_buffer_import_dmabuf(buf, arg.dmabuf_fd);
+ if (ret)
+ goto err_put;
+
+ mutex_lock(&ctx->lock);
+ ret = xa_alloc(&ctx->buffers, &id, buf, xa_limit_32b, GFP_KERNEL);
+ if (!ret)
+ buf->idx = (u64)id << PAGE_SHIFT;
+ mutex_unlock(&ctx->lock);
+ if (ret)
+ goto err_put;
+
+ arg.idx = buf->idx;
+ if (copy_to_user(argp, &arg, sizeof(arg))) {
+ struct fgds_buffer *erased;
+ u32 id = buf->idx >> PAGE_SHIFT;
+
+ /*
+ * Drops reference only if this thread successfully erases
+ * the entry.
+ */
+ mutex_lock(&ctx->lock);
+ erased = xa_erase(&ctx->buffers, id);
+ mutex_unlock(&ctx->lock);
+ if (erased)
+ fgds_buffer_put(buf);
+ ret = -EFAULT;
+ goto err;
+ }
+
+ pr_debug("reg: %s dmabuf %d offset 0x%llx size 0x%llx -> token 0x%llx\n",
+ dev_name(&buf->fdev->pdev->dev), arg.dmabuf_fd,
+ buf->dmabuf_offset, buf->size, buf->idx);
+ ret = 0;
+
+err:
+ return ret;
+
+err_put:
+ fgds_buffer_put(buf);
+ return ret;
+}
+
+static int fgds_ioctl_unreg_buffer(struct fgds_file_ctx *ctx,
+ struct fgds_ioctl_unreg_buffer __user *argp)
+{
+ struct fgds_ioctl_unreg_buffer arg;
+ struct fgds_buffer *buf;
+ u32 id;
+
+ if (copy_from_user(&arg, argp, sizeof(arg)))
+ return -EFAULT;
+
+ if (arg.idx & (PAGE_SIZE - 1))
+ return -EINVAL;
+
+ id = arg.idx >> PAGE_SHIFT;
+
+ mutex_lock(&ctx->lock);
+ buf = xa_erase(&ctx->buffers, id);
+ mutex_unlock(&ctx->lock);
+
+ if (!buf)
+ return -ENOENT;
+
+ fgds_buffer_put(buf);
+ return 0;
+}
+
+static long fgds_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
+{
+ struct fgds_file_ctx *ctx = filp->private_data;
+ void __user *argp = (void __user *)arg;
+
+ switch (cmd) {
+ case FGDS_IOCTL_REG_BUFFER:
+ return fgds_ioctl_reg_buffer(ctx, argp);
+ case FGDS_IOCTL_UNREG_BUFFER:
+ return fgds_ioctl_unreg_buffer(ctx, argp);
+ default:
+ return -ENOTTY;
+ }
+}
+
+static int fgds_open(struct inode *inode, struct file *filp)
+{
+ struct fgds_dev *gdev = container_of(inode->i_cdev,
+ struct fgds_dev, cdev);
+ struct fgds_file_ctx *ctx;
+
+ ctx = kzalloc_obj(*ctx, GFP_KERNEL);
+ if (!ctx)
+ return -ENOMEM;
+
+ ctx->fdev = gdev;
+ mutex_init(&ctx->lock);
+ xa_init_flags(&ctx->buffers, XA_FLAGS_ALLOC);
+ filp->private_data = ctx;
+
+ pr_debug("open: %s\n", dev_name(&gdev->pdev->dev));
+ return 0;
+}
+
+static int fgds_release(struct inode *inode, struct file *filp)
+{
+ struct fgds_file_ctx *ctx = filp->private_data;
+ struct fgds_buffer *buf;
+ unsigned long index;
+
+ xa_for_each(&ctx->buffers, index, buf) {
+ xa_erase(&ctx->buffers, index);
+ fgds_buffer_put(buf);
+ }
+ xa_destroy(&ctx->buffers);
+ mutex_destroy(&ctx->lock);
+ kfree(ctx);
+ return 0;
+}
+
+static const struct file_operations fgds_fops = {
+ .owner = THIS_MODULE,
+ .open = fgds_open,
+ .release = fgds_release,
+ .unlocked_ioctl = fgds_ioctl,
+ .mmap = fgds_mmap,
+};
+
+static void fgds_dev_release(struct device *dev)
+{
+ struct fgds_dev *gdev = container_of(dev, struct fgds_dev, device);
+
+ if (gdev->pgmap) {
+ devm_memunmap_pages(&gdev->pdev->dev, gdev->pgmap);
+ devm_kfree(&gdev->pdev->dev, gdev->pgmap);
+ gdev->pgmap = NULL;
+ }
+ if (gdev->idx >= 0)
+ ida_free(&fgds_ida, gdev->idx);
+ if (gdev->pdev) {
+ pci_dev_put(gdev->pdev);
+ gdev->pdev = NULL;
+ }
+ kfree(gdev);
+}
+
+/* Device naming: /dev/fgds_<domain>_<bus>_<dev>_<func> */
+#define FGDS_DEV_NAME_FMT "fgds_%04x_%02x_%02x_%01x"
+
+/* Format char device name */
+static int fgds_dev_name(struct fgds_dev *gdev)
+{
+ struct pci_dev *pdev = gdev->pdev;
+
+ return dev_set_name(&gdev->device, FGDS_DEV_NAME_FMT,
+ pci_domain_nr(pdev->bus), pdev->bus->number,
+ PCI_SLOT(pdev->devfn), PCI_FUNC(pdev->devfn));
+}
+
+static int fgds_cdev_add(struct fgds_dev *gdev)
+{
+ struct device *dev = &gdev->device;
+ int ret;
+
+ dev->devt = MKDEV(MAJOR(fgds_chr_devt), gdev->idx);
+ dev->class = fgds_chr_class;
+ dev->parent = &gdev->pdev->dev;
+
+ ret = fgds_dev_name(gdev);
+ if (ret)
+ return ret;
+
+ cdev_init(&gdev->cdev, &fgds_fops);
+ gdev->cdev.owner = THIS_MODULE;
+
+ ret = cdev_device_add(&gdev->cdev, dev);
+ if (ret)
+ return ret;
+
+ pr_info("registered PCI %s -> /dev/%s (minor=%d)\n",
+ dev_name(&gdev->pdev->dev), dev_name(dev), gdev->idx);
+ return 0;
+}
+
+/* Build and register an fgds device for a GPU */
+static int fgds_create_device(struct pci_dev *pdev)
+{
+ struct fgds_dev *gdev;
+ int id, ret, j;
+
+ list_for_each_entry(gdev, &fgds_dev_list, node) {
+ if (gdev->pdev == pdev)
+ return -EEXIST;
+ }
+
+ if (fgds_check_gpu_iommu(pdev))
+ return -EPERM;
+
+ id = ida_alloc_range(&fgds_ida, 0, FGDS_MAX_MINORS - 1, GFP_KERNEL);
+ if (id < 0)
+ return id;
+
+ gdev = kzalloc_obj(*gdev, GFP_KERNEL);
+ if (!gdev) {
+ ida_free(&fgds_ida, id);
+ return -ENOMEM;
+ }
+ gdev->idx = id;
+ gdev->pdev = pci_dev_get(pdev);
+
+ device_initialize(&gdev->device);
+ gdev->device.release = fgds_dev_release;
+
+ /*
+ * Same predicate as fgds_has_large_memory_bar(): without the MEM
+ * and PREFETCH test the largest BAR could be a different one than
+ * the BAR that was vetted, and the dma-buf extents would then be
+ * checked against the wrong window.
+ */
+ for (j = 0; j < PCI_STD_NUM_BARS; j++) {
+ resource_size_t sz = pci_resource_len(pdev, j);
+
+ if (!fgds_bar_is_prefetch_mem(pdev, j))
+ continue;
+ if (sz > gdev->bar_size) {
+ gdev->bar_paddr = pci_resource_start(pdev, j);
+ gdev->bar_size = sz;
+ }
+ }
+ pr_info("GPU %s: bus %#x, BAR size 0x%llx, BAR phys %#llx, remapping BAR to kernel VA\n",
+ dev_name(&pdev->dev), pdev->bus->number,
+ (u64)gdev->bar_size, (u64)gdev->bar_paddr);
+
+ ret = fgds_devm_memremap(gdev);
+ if (ret)
+ goto err_put_dev;
+
+ ret = fgds_cdev_add(gdev);
+ if (ret)
+ goto err_put_dev;
+
+ list_add_tail(&gdev->node, &fgds_dev_list);
+ fgds_dev_count++;
+ return 0;
+
+err_put_dev:
+ put_device(&gdev->device);
+ return ret;
+}
+
+static void fgds_destroy_device(struct fgds_dev *gdev)
+{
+ list_del(&gdev->node);
+ fgds_dev_count--;
+ cdev_device_del(&gdev->cdev, &gdev->device);
+ put_device(&gdev->device);
+}
+
+static void fgds_remove_devices(void)
+{
+ struct fgds_dev *gdev, *tmp;
+
+ list_for_each_entry_safe(gdev, tmp, &fgds_dev_list, node)
+ fgds_destroy_device(gdev);
+}
+
+static int __init fgds_init(void)
+{
+ struct pci_dev *pdev = NULL;
+ int ret;
+
+ ret = alloc_chrdev_region(&fgds_chr_devt, 0, FGDS_MAX_MINORS, "fgds");
+ if (ret)
+ return ret;
+
+ fgds_chr_class = class_create("fgds");
+ if (IS_ERR(fgds_chr_class)) {
+ ret = PTR_ERR(fgds_chr_class);
+ goto err_unreg_chrdev;
+ }
+
+ fgds_parse_bdf_list(devices, &fgds_whitelist);
+
+ /* Scan PCI for GPUs: 3D controllers first, then VGA adapters. */
+ while ((pdev = pci_get_class(PCI_CLASS_DISPLAY_3D << 8, pdev)) != NULL) {
+ if (fgds_has_large_memory_bar(pdev) && fgds_should_bind(pdev))
+ fgds_create_device(pdev);
+ }
+
+ pdev = NULL;
+ while ((pdev = pci_get_class(PCI_CLASS_DISPLAY_VGA << 8, pdev)) != NULL) {
+ if (fgds_has_large_memory_bar(pdev) && fgds_should_bind(pdev))
+ fgds_create_device(pdev);
+ }
+
+ if (list_empty(&fgds_dev_list)) {
+ pr_err("no GPU devices registered\n");
+ ret = -ENODEV;
+ goto err_destroy_class;
+ }
+
+ pr_info("loaded successfully: %u GPU(s) active\n", fgds_dev_count);
+ return 0;
+
+err_destroy_class:
+ fgds_remove_devices();
+ class_destroy(fgds_chr_class);
+err_unreg_chrdev:
+ unregister_chrdev_region(fgds_chr_devt, FGDS_MAX_MINORS);
+ return ret;
+}
+
+static void __exit fgds_exit(void)
+{
+ struct fgds_bdf_entry *entry, *tmp;
+
+ fgds_remove_devices();
+ ida_destroy(&fgds_ida);
+ class_destroy(fgds_chr_class);
+ unregister_chrdev_region(fgds_chr_devt, FGDS_MAX_MINORS);
+
+ list_for_each_entry_safe(entry, tmp, &fgds_whitelist, list) {
+ list_del(&entry->list);
+ kfree(entry);
+ }
+
+ pr_info("exit, Good bye!\n");
+}
+
+module_init(fgds_init);
+module_exit(fgds_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("DMA_BUF");
+MODULE_AUTHOR("Mengmeng Zhao <zhaomengmeng@kylinos.cn>, Li Wang <liwang@kylinos.cn>");
+MODULE_DESCRIPTION("Fast GPU Direct Storage");
+MODULE_VERSION("1.0.0");
diff --git a/include/uapi/linux/fgds.h b/include/uapi/linux/fgds.h
new file mode 100644
index 000000000000..4626b8497474
--- /dev/null
+++ b/include/uapi/linux/fgds.h
@@ -0,0 +1,54 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * Fast GPU Direct Storage user-kernel ABI definitions.
+ *
+ * Copyright (C) 2026 KylinSoft. Co., Ltd. All rights reserved.
+ */
+#ifndef _FGDS_H
+#define _FGDS_H
+
+#include <linux/types.h>
+#include <linux/ioctl.h>
+
+/*
+ * Register a GPU dma-buf range.
+ *
+ * @dmabuf_fd: File descriptor exported by the GPU runtime.
+ * @flags: Reserved for future extentions, must be 0.
+ * @dmabuf_offset: Byte offset inside dma-buf (PAGE_SIZE aligned).
+ * @size: Range size in bytes (PAGE_SIZE aligned, non-zero).
+ * @idx: Output token to pass as mmap(2) offset.
+ *
+ * Return mmap token by @idx on success. Caller may close @dmabuf_fd after
+ * registration. Higher platform alignment (e.g., 64KB) must be handled
+ * in user space.
+ */
+struct fgds_ioctl_reg_buffer {
+ /* Input */
+ __s32 dmabuf_fd;
+ __u32 flags; /* Reserved, must be 0 */
+ __u64 dmabuf_offset;
+ __u64 size;
+
+ /* Output */
+ __u64 idx;
+};
+
+/*
+ * Unregister a GPU dma-buf range
+ *
+ * @idx: Token returned by %FGDS_IOCTL_REG_BUFFER.
+ *
+ */
+struct fgds_ioctl_unreg_buffer {
+ __u64 idx;
+};
+
+#define FGDS_IOCTL_MAGIC 0x88
+
+#define FGDS_IOCTL_REG_BUFFER _IOWR(FGDS_IOCTL_MAGIC, 0x01, \
+ struct fgds_ioctl_reg_buffer)
+#define FGDS_IOCTL_UNREG_BUFFER _IOW(FGDS_IOCTL_MAGIC, 0x02, \
+ struct fgds_ioctl_unreg_buffer)
+
+#endif /* _FGDS_H */
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring
2026-09-08 13:15 [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring Li Wang
@ 2026-09-09 6:10 ` Greg Kroah-Hartman
2026-09-09 10:42 ` Li Wang
2026-10-27 9:08 ` [RFC PATCH v2 0/1] misc: fgds: introduce GPU-File direct I/O via dma-buf Li Wang
1 sibling, 1 reply; 14+ messages in thread
From: Greg Kroah-Hartman @ 2026-09-09 6:10 UTC (permalink / raw)
To: Li Wang
Cc: Arnd Bergmann, Sumit Semwal, Christian König, linux-media,
dri-devel, linaro-mm-sig, linux-kernel, Mengmeng Zhao
On Tue, Sep 08, 2026 at 09:15:45PM +0800, Li Wang wrote:
> From: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
>
> Inspired by the paper published in SC'25 [1], we implemented a character
> device named fgds that provides two ioctl interfaces:
> `REG_BUFFER/UNREG_BUFFER`. It enables applications to perform direct I/O
> between GPU memory and NVMe via POSIX and io_uring APIs. This is
> particularly useful for LLM workloads, such as model loading, KV cache
> offloading, and checkpointing. The fgds device corresponds one-to-one with
> the PCIe GPU on the machine. The usage is straightforward: an application
> simply opens the corresponding fgds device, calls ioctl on the returned fd
> with REG_BUFFER, taking the target GPU memory buffer address (represented
> as a dma-buf fd), and the buffer length as inputs, and then invokes mmap on
> the fgds device fd, using the return value of ioctl as the input. The mmap
> call returns a CPU virtual address (call it cpu_vaddr). Afterward,
> cpu_vaddr can be passed directly to pread/pwrite, or
> io_uring_prep_read/io_uring_prep_write to perform direct I/O between files
> on NVMe and GPU memory. A minimal working example can be found in [2].
> The underlying mechanism is that, with the support of fgds device,
> cpu_vaddr is made to point directly to the GPU memory buffer corresponding
> to the dma-buf fd. This solution is loosely coupled with the GPU vendor's
> driver; the GPU vendor only needs to support exporting the allocated GPU
> memory buffer through the standard Linux kernel dma-buf framework, which
> the vast majority of mainstream GPUs already support. This allows both
> applications and the fgds device to work seamlessly with GPUs from
> different vendors without any modifications. Furthermore, applications no
> longer need to call vendor-specific proprietary APIs (such as NVIDIA's
> cuFile API) or install vendor-specific kernel modules (such as NVIDIA's
> nvidia-fs.ko) for different GPU vendors. We have tested fgds on GPU cards
> from NVIDIA, AMD, and several other vendors, and it works well.
>
> Besides the benefits in ease of use and compatibility, another key
> advantage of this solution is higher performance. [2] presents the
> performance comparison results between fgds and NVIDIA GDS. Because fgds
> eliminates the overhead of phony buffers incurred by NVIDIA GDS, it
> achieves significantly higher performance. For example, for reads, fgds
> outperforms GDS by 11% to 109%; for writes, fgds outperforms GDS by 10%
> to 71%.
>
> To further accelerate the read and write operations of large files or
> massive data volumes—which are very common in LLM scenarios—we have
> implemented library functions `fgds_read` and `fgds_write`. Under the hood,
> these interfaces split large data into chunks and submit them
> asynchronously and in parallel via io_uring, thereby further boosting I/O
> performance, with read performance improved by up to 115% and write
> performance by up to 40%. In addition, we also provide the `fgds_register`
> library interface to encapsulate the `open`, `ioctl' and `mmap` operations.
> Readers who are interested can refer to [2].
>
> In addition, we have added the LMCache backend, enabling vLLM to offload KV
> cache via LMCache using fgds, which accelerates inference performance. We
> also added PyTorch APIs, compatible with the PyTorch GDS API, to improve
> the performance of reading and writing checkpoints during LLM training.
>
> We look forward to community feedback and are fully committed to iterating
> on this series to work towards upstreaming.
That's not really needed in a changelog text, it could be in the 0/X
patch :)
Anyway, you didn't cc: the io_uring list, why?
Also, as a first cut, please see the sashiko comments on this patch:
https://sashiko.dev/#/patchset/20260908131545.105987-1-liwang@kylinos.cn
>
> [1] https://dl.acm.org/doi/10.1145/3712285.3759862
> [2] https://github.com/Storage-and-OS-for-AI/fgds
>
> Signed-off-by: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
> Signed-off-by: Li Wang <liwang@kylinos.cn>
> ---
> drivers/misc/Kconfig | 9 +
> drivers/misc/Makefile | 1 +
> drivers/misc/fgds.c | 989 ++++++++++++++++++++++++++++++++++++++
> include/uapi/linux/fgds.h | 54 +++
> 4 files changed, 1053 insertions(+)
> create mode 100644 drivers/misc/fgds.c
> create mode 100644 include/uapi/linux/fgds.h
>
> diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
> index 7364931dad3a..2f3a5a5fd0bf 100644
> --- a/drivers/misc/Kconfig
> +++ b/drivers/misc/Kconfig
> @@ -568,6 +568,15 @@ config MCHP_LAN966X_PCI
> - lan966x-miim (MDIO_MSCC_MIIM)
> - lan966x-switch (LAN966X_SWITCH)
>
> +config FGDS
> + tristate "GPU-NVMe direct I/O control driver"
> + depends on PCI && DMA_SHARED_BUFFER && ZONE_DEVICE
> + help
> + Say Y here if you want to support GPU-NVME direct I/O
> + via POSIX/io_uring interfaces.
> +
> + If unsure, say N.
Module name is not listed here.
Nor why "fgds" is the name, that's going to be hard to remember, does it
stand for something?
> +
> source "drivers/misc/c2port/Kconfig"
> source "drivers/misc/eeprom/Kconfig"
> source "drivers/misc/cb710/Kconfig"
> diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
> index e8d8d5d88c0d..04985abe1678 100644
> --- a/drivers/misc/Makefile
> +++ b/drivers/misc/Makefile
> @@ -71,3 +71,4 @@ obj-y += keba/
> obj-y += amd-sbi/
> obj-$(CONFIG_MISC_RP1) += rp1/
> obj-$(CONFIG_INTEL_SSEI) += issei/
> +obj-$(CONFIG_FGDS) += fgds.o
> diff --git a/drivers/misc/fgds.c b/drivers/misc/fgds.c
> new file mode 100644
> index 000000000000..3aa4945f701b
> --- /dev/null
> +++ b/drivers/misc/fgds.c
> @@ -0,0 +1,989 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Fast GPU Direct Storage via dma-buf.
> + *
> + * Copyright (C) 2026 KylinSoft. Co., Ltd. All rights reserved.
> + *
> + * Maps GPU memory into user space to enable direct NVME-to-GPU DMA
> + * pread/pwrite syscalls. BAR pages are remapped into ZONE_DEVICE via
> + * devm_memremap_pages() and populated using dma-buf backing pages.
> + */
> +#define pr_fmt(fmt) "fgds: " fmt
You are a driver, always use dev_*() print functions, not pr_()
functions, as you will loose the device information. For example:
> +/*
> + * BAR-based mapping requires device physical addresses. When using
> + * IOMMU, DMA addresses are IOVAs, which cannot be mapped directly.
> + */
> +static int fgds_check_gpu_iommu(struct pci_dev *pdev)
> +{
> + struct iommu_domain *domain;
> +
> + domain = iommu_get_domain_for_dev(&pdev->dev);
> + if (domain && domain->type != IOMMU_DOMAIN_IDENTITY) {
> + pr_warn("%s: reject attaching a translating IOMMU domain (requires iommu=pt or off\n",
> + dev_name(&pdev->dev));
Should be dev_warn(), right?
But what can userspace do with that warning, did something just break?
> + pr_info("loaded successfully: %u GPU(s) active\n", fgds_dev_count);
When drivers work, they are quiet, please remove this, and the other
pr_info() lines, as they seem to be left over from your debugging.
thanks,
greg k-h
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring
2026-09-09 6:10 ` Greg Kroah-Hartman
@ 2026-09-09 10:42 ` Li Wang
2026-09-09 13:35 ` Greg Kroah-Hartman
0 siblings, 1 reply; 14+ messages in thread
From: Li Wang @ 2026-09-09 10:42 UTC (permalink / raw)
To: Greg Kroah-Hartman
Cc: Arnd Bergmann, Sumit Semwal, Christian König, linux-media,
dri-devel, linaro-mm-sig, io-uring, linux-kernel, Mengmeng Zhao
Hi Greg,
Thanks for the review!
>
> That's not really needed in a changelog text, it could be in the 0/X
> patch :)
>
Sorry for the clutter. I will move most of them into the 0/X patch in v2.
> Anyway, you didn't cc: the io_uring list, why?
>
`scripts/get_maintainer.pl` didn't output the io_uring mailing list, likely
because this patch doesn't directly touch the io_uring codebase itself. It only
enables remapping GPU memory buffers to CPU virtual addresses, which can then be
consumed via standard io_uring APIs.
I've added io-uring@vger.kernel.org to CC for this reply and will keep it in v2.
>
> Nor why "fgds" is the name, that's going to be hard to remember, does it
> stand for something?
>
"FGDS" stands for Fast GPUDirect Storage. GPUDirect Storage (GDS) is NVIDIA's
technology enabling direct I/O between GPU memory and files on NVMe,
widely used in LLM workloads to bypass CPU overhead.
If "FGDS" feels unintuitive, please let us know if you have a better alternative.
We will address the rest of the code comments in the v2 patch series soon.
Thanks,
Li Wang
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring
2026-09-09 10:42 ` Li Wang
@ 2026-09-09 13:35 ` Greg Kroah-Hartman
2026-09-09 13:45 ` Christian König
0 siblings, 1 reply; 14+ messages in thread
From: Greg Kroah-Hartman @ 2026-09-09 13:35 UTC (permalink / raw)
To: Li Wang
Cc: Arnd Bergmann, Sumit Semwal, Christian König, linux-media,
dri-devel, linaro-mm-sig, io-uring, linux-kernel, Mengmeng Zhao
On Wed, Sep 09, 2026 at 06:42:03PM +0800, Li Wang wrote:
> Hi Greg,
> Thanks for the review!
>
> >
> > That's not really needed in a changelog text, it could be in the 0/X
> > patch :)
> >
> Sorry for the clutter. I will move most of them into the 0/X patch in v2.
>
> > Anyway, you didn't cc: the io_uring list, why?
> >
> `scripts/get_maintainer.pl` didn't output the io_uring mailing list, likely
> because this patch doesn't directly touch the io_uring codebase itself. It only
> enables remapping GPU memory buffers to CPU virtual addresses, which can then be
> consumed via standard io_uring APIs.
>
> I've added io-uring@vger.kernel.org to CC for this reply and will keep it in v2.
Great, as you are using that as the api, there might be some parts that
will need to be reviewed by them.
> > Nor why "fgds" is the name, that's going to be hard to remember, does it
> > stand for something?
> >
> "FGDS" stands for Fast GPUDirect Storage. GPUDirect Storage (GDS) is NVIDIA's
> technology enabling direct I/O between GPU memory and files on NVMe,
> widely used in LLM workloads to bypass CPU overhead.
That's nvidia's specific solution, but this works on other devices,
right? Or just for that one platform?
And you are using this as a "bypass" for the normal accel subsystem,
shouldn't this be part of that subsystem instead of a custom user/kernel
api like you are creating here?
thanks,
greg k-h
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring
2026-09-09 13:35 ` Greg Kroah-Hartman
@ 2026-09-09 13:45 ` Christian König
2026-09-10 3:57 ` Li Wang
0 siblings, 1 reply; 14+ messages in thread
From: Christian König @ 2026-09-09 13:45 UTC (permalink / raw)
To: Greg Kroah-Hartman, Li Wang
Cc: Arnd Bergmann, Sumit Semwal, linux-media, dri-devel,
linaro-mm-sig, io-uring, linux-kernel, Mengmeng Zhao
On 9/9/26 15:35, Greg Kroah-Hartman wrote:
> On Wed, Sep 09, 2026 at 06:42:03PM +0800, Li Wang wrote:
>> Hi Greg,
>> Thanks for the review!
>>
>>>
>>> That's not really needed in a changelog text, it could be in the 0/X
>>> patch :)
>>>
>> Sorry for the clutter. I will move most of them into the 0/X patch in v2.
>>
>>> Anyway, you didn't cc: the io_uring list, why?
>>>
>> `scripts/get_maintainer.pl` didn't output the io_uring mailing list, likely
>> because this patch doesn't directly touch the io_uring codebase itself. It only
>> enables remapping GPU memory buffers to CPU virtual addresses, which can then be
>> consumed via standard io_uring APIs.
>>
>> I've added io-uring@vger.kernel.org to CC for this reply and will keep it in v2.
>
> Great, as you are using that as the api, there might be some parts that
> will need to be reviewed by them.
>
>>> Nor why "fgds" is the name, that's going to be hard to remember, does it
>>> stand for something?
>>>
>> "FGDS" stands for Fast GPUDirect Storage. GPUDirect Storage (GDS) is NVIDIA's
>> technology enabling direct I/O between GPU memory and files on NVMe,
>> widely used in LLM workloads to bypass CPU overhead.
>
> That's nvidia's specific solution, but this works on other devices,
> right? Or just for that one platform?
That was nvidia's specific and very hacky out of tree solution which as far as I know is pretty much abandoned everywhere.
AMD came up with something similar, but all those approaches are so fundamentally broken that we didn't even considered upstreaming it.
> And you are using this as a "bypass" for the normal accel subsystem,
> shouldn't this be part of that subsystem instead of a custom user/kernel
> api like you are creating here?
As far as I know there is a patch set under review and even already partially merged which enables exactly that functionality as general feature for DMA-buf which is vendor independent and should at least in theory work with all drivers.
I'm really surprised that somebody is still working on the vendor specific stuff.
Regards,
Christian.
>
> thanks,
>
> greg k-h
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring
2026-09-09 13:45 ` Christian König
@ 2026-09-10 3:57 ` Li Wang
2026-09-10 8:35 ` Christian König
2026-09-12 12:45 ` Pavel Begunkov
0 siblings, 2 replies; 14+ messages in thread
From: Li Wang @ 2026-09-10 3:57 UTC (permalink / raw)
To: Christian König, Greg Kroah-Hartman
Cc: Arnd Bergmann, Sumit Semwal, linux-media, dri-devel,
linaro-mm-sig, io-uring, linux-kernel, Mengmeng Zhao
Hi Christian,
>>>> Nor why "fgds" is the name, that's going to be hard to remember, does it
>>>> stand for something?
>>>>
>>> "FGDS" stands for Fast GPUDirect Storage. GPUDirect Storage (GDS) is NVIDIA's
>>> technology enabling direct I/O between GPU memory and files on NVMe,
>>> widely used in LLM workloads to bypass CPU overhead.
>>
>> That's nvidia's specific solution, but this works on other devices,
>> right? Or just for that one platform?
>
> That was nvidia's specific and very hacky out of tree solution
Exactly, I completely agree with your viewpoint. In fact, we previously conducted
a deep analysis of NVIDIA's GDS implementation code, which was also one of the
motivations for us to develop fgds.
Please allow me to introduce NVIDIA's GDS implementation briefly:
NVIDIA introduced two kernel modules: one module called nvidia-fs, and the other module
is a customized NVMe driver replacing the Linux kernel's default NVMe driver.
nvidia-fs creates a device file for every NVIDIA GPU on the machine: /dev/nvidia-fs<gpu-id>.
Applications call cuFileBufRegister to register the GPU memory buffer to perform I/O.
cuFileBufRegister executes the following operations: calls nvidia-fs via ioctl. The
implementation of this ioctl allocates a corresponding phony buffer of the same size
in the host memory, and establishes a mapping between the CPU memory phony buffer
and the GPU memory buffer.
Then, the application calls cuFileRead/cuFileWrite to perform file I/O operations.
Its implementation calls ioctl on nvidia-fs with NVFS_IOCTL_READ/NVFS_IOCTL_WRITE
as parameters. Inside the ioctl implementation, it calls the common kernel interface
filp->f_op->read_iter/write_iter() on the file on NVME. These interfaces can only take
the CPU memory phony buffer address as a input, constructing read/write requests sent
through the block layer to the customized NVMe driver.
The customized NVMe driver intercepts the I/O operations, calls the nvidia-fs interface to
query, replaces the phony buffer address with the GPU memory dma address, and performs
DMA transfer between GPU memory and NVMe.
As we can see, this implementation is indeed very hacky and non-elegant. Furthermore,
as evaluated in this paper published in SC'25 [1], the phony buffer brings a considerable
performance overhead.
> which as far as I know is pretty much abandoned everywhere.
However, despite the overhead of phony buffers, GDS performance is still significantly higher
than transferring through CPU host memory (as shown in our performance benchmark tests [2]).
Therefore, GDS is actually still widely used in the LLM ecosystem. For instance, model loading
plugins used in inference engines like vLLM and SGLang—such as fastsafetensors and InstantTensor—
both support acceleration via GDS [3,4], with fastsafetensors enabling GDS by default for model loading.
Furthermore, LMCache, a plugin used for KV cache offloading in vLLM and SGLang, also supports
GDS acceleration [5]. PyTorch itself also provides file access APIs based on GDS [6].
>
> AMD came up with something similar, but all those approaches are so fundamentally broken that we didn't even considered upstreaming it.
>> And you are using this as a "bypass" for the normal accel subsystem,
>> shouldn't this be part of that subsystem instead of a custom user/kernel
>> api like you are creating here?
>
> As far as I know there is a patch set under review and even already partially merged which enables exactly that functionality as general feature for DMA-buf which is vendor independent and should at least in theory work with all drivers.
>
> I'm really surprised that somebody is still working on the vendor specific stuff.
As you pointed out, every vendor has been inventing their own way and interfaces to support GDS,
introducing custom kernel modules and proprietary UAPI interfaces, with varying performance that
leaves developers heavily frustrated. Apologies for not making this clear enough in our commit
messages, which understandably caused some confusion. We merely borrowed the name "GDS" to describe
the functional purpose of fgds.
In fact, we believe fgds offers four key advantages:
(1) GPU platform independence;
(2) POSIX/io_uring interface compatibility;
(3) Higher performance than GDS;
(4) Minimal kernel footprint and UAPI footprint
Regarding (1), (2), and (3), please allow me to briefly explain the design mechanism of fgds:
fgds turns a GPU memory buffer into a POSIX/io_uring-compatible user-space virtual address via
three main steps:
Step 1: Utilizing ZONE_DEVICE support, we remap the GPU memory exposed via PCIe BAR into struct pages
using devm_memremap_pages();
Step 2: Utilizing dma-buf support, the GPU memory buffer is exported as a dma-buf file descriptor (fd).
Using this fd as a bridge, we look up the corresponding DMA addresses for the GPU memory buffer inside
the kernel;
Step 3: Through mmap, we insert the struct pages corresponding to the GPU memory buffer into the userspace
VMA, mapping their physical/DMA addresses directly. The virtual address returned by mmap can then be directly
passed into standard POSIX or io_uring interfaces.
As you can see, since almost all major GPU vendors support exporting GPU memory buffers via dma-buf,
all remaining technical dependencies of fgds rely on standard Linux kernel infrastructure. Therefore,
fgds is completely vendor-agnostic and natively compatible with POSIX/io_uring without introducing any proprietary
vendor interfaces, which greatly simplifies development, deployment, operations and unifies standard usage.
Furthermore, because this technique completely eliminates the phony buffer, its performance is significantly
better than NVIDIA's GDS (as shown in our benchmarks [2]).
In addition, since fgds uses only the most fundamental dma-buf mechanisms, it relies on baseline dma-buf features
that have been supported in the upstream kernel for a long time, rather than any new dma-buf features currently
under active development. In fact, before we recently ported fgds to the latest kernel tree, it was developed
and ran on our internal 6.6 kernel. It has been running stably in our production clusters for over half a year
across various hardware platforms (including NVIDIA, AMD, and several other vendors) without requiring a single
line of GPU-platform-specific fgds code modification.
Regarding (4): We believe that implementing GDS-like functionality inherently requires kernel assistance to map
the GPU memory buffer to a valid userspace virtual address. This inevitably requires userspace-kernel interaction.
To the best of our knowledge, the mainline kernel currently lacks a dedicated, unified path for this specific interaction,
which is why various vendors ended up writing their own out-of-tree interfaces. In contrast, fgds introduces
only one single new ioctl parameter (REG_BUFFER, excluding UNREG_BUFFER), and confines its scope strictly to a
standalone device driver. We believe this achieves a minimal kernel footprint and minimal UAPI addition.
[1] https://dl.acm.org/doi/10.1145/3712285.3759862
[2] https://github.com/Storage-and-OS-for-AI/fgds
[3] https://github.com/foundation-model-stack/fastsafetensors/blob/main/docs/configuration.md
[4] https://github.com/scitix/InstantTensor/blob/main/csrc/loader_io_cufile.cpp
[5] https://github.com/LMCache/LMCache/blob/dev/lmcache/v1/storage_backend/gds_backend.py
[6] https://docs.pytorch.org/docs/2.14/generated/torch.cuda.gds.GdsFile.html
Thanks,
Li Wang> Regards,
> Christian.
>
>>
>> thanks,
>>
>> greg k-h
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring
2026-09-10 3:57 ` Li Wang
@ 2026-09-10 8:35 ` Christian König
2026-09-12 12:45 ` Pavel Begunkov
1 sibling, 0 replies; 14+ messages in thread
From: Christian König @ 2026-09-10 8:35 UTC (permalink / raw)
To: Li Wang, Greg Kroah-Hartman
Cc: Arnd Bergmann, Sumit Semwal, linux-media, dri-devel,
linaro-mm-sig, io-uring, linux-kernel, Mengmeng Zhao
On 9/10/26 05:57, Li Wang wrote:
...
>> AMD came up with something similar, but all those approaches are so fundamentally broken that we didn't even considered upstreaming it.
>>> And you are using this as a "bypass" for the normal accel subsystem,
>>> shouldn't this be part of that subsystem instead of a custom user/kernel
>>> api like you are creating here?
>>
>> As far as I know there is a patch set under review and even already partially merged which enables exactly that functionality as general feature for DMA-buf which is vendor independent and should at least in theory work with all drivers.
>>
>> I'm really surprised that somebody is still working on the vendor specific stuff.
> As you pointed out, every vendor has been inventing their own way and interfaces to support GDS,
> introducing custom kernel modules and proprietary UAPI interfaces, with varying performance that
> leaves developers heavily frustrated. Apologies for not making this clear enough in our commit
> messages, which understandably caused some confusion. We merely borrowed the name "GDS" to describe
> the functional purpose of fgds.
>
> In fact, we believe fgds offers four key advantages:
> (1) GPU platform independence;
> (2) POSIX/io_uring interface compatibility;
> (3) Higher performance than GDS;
> (4) Minimal kernel footprint and UAPI footprint
>
> Regarding (1), (2), and (3), please allow me to briefly explain the design mechanism of fgds:
> fgds turns a GPU memory buffer into a POSIX/io_uring-compatible user-space virtual address via
> three main steps:
>
> Step 1: Utilizing ZONE_DEVICE support, we remap the GPU memory exposed via PCIe BAR into struct pages
> using devm_memremap_pages();
>
> Step 2: Utilizing dma-buf support, the GPU memory buffer is exported as a dma-buf file descriptor (fd).
> Using this fd as a bridge, we look up the corresponding DMA addresses for the GPU memory buffer inside
> the kernel;
>
> Step 3: Through mmap, we insert the struct pages corresponding to the GPU memory buffer into the userspace
> VMA, mapping their physical/DMA addresses directly. The virtual address returned by mmap can then be directly
> passed into standard POSIX or io_uring interfaces.
Well long story short what you do here is completely broken.
Approaches like those have been suggested before and we added both documentation as well as code to prevent such hacks from working.
Please see Pavel Begunkov patch set on the LKML which adds DMA-buf support to io_uring for how to do it correctly. Just google for "Add dmabuf read/write via io_uring".
Regards,
Christian.
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring
2026-09-10 3:57 ` Li Wang
2026-09-10 8:35 ` Christian König
@ 2026-09-12 12:45 ` Pavel Begunkov
1 sibling, 0 replies; 14+ messages in thread
From: Pavel Begunkov @ 2026-09-12 12:45 UTC (permalink / raw)
To: Li Wang, Christian König, Greg Kroah-Hartman
Cc: Arnd Bergmann, Sumit Semwal, linux-media, dri-devel,
linaro-mm-sig, io-uring, linux-kernel, Mengmeng Zhao
On 9/10/26 04:57, Li Wang wrote:
> Hi Christian,
>
>>>>> Nor why "fgds" is the name, that's going to be hard to remember, does it
>>>>> stand for something?
>>>>>
>>>> "FGDS" stands for Fast GPUDirect Storage. GPUDirect Storage (GDS) is NVIDIA's
>>>> technology enabling direct I/O between GPU memory and files on NVMe,
>>>> widely used in LLM workloads to bypass CPU overhead.
>>>
>>> That's nvidia's specific solution, but this works on other devices,
>>> right? Or just for that one platform?
>>
>> That was nvidia's specific and very hacky out of tree solution
> Exactly, I completely agree with your viewpoint. In fact, we previously conducted
> a deep analysis of NVIDIA's GDS implementation code, which was also one of the
> motivations for us to develop fgds.
>
> Please allow me to introduce NVIDIA's GDS implementation briefly:
> NVIDIA introduced two kernel modules: one module called nvidia-fs, and the other module
> is a customized NVMe driver replacing the Linux kernel's default NVMe driver.
> nvidia-fs creates a device file for every NVIDIA GPU on the machine: /dev/nvidia-fs<gpu-id>.
I'm not an nvidia expert, but IIRC they switched to P2PDMA, which
is supported by the upstream nvme driver.
--
Pavel Begunkov
^ permalink raw reply [flat|nested] 14+ messages in thread
* [RFC PATCH v2 0/1] misc: fgds: introduce GPU-File direct I/O via dma-buf
2026-09-08 13:15 [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring Li Wang
2026-09-09 6:10 ` Greg Kroah-Hartman
@ 2026-10-27 9:08 ` Li Wang
2026-10-27 9:08 ` [RFC PATCH v2 1/1] " Li Wang
2026-10-27 13:54 ` [RFC PATCH v2 0/1] " Pavel Begunkov
1 sibling, 2 replies; 14+ messages in thread
From: Li Wang @ 2026-10-27 9:08 UTC (permalink / raw)
To: Arnd Bergmann, Greg Kroah-Hartman
Cc: Sumit Semwal, Christian König, Pavel Begunkov, linux-media,
dri-devel, linaro-mm-sig, io-uring, linux-kernel, Li Wang
This patch introduces a driver named fgds that allows moving data between
a GPU dma-buf and a regular file on NVME without staging it through the
page cache. This is particularly useful for LLM workloads, such as model
loading, KV cache offloading, and checkpointing. The fgds device
corresponds one-to-one with the PCIe GPU on the machine. The usage is
straightforward: an application first calls ioctl on the corresponding
fgds device with REGISTER_BUF, passing the target GPU memory buffer
address (represented as a dma-buf fd). Subsequently, the application can
perform direct data transfer between a regular file on NVMe and GPU memory
by invoking ioctl on the fgds device with READ/WRITE, passing the file
descriptor and the handle returned by REGISTER_BUF.
This solution allows both applications and the fgds device to work
seamlessly with GPUs from different vendors without any modifications.
Furthermore, applications no longer need to call vendor-specific
proprietary APIs (such as NVIDIA's cuFile API) or install vendor-specific
kernel modules (such as NVIDIA's nvidia-fs.ko) for different GPU vendors.
We have tested fgds on NVDIA and AMD GPU cards, and theoretically, it also
works on GPUs from other vendors that support exporting GPU memory via
dma-buf. Some performance test results can be found in [1].
This implementation offers several advantages:
- Simplicity and Efficiency: It provides a lightweight and intuitive
interface. The single ioctl call mimics the parameter structure of
pread/pwrite, and its return value semantics are fully compatible with
pread/pwrite
- Isolation and Security: The interface is independent and has a minimal
impact scope. This isolation facilitates fine-grained permission control
on the device node, enhancing security management
Changes since v1:
- Refactor implementation per Christian's feedback, on top of Pavel's
DMA-buf support for io_uring [2]
- Add validation checks in the XFS I/O submission path to ensure that
bios with the REQ_DMABUF flag are only processed via direct I/O, and
prevents the kernel from attempting to fall back to buffered I/O, which
is incompatible with DMA-buf backed GPU memory regions. While currently
supports only XFS, the underlying mechanism is filesystem-agnostic and
can be adapted for other filesystems with similar minor changes
- Address the comments from Greg and the Sashiko AI
[1] https://github.com/Storage-and-OS-for-AI/fgds
[2] https://git.kernel.org/pub/scm/linux/kernel/git/axboe/linux.git/
tag/?h=for-7.4/io_uring-dmabuf-20261023
Mengmeng Zhao (1):
misc: fgds: introduce GPU-File direct I/O via dma-buf
.../userspace-api/ioctl/ioctl-number.rst | 2 +
drivers/dma-buf/dma-buf-io.c | 3 +
drivers/misc/Kconfig | 16 +
drivers/misc/Makefile | 1 +
drivers/misc/fgds.c | 506 ++++++++++++++++++
fs/iomap/buffered-io.c | 11 +
fs/iomap/direct-io.c | 23 +-
fs/xfs/xfs_file.c | 4 +
include/linux/iov_iter.h | 4 +
include/uapi/linux/fgds.h | 124 +++++
lib/iov_iter.c | 1 +
mm/filemap.c | 22 +-
12 files changed, 713 insertions(+), 4 deletions(-)
create mode 100644 drivers/misc/fgds.c
create mode 100644 include/uapi/linux/fgds.h
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread
* [RFC PATCH v2 1/1] misc: fgds: introduce GPU-File direct I/O via dma-buf
2026-10-27 9:08 ` [RFC PATCH v2 0/1] misc: fgds: introduce GPU-File direct I/O via dma-buf Li Wang
@ 2026-10-27 9:08 ` Li Wang
2026-10-27 13:03 ` Jens Axboe
2026-10-27 13:12 ` Christian König
2026-10-27 13:54 ` [RFC PATCH v2 0/1] " Pavel Begunkov
1 sibling, 2 replies; 14+ messages in thread
From: Li Wang @ 2026-10-27 9:08 UTC (permalink / raw)
To: Arnd Bergmann, Greg Kroah-Hartman
Cc: Sumit Semwal, Christian König, Pavel Begunkov, linux-media,
dri-devel, linaro-mm-sig, io-uring, linux-kernel, Mengmeng Zhao,
Li Wang
From: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
This patch introduces a driver that allows moving data between a GPU
dma-buf and a regular file on NVME without staging it through the page
cache. Userspace registers the GPU dma-buf with the device, then issues
read and write requests via the device's ioctls. Each transfer is direct
I/O: an ITER_DMABUF_MAP iterator is passed to the file's read_iter or
write_iter, and the filesystem's iomap path submits REQ_DMABUF bios that
already carry the dma addresses.
Signed-off-by: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
Signed-off-by: Li Wang <liwang@kylinos.cn>
---
.../userspace-api/ioctl/ioctl-number.rst | 2 +
drivers/dma-buf/dma-buf-io.c | 3 +
drivers/misc/Kconfig | 16 +
drivers/misc/Makefile | 1 +
drivers/misc/fgds.c | 506 ++++++++++++++++++
fs/iomap/buffered-io.c | 11 +
fs/iomap/direct-io.c | 23 +-
fs/xfs/xfs_file.c | 4 +
include/linux/iov_iter.h | 4 +
include/uapi/linux/fgds.h | 124 +++++
lib/iov_iter.c | 1 +
mm/filemap.c | 22 +-
12 files changed, 713 insertions(+), 4 deletions(-)
create mode 100644 drivers/misc/fgds.c
create mode 100644 include/uapi/linux/fgds.h
diff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst
index 2fc53093752d..5f93a252e773 100644
--- a/Documentation/userspace-api/ioctl/ioctl-number.rst
+++ b/Documentation/userspace-api/ioctl/ioctl-number.rst
@@ -306,6 +306,8 @@ Code Seq# Include File Comments
'v' 20-27 arch/powerpc/include/uapi/asm/vas-api.h VAS API
'v' C0-FF linux/meye.h conflict!
'w' all CERN SCI driver
+'x' 00-0F linux/fgds.h Fast GPU Direct Storage
+ <mailto:liwang@kylinos.cn>
'y' 00-1F packet based user level communications
<mailto:zapman@interlan.net>
'z' 00-3F CAN bus card conflict!
diff --git a/drivers/dma-buf/dma-buf-io.c b/drivers/dma-buf/dma-buf-io.c
index 8440151de860..2ee4fc683c74 100644
--- a/drivers/dma-buf/dma-buf-io.c
+++ b/drivers/dma-buf/dma-buf-io.c
@@ -135,6 +135,7 @@ struct dma_buf_io_map *dma_buf_io_create_map(struct dma_buf_io_ctx *ctx)
dma_resv_unlock(ctx->dmabuf->resv);
return map;
}
+EXPORT_SYMBOL_NS_GPL(dma_buf_io_create_map, "DMA_BUF");
static void dma_buf_io_ctx_shutdown(struct dma_buf_io_ctx *ctx)
{
@@ -190,6 +191,7 @@ int dma_buf_io_ctx_create(struct file *file,
*out_ctx = ctx;
return 0;
}
+EXPORT_SYMBOL_NS_GPL(dma_buf_io_ctx_create, "DMA_BUF");
/**
* dma_buf_io_ctx_release - Destroy the I/O context
@@ -211,6 +213,7 @@ void dma_buf_io_ctx_release(struct dma_buf_io_ctx *ctx)
dma_buf_put(ctx->dmabuf);
kfree(ctx);
}
+EXPORT_SYMBOL_NS_GPL(dma_buf_io_ctx_release, "DMA_BUF");
/**
* dma_buf_io_invalidate_mappings - Invalidate the current map
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 7364931dad3a..b85fd64d27ad 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -568,6 +568,22 @@ config MCHP_LAN966X_PCI
- lan966x-miim (MDIO_MSCC_MIIM)
- lan966x-switch (LAN966X_SWITCH)
+config FGDS
+ tristate "Fast GPU Direct Storage support"
+ depends on DMA_SHARED_BUFFER && BLOCK
+ default n
+ help
+ This driver exposes a misc device that moves data between a GPU
+ dma-buf and a regular file with synchronous read/write ioctls,
+ submitting the transfer as direct I/O (REQ_DMABUF bios) through
+ the file system's iomap DIO path without a page cache bounce
+ buffer.
+
+ To compile this driver as a module, choose M here: the module
+ will be called fgds.
+
+ If unsure, say N.
+
source "drivers/misc/c2port/Kconfig"
source "drivers/misc/eeprom/Kconfig"
source "drivers/misc/cb710/Kconfig"
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index e8d8d5d88c0d..cd98ef3d008e 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -70,4 +70,5 @@ obj-$(CONFIG_MCHP_LAN966X_PCI) += lan966x-pci.o
obj-y += keba/
obj-y += amd-sbi/
obj-$(CONFIG_MISC_RP1) += rp1/
+obj-$(CONFIG_FGDS) += fgds.o
obj-$(CONFIG_INTEL_SSEI) += issei/
diff --git a/drivers/misc/fgds.c b/drivers/misc/fgds.c
new file mode 100644
index 000000000000..3929a6514d23
--- /dev/null
+++ b/drivers/misc/fgds.c
@@ -0,0 +1,506 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Fast GPU Direct Storage via dma-buf.
+ *
+ * Copyright (C) 2026 KylinSoft. Co., Ltd. All rights reserved.
+ *
+ * Synchronous read/write ioctls that move data between a GPU dma-buf and
+ * a regular file.
+ */
+
+#include <linux/blkdev.h>
+#include <linux/dma-buf.h>
+#include <linux/dma-buf-io.h>
+#include <linux/device.h>
+#include <linux/fcntl.h>
+#include <linux/fileattr.h>
+#include <linux/fs.h>
+#include <linux/fsnotify.h>
+#include <linux/magic.h>
+#include <linux/math.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/overflow.h>
+#include <linux/pagemap.h>
+#include <linux/sched/xacct.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+#include <linux/uio.h>
+#include <linux/xarray.h>
+
+#include <linux/fgds.h>
+
+static struct miscdevice fgds_misc;
+
+/* One registered buffer: a dma-buf bound to a block device. */
+struct fgds_reg {
+ struct kref kref;
+ struct dma_buf_io_ctx *ctx;
+ struct file *bdev_file;
+ struct block_device *bdev;
+ u64 size;
+};
+
+/* State of one /dev/fgds open file description; handles are private to it. */
+struct fgds_state {
+ struct xarray regs;
+ /* protects @regs and, with the reg kref, the lifetime of entries */
+ struct mutex lock;
+};
+
+static void fgds_reg_release(struct kref *kref)
+{
+ struct fgds_reg *reg = container_of(kref, struct fgds_reg, kref);
+
+ dma_buf_io_ctx_release(reg->ctx);
+ fput(reg->bdev_file);
+ kfree(reg);
+}
+
+static void fgds_reg_put(struct fgds_reg *reg)
+{
+ kref_put(®->kref, fgds_reg_release);
+}
+
+static struct fgds_state *fgds_state(struct file *file)
+{
+ return file->private_data;
+}
+
+/*
+ * File and file-system level gate.
+ */
+static int fgds_gate(struct file *ffile)
+{
+ struct inode *inode = file_inode(ffile);
+ struct super_block *sb = inode->i_sb;
+ struct file_kattr fa;
+
+ if (!S_ISREG(inode->i_mode))
+ return -EINVAL;
+ if (sb->s_magic != XFS_SUPER_MAGIC)
+ return -EOPNOTSUPP;
+ if (IS_DAX(inode))
+ return -EOPNOTSUPP;
+ if (mapping_stable_writes(inode->i_mapping))
+ return -EOPNOTSUPP;
+ if (!sb->s_bdev)
+ return -EOPNOTSUPP;
+ if (bdev_is_zoned(sb->s_bdev))
+ return -EOPNOTSUPP;
+ if (!sb->s_bdev->bd_disk->fops->init_dma_buf_io_ctx)
+ return -EOPNOTSUPP;
+
+ /*
+ * Realtime inodes live on a separate device whose DMA setup is not
+ * covered here.
+ */
+ if (vfs_fileattr_get(ffile->f_path.dentry, &fa))
+ return -EOPNOTSUPP;
+ if (fa.fsx_xflags & FS_XFLAG_REALTIME)
+ return -EOPNOTSUPP;
+ return 0;
+}
+
+static unsigned int fgds_file_align(struct inode *inode, bool write)
+{
+ unsigned int lbs = bdev_logical_block_size(inode->i_sb->s_bdev);
+
+ if (!write)
+ return lbs;
+ return max(1U << inode->i_blkbits, lbs);
+}
+
+static unsigned int fgds_dma_align(struct inode *inode)
+{
+ return bdev_dma_alignment(inode->i_sb->s_bdev) + 1;
+}
+
+static struct fgds_reg *fgds_reg_get(struct fgds_state *state, u32 handle)
+{
+ struct fgds_reg *reg;
+
+ mutex_lock(&state->lock);
+ reg = xa_load(&state->regs, handle);
+ if (reg)
+ kref_get(®->kref);
+ mutex_unlock(&state->lock);
+ return reg;
+}
+
+static int fgds_unregister_handle(struct fgds_state *state, u32 handle)
+{
+ struct fgds_reg *reg;
+
+ mutex_lock(&state->lock);
+ reg = xa_erase(&state->regs, handle);
+ mutex_unlock(&state->lock);
+ if (reg)
+ fgds_reg_put(reg);
+ return reg ? 0 : -EINVAL;
+}
+
+static int fgds_register(struct file *file, struct fgds_register_args __user *uarg)
+{
+ struct fgds_register_args args;
+ struct fgds_state *state = fgds_state(file);
+ struct dma_buf_io_ctx *ctx = NULL;
+ struct block_device *bdev;
+ struct file *bdev_file;
+ struct dma_buf *dbuf;
+ struct fd bfdf;
+ struct fgds_reg *reg;
+ u32 id;
+ int ret;
+
+ if (copy_from_user(&args, uarg, sizeof(args)))
+ return -EFAULT;
+ if (args.handle || args.pad || args.reserved)
+ return -EINVAL;
+
+ dbuf = dma_buf_get(args.dfd);
+ if (IS_ERR(dbuf))
+ return PTR_ERR(dbuf);
+
+ bfdf = fdget(args.bdev_fd);
+ if (fd_empty(bfdf)) {
+ ret = -EBADF;
+ goto out_dmabuf;
+ }
+ bdev_file = fd_file(bfdf);
+ if (!S_ISBLK(file_inode(bdev_file)->i_mode)) {
+ ret = -EINVAL;
+ goto out_fdput;
+ }
+ /* blkdev_init_dma_buf_io_ctx() needs O_DIRECT. */
+ if (!(bdev_file->f_flags & O_DIRECT)) {
+ ret = -EINVAL;
+ goto out_fdput;
+ }
+
+ bdev = file_bdev(bdev_file);
+ if (!bdev->bd_disk->fops->init_dma_buf_io_ctx) {
+ ret = -EOPNOTSUPP;
+ goto out_fdput;
+ }
+
+ ret = -ENOMEM;
+ reg = kzalloc_obj(*reg);
+ if (!reg)
+ goto out_fdput;
+
+ get_file(bdev_file);
+
+ ret = dma_buf_io_ctx_create(bdev_file, dbuf, DMA_BIDIRECTIONAL, &ctx);
+ if (ret) {
+ dev_err(fgds_misc.this_device,
+ "dma-buf-io context on %pg failed: %pe\n",
+ bdev, ERR_PTR(ret));
+ goto out_file;
+ }
+
+ kref_init(®->kref);
+ reg->ctx = ctx;
+ reg->bdev_file = bdev_file;
+ reg->bdev = bdev;
+ reg->size = dbuf->size;
+
+ mutex_lock(&state->lock);
+ ret = xa_alloc(&state->regs, &id, reg, XA_LIMIT(1, UINT_MAX),
+ GFP_KERNEL);
+ if (ret) {
+ mutex_unlock(&state->lock);
+ goto out_ctx;
+ }
+
+ args.handle = id;
+ /* the window must stay inside the buffer and within 32 bits */
+ args.max_doffset = min_t(u64, reg->size, UINT_MAX);
+
+ if (copy_to_user(uarg, &args, sizeof(args))) {
+ xa_erase(&state->regs, id);
+ mutex_unlock(&state->lock);
+ ret = -EFAULT;
+ goto out_ctx;
+ }
+ mutex_unlock(&state->lock);
+
+ fdput(bfdf);
+ dma_buf_put(dbuf);
+ return 0;
+
+out_ctx:
+ dma_buf_io_ctx_release(ctx);
+out_file:
+ fput(bdev_file);
+ kfree(reg);
+out_fdput:
+ fdput(bfdf);
+out_dmabuf:
+ dma_buf_put(dbuf);
+ return ret;
+}
+
+static int fgds_unregister(struct file *file, struct fgds_unregister_args __user *uarg)
+{
+ struct fgds_unregister_args args;
+
+ if (copy_from_user(&args, uarg, sizeof(args)))
+ return -EFAULT;
+ if (args.reserved)
+ return -EINVAL;
+ return fgds_unregister_handle(fgds_state(file), args.handle);
+}
+
+/*
+ * Launch one read_iter()/write_iter() call.
+ * Returns the number of bytes the file system moved, or a negative
+ * errno; a short read means the end of the file.
+ */
+static ssize_t fgds_do_rw(struct fgds_state *state,
+ const struct fgds_rw_args *args, bool write)
+{
+ struct fgds_reg *reg;
+ struct dma_buf_io_map *map;
+ struct file *ffile;
+ struct inode *inode;
+ struct iov_iter iter;
+ struct kiocb kiocb;
+ struct fd fdf;
+ ssize_t (*iter_fn)(struct kiocb *kiocb, struct iov_iter *i);
+ unsigned int file_align, dma_align;
+ u64 length = args->length;
+ u64 dbuf_end;
+ loff_t offset, end;
+ ssize_t ret;
+
+ if (args->flags)
+ return -EINVAL;
+ if (length > MAX_RW_COUNT)
+ return -EINVAL;
+
+ offset = args->offset;
+ if (offset < 0 || check_add_overflow(offset, (loff_t)length, &end))
+ return -EINVAL;
+ /*
+ * The block layer's dma-buf iterator truncates the segment offset to
+ * 32 bits, and the window must stay inside the registered buffer.
+ */
+ if (check_add_overflow(args->doffset, length, &dbuf_end))
+ return -EINVAL;
+ if (dbuf_end > UINT_MAX)
+ return -EINVAL;
+
+ fdf = fdget(args->ffd);
+ if (fd_empty(fdf))
+ return -EBADF;
+ ffile = fd_file(fdf);
+ inode = file_inode(ffile);
+
+ ret = -EINVAL;
+ if (!S_ISREG(inode->i_mode) || (ffile->f_flags & O_APPEND))
+ goto out_fdput;
+
+ if (write) {
+ if (!(ffile->f_mode & FMODE_WRITE) ||
+ !(ffile->f_mode & FMODE_CAN_WRITE))
+ goto out_fdput;
+ iter_fn = ffile->f_op->write_iter;
+ } else {
+ if (!(ffile->f_mode & FMODE_READ) ||
+ !(ffile->f_mode & FMODE_CAN_READ))
+ goto out_fdput;
+ iter_fn = ffile->f_op->read_iter;
+ }
+ if (!iter_fn) {
+ ret = -EOPNOTSUPP;
+ goto out_fdput;
+ }
+
+ ret = fgds_gate(ffile);
+ if (ret)
+ goto out_fdput;
+
+ reg = fgds_reg_get(state, args->handle);
+ if (!reg) {
+ ret = -EINVAL;
+ goto out_fdput;
+ }
+
+ if (inode->i_sb->s_bdev != reg->bdev) {
+ ret = -EXDEV;
+ goto out_reg;
+ }
+
+ file_align = fgds_file_align(inode, write);
+ dma_align = fgds_dma_align(inode);
+ if ((offset | (loff_t)length) & (file_align - 1) ||
+ (args->doffset | (loff_t)length) & (dma_align - 1) ||
+ dbuf_end > reg->size ||
+ (!write && end > i_size_read(inode))) {
+ ret = -EINVAL;
+ goto out_reg;
+ }
+
+ ret = rw_verify_area(write ? WRITE : READ, ffile, &offset, length);
+ if (ret)
+ goto out_reg;
+ if (write)
+ inc_syscw(current);
+ else
+ inc_syscr(current);
+
+ if (!length) {
+ ret = 0;
+ goto out_reg;
+ }
+
+ map = dma_buf_io_get_map(reg->ctx, false);
+ if (IS_ERR(map)) {
+ ret = PTR_ERR(map);
+ if (ret != -EAGAIN)
+ dev_err_ratelimited(fgds_misc.this_device,
+ "dma-buf mapping failed on %pg: %pe\n",
+ reg->bdev, ERR_PTR(ret));
+ goto out_reg;
+ }
+
+ /*
+ * A direct I/O read would leave any cached pages of the range
+ * stale, so flush and drop them before submitting.
+ */
+ if (!write) {
+ ret = filemap_write_and_wait_range(ffile->f_mapping,
+ offset, end - 1);
+ if (ret)
+ goto out_map;
+ ret = invalidate_inode_pages2_range(ffile->f_mapping,
+ offset >> PAGE_SHIFT,
+ (end - 1) >> PAGE_SHIFT);
+ if (ret) {
+ /*
+ * Report a retryable error instead of letting
+ * -EBUSY escape.
+ */
+ if (ret == -EBUSY)
+ ret = -EAGAIN;
+ goto out_map;
+ }
+ }
+
+ if (write)
+ file_start_write(ffile);
+ iov_iter_dmabuf_map(&iter, write ? ITER_SOURCE : ITER_DEST,
+ map, args->doffset, length);
+ init_sync_kiocb(&kiocb, ffile);
+ kiocb.ki_flags |= IOCB_DIRECT;
+ if (write)
+ kiocb.ki_flags |= IOCB_WRITE;
+ kiocb.ki_pos = offset;
+
+ ret = iter_fn(&kiocb, &iter);
+ if (WARN_ON_ONCE(ret == -EIOCBQUEUED))
+ ret = -EIO;
+ if (write)
+ file_end_write(ffile);
+
+ if (ret > 0) {
+ if (write) {
+ fsnotify_modify(ffile);
+ add_wchar(current, ret);
+ } else {
+ fsnotify_access(ffile);
+ add_rchar(current, ret);
+ }
+ }
+
+out_map:
+ dma_buf_io_map_drop(map);
+out_reg:
+ fgds_reg_put(reg);
+out_fdput:
+ fdput(fdf);
+ return ret;
+}
+
+static long fgds_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
+{
+ struct fgds_state *state = fgds_state(file);
+ struct fgds_rw_args args;
+
+ switch (cmd) {
+ case FGDS_IOCTL_WRITE:
+ case FGDS_IOCTL_READ:
+ if (copy_from_user(&args, (void __user *)arg, sizeof(args)))
+ return -EFAULT;
+ return fgds_do_rw(state, &args, cmd == FGDS_IOCTL_WRITE);
+ case FGDS_IOCTL_REGISTER_BUF:
+ return fgds_register(file, (struct fgds_register_args __user *)arg);
+ case FGDS_IOCTL_UNREGISTER_BUF:
+ return fgds_unregister(file, (struct fgds_unregister_args __user *)arg);
+ }
+ return -ENOTTY;
+}
+
+static int fgds_open(struct inode *inode, struct file *file)
+{
+ struct fgds_state *state;
+
+ state = kzalloc_obj(*state);
+ if (!state)
+ return -ENOMEM;
+
+ xa_init_flags(&state->regs, XA_FLAGS_ALLOC);
+ mutex_init(&state->lock);
+ file->private_data = state;
+ return 0;
+}
+
+static int fgds_release(struct inode *inode, struct file *file)
+{
+ struct fgds_state *state = fgds_state(file);
+ struct fgds_reg *reg;
+ unsigned long id;
+
+ xa_for_each(&state->regs, id, reg) {
+ xa_erase(&state->regs, id);
+ fgds_reg_put(reg);
+ }
+ xa_destroy(&state->regs);
+ mutex_destroy(&state->lock);
+ kfree(state);
+ return 0;
+}
+
+static const struct file_operations fgds_fops = {
+ .owner = THIS_MODULE,
+ .open = fgds_open,
+ .release = fgds_release,
+ .unlocked_ioctl = fgds_ioctl,
+ .compat_ioctl = compat_ptr_ioctl,
+};
+
+static struct miscdevice fgds_misc = {
+ .minor = MISC_DYNAMIC_MINOR,
+ .name = "fgds",
+ .fops = &fgds_fops,
+};
+
+static int __init fgds_init(void)
+{
+ return misc_register(&fgds_misc);
+}
+
+static void __exit fgds_exit(void)
+{
+ misc_deregister(&fgds_misc);
+}
+
+module_init(fgds_init);
+module_exit(fgds_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("DMA_BUF");
+MODULE_AUTHOR("Mengmeng Zhao <zhaomengmeng@kylinos.cn>, Li Wang <liwang@kylinos.cn>");
+MODULE_DESCRIPTION("Fast GPU Direct Storage");
+MODULE_VERSION("1.0.0");
diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
index 0a5ebfda90f1..a7ab28edab98 100644
--- a/fs/iomap/buffered-io.c
+++ b/fs/iomap/buffered-io.c
@@ -1304,6 +1304,17 @@ iomap_file_buffered_write(struct kiocb *iocb, struct iov_iter *i,
};
ssize_t ret;
+ /*
+ * The copy helpers have no source for a dma-buf iterator: the copy
+ * would move zero bytes, ->write_end() would reject the untouched
+ * folio, fault_in_iov_iter_readable() reports the iterator as
+ * readable, and the loop below would never advance. Refuse here
+ * rather than at each caller's entry point, so no path can reach
+ * the copy with such an iterator.
+ */
+ if (iov_iter_is_dmabuf_map(i))
+ return -EOPNOTSUPP;
+
if (iocb->ki_flags & IOCB_NOWAIT)
iter.flags |= IOMAP_NOWAIT;
if (iocb->ki_flags & IOCB_DONTCACHE)
diff --git a/fs/iomap/direct-io.c b/fs/iomap/direct-io.c
index 8b4039d16ce8..eccc07d53e9c 100644
--- a/fs/iomap/direct-io.c
+++ b/fs/iomap/direct-io.c
@@ -595,8 +595,16 @@ static int iomap_dio_bio_iter(struct iomap_iter *iter, struct iomap_dio *dio)
static int iomap_dio_hole_iter(struct iomap_iter *iter, struct iomap_dio *dio)
{
- loff_t length = iov_iter_zero(iomap_length(iter), dio->submit.iter);
+ loff_t length;
+ /*
+ * dmabuf maps do not support zero-filling holes or
+ * unwritten extents.
+ */
+ if (iov_iter_is_dmabuf_map(dio->submit.iter))
+ return -EOPNOTSUPP;
+
+ length = iov_iter_zero(iomap_length(iter), dio->submit.iter);
dio->size += length;
if (!length)
return -EFAULT;
@@ -683,8 +691,8 @@ static int iomap_dio_iter(struct iomap_iter *iter, struct iomap_dio *dio)
* the correct total number of bytes transferred; this is essential for
* completing partial requests asynchronously.
*
- * Returns -ENOTBLK In case of a page invalidation invalidation failure for
- * writes. The callers needs to fall back to buffered I/O in this case.
+ * Returns -ENOTBLK in case of a page cache invalidation failure for writes.
+ * The caller needs to fall back to buffered I/O in this case.
*/
struct iomap_dio *
__iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
@@ -794,6 +802,15 @@ __iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
* the caller tries again.
*/
ret = -EAGAIN;
+ } else if (iov_iter_is_dmabuf_map(iter)) {
+ /*
+ * Avoid -ENOTBLK (buffered fallback)
+ * as dmabuf are not supported. Map
+ * -EBUSY to -EAGAIN for transient
+ * retires.
+ */
+ if (ret == -EBUSY)
+ ret = -EAGAIN;
} else {
/* fall back to buffered write */
ret = -ENOTBLK;
diff --git a/fs/xfs/xfs_file.c b/fs/xfs/xfs_file.c
index 426a67b813a7..f16e9cb63e9e 100644
--- a/fs/xfs/xfs_file.c
+++ b/fs/xfs/xfs_file.c
@@ -1211,6 +1211,10 @@ xfs_file_write_iter(
return ret;
}
+ /* zoned and buffered I/O is not supported for dmabuf */
+ if (iov_iter_is_dmabuf_map(from))
+ return -EOPNOTSUPP;
+
if (xfs_is_zoned_inode(ip))
return xfs_file_buffered_write_zoned(iocb, from);
return xfs_file_buffered_write(iocb, from);
diff --git a/include/linux/iov_iter.h b/include/linux/iov_iter.h
index 2f91884d733b..a3b99e6d8255 100644
--- a/include/linux/iov_iter.h
+++ b/include/linux/iov_iter.h
@@ -376,6 +376,10 @@ size_t iterate_and_advance_kernel(struct iov_iter *iter, size_t len, void *priv,
return iterate_folioq(iter, len, priv, priv2, step);
if (iov_iter_is_xarray(iter))
return iterate_xarray(iter, len, priv, priv2, step);
+ if (iov_iter_is_dmabuf_map(iter)) {
+ WARN_ON_ONCE(1);
+ return 0;
+ }
return iterate_discard(iter, len, priv, priv2, step);
}
diff --git a/include/uapi/linux/fgds.h b/include/uapi/linux/fgds.h
new file mode 100644
index 000000000000..9a721c92cde2
--- /dev/null
+++ b/include/uapi/linux/fgds.h
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * Fast GPU Direct Storage user-kernel ABI definitions.
+ *
+ * Copyright (C) 2026 KylinSoft. Co., Ltd. All rights reserved.
+ *
+ * Synchronous read/write ioctls that move data between a GPU dma-buf and
+ * a regular file.
+ */
+#ifndef _FGDS_H
+#define _FGDS_H
+
+#include <linux/types.h>
+#include <linux/ioctl.h>
+
+/**
+ * struct fgds_register_args - arguments for %FGDS_IOCTL_REGISTER_BUF
+ *
+ * @dfd: dma-buf file descriptor exported by the GPU runtime.
+ * @bdev_fd: file descriptor of the underlying block device (the
+ * device the target files live on), opened with %O_DIRECT.
+ * The device is named separately from the file because:
+ * building the dma-buf I/O context needs a file for the
+ * device itself, and the device side requires %O_DIRECT
+ * on that file, which a regular file descriptor cannot
+ * provide.
+ * @handle: Returned handle for use with the other ioctls. Handles
+ * are private to the /dev/fgds file descriptor they were
+ * created on and are released automatically when that
+ * descriptor is closed. Output.
+ * @pad: Must be 0.
+ * @max_doffset: Largest accepted value of @doffset plus @length: the
+ * window must stay inside the registered dma-buf and
+ * within the 32-bit offset bound of the block-layer
+ * dma-buf iterator, so this reports the smaller of the
+ * dma-buf size and %UINT_MAX. Output.
+ * @reserved: Must be 0. Extension slot for further output fields.
+ *
+ * A registered handle keeps the device attachment alive, and once a
+ * transfer has mapped the buffer that mapping is held for the handle's
+ * lifetime.
+ *
+ * Returns 0, or a negative errno.
+ */
+struct fgds_register_args {
+ __s32 dfd;
+ __s32 bdev_fd;
+ __u32 handle;
+ __u32 pad;
+ __u64 max_doffset;
+ __u64 reserved;
+};
+
+/**
+ * struct fgds_unregister_args - arguments for %FGDS_IOCTL_UNREGISTER_BUF
+ *
+ * @handle: Handle returned by %FGDS_IOCTL_REGISTER_BUF. Input.
+ * @reserved: Must be 0.
+ *
+ * Returns 0, or -EINVAL if @handle is not registered on this file
+ * descriptor.
+ */
+struct fgds_unregister_args {
+ __u32 handle;
+ __u32 reserved;
+};
+
+/**
+ * struct fgds_rw_args - arguments for %FGDS_IOCTL_WRITE / %FGDS_IOCTL_READ
+ *
+ * @handle: Handle from %FGDS_IOCTL_REGISTER_BUF naming both the
+ * dma-buf and the block device to transfer between. Input.
+ * @ffd: Regular file descriptor. %O_DIRECT is not required on
+ * it, fgds forces the direct I/O path itself. Its file
+ * system's device must match the one registered with
+ * @handle or the transfer fails with %EXDEV. Input.
+ * @offset: Byte offset inside the file. Input.
+ * @length: Transfer length in bytes. Unlike pread() and pwrite(),
+ * which silently truncate an oversized request, fgds rejects
+ * it with %EINVAL. Input.
+ * @doffset: Byte offset inside the dma-buf, i.e. the start of the
+ * window the transfer reads from or writes to. Input.
+ * @flags: Must be 0. Input.
+ *
+ * Like pread() and pwrite(), the ioctl returns the number of bytes
+ * transferred or a negative errno. A return value below @length means a
+ * read stopped at the end of the file, or a write clamped by the calling
+ * process' RLIMIT_FSIZE.
+ *
+ * Contract and failure modes:
+ *
+ * - Alignment is enforced on two independent axes, both answered by
+ * statx() with %STATX_DIOALIGN. The file side applies to @offset and
+ * @length: the device logical block size for a read, the max of that
+ * and the file system block size for a write. The buffer side applies
+ * to @doffset and @length: the device DMA alignment. A transfer must
+ * satisfy both axes at once.
+ * - Ranges backed by file holes or unwritten extents are not supported
+ * for reads and fail with %EOPNOTSUPP: there is no page cache bounce
+ * to serve zeros from, and GPU memory is generally not CPU accessible.
+ * - Writes past the end of the file go through the file system's own
+ * direct I/O extend path; no preallocation is performed by fgds. An
+ * application that wants the space reserved must call fallocate()
+ * itself first.
+ * - Reads flush and drop any page cache of the transferred range before
+ * submitting, so subsequent buffered reads observe the fresh data.
+ */
+struct fgds_rw_args {
+ __u32 handle;
+ __s32 ffd;
+ __u64 offset;
+ __u64 length;
+ __u64 doffset;
+ __u64 flags;
+};
+
+#define FGDS_IOCTL_MAGIC 'x'
+
+#define FGDS_IOCTL_WRITE _IOW(FGDS_IOCTL_MAGIC, 1, struct fgds_rw_args)
+#define FGDS_IOCTL_READ _IOW(FGDS_IOCTL_MAGIC, 2, struct fgds_rw_args)
+#define FGDS_IOCTL_REGISTER_BUF _IOWR(FGDS_IOCTL_MAGIC, 3, struct fgds_register_args)
+#define FGDS_IOCTL_UNREGISTER_BUF _IOW(FGDS_IOCTL_MAGIC, 4, struct fgds_unregister_args)
+
+#endif /* _FGDS_H */
diff --git a/lib/iov_iter.c b/lib/iov_iter.c
index 9b46f9ada8f9..b19bf7f5e4cf 100644
--- a/lib/iov_iter.c
+++ b/lib/iov_iter.c
@@ -790,6 +790,7 @@ void iov_iter_dmabuf_map(struct iov_iter *i, unsigned int direction,
.iov_offset = off,
};
}
+EXPORT_SYMBOL_GPL(iov_iter_dmabuf_map);
/**
* iov_iter_discard - Initialise an I/O iterator that discards data
diff --git a/mm/filemap.c b/mm/filemap.c
index 6afec636881f..ab8a5391a699 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -3013,6 +3013,14 @@ generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
return retval;
}
+ /*
+ * Buffered I/O read is not supported for dmabuf; a short direct
+ * read before this point stands on its own, as in
+ * direct_write_fallback().
+ */
+ if (iov_iter_is_dmabuf_map(iter))
+ return retval > 0 ? retval : -EOPNOTSUPP;
+
return filemap_read(iocb, iter, retval);
}
EXPORT_SYMBOL(generic_file_read_iter);
@@ -4300,8 +4308,14 @@ generic_file_direct_write(struct kiocb *iocb, struct iov_iter *from)
*/
written = kiocb_invalidate_pages(iocb, write_len);
if (written) {
- if (written == -EBUSY)
+ if (written == -EBUSY) {
+ /*
+ * Avoid fallback to buffered write for dmabuf.
+ */
+ if (iov_iter_is_dmabuf_map(from))
+ return -EAGAIN;
return 0;
+ }
return written;
}
@@ -4353,6 +4367,12 @@ ssize_t generic_perform_write(struct kiocb *iocb, struct iov_iter *i)
long status = 0;
ssize_t written = 0;
+ /*
+ * Buffered write is not supported for dmabuf.
+ */
+ if (iov_iter_is_dmabuf_map(i))
+ return -EOPNOTSUPP;
+
do {
struct folio *folio;
size_t offset; /* Offset into folio */
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH v2 1/1] misc: fgds: introduce GPU-File direct I/O via dma-buf
2026-10-27 9:08 ` [RFC PATCH v2 1/1] " Li Wang
@ 2026-10-27 13:03 ` Jens Axboe
2026-10-27 13:12 ` Greg Kroah-Hartman
2026-10-27 13:12 ` Christian König
1 sibling, 1 reply; 14+ messages in thread
From: Jens Axboe @ 2026-10-27 13:03 UTC (permalink / raw)
To: Li Wang, Arnd Bergmann, Greg Kroah-Hartman
Cc: Sumit Semwal, Christian König, Pavel Begunkov, linux-media,
dri-devel, linaro-mm-sig, io-uring, linux-kernel, Mengmeng Zhao
On 10/27/26 3:08 AM, Li Wang wrote:
> From: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
>
> This patch introduces a driver that allows moving data between a GPU
> dma-buf and a regular file on NVME without staging it through the page
> cache. Userspace registers the GPU dma-buf with the device, then issues
> read and write requests via the device's ioctls. Each transfer is direct
> I/O: an ITER_DMABUF_MAP iterator is passed to the file's read_iter or
> write_iter, and the filesystem's iomap path submits REQ_DMABUF bios that
> already carry the dma addresses.
>
> Signed-off-by: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
> Signed-off-by: Li Wang <liwang@kylinos.cn>
> ---
> .../userspace-api/ioctl/ioctl-number.rst | 2 +
> drivers/dma-buf/dma-buf-io.c | 3 +
> drivers/misc/Kconfig | 16 +
> drivers/misc/Makefile | 1 +
> drivers/misc/fgds.c | 506 ++++++++++++++++++
> fs/iomap/buffered-io.c | 11 +
> fs/iomap/direct-io.c | 23 +-
> fs/xfs/xfs_file.c | 4 +
> include/linux/iov_iter.h | 4 +
> include/uapi/linux/fgds.h | 124 +++++
> lib/iov_iter.c | 1 +
> mm/filemap.c | 22 +-
> 12 files changed, 713 insertions(+), 4 deletions(-)
> create mode 100644 drivers/misc/fgds.c
> create mode 100644 include/uapi/linux/fgds.h
What's the point of this, when you can just use io_uring to do
the same?!
--
Jens Axboe
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH v2 1/1] misc: fgds: introduce GPU-File direct I/O via dma-buf
2026-10-27 13:03 ` Jens Axboe
@ 2026-10-27 13:12 ` Greg Kroah-Hartman
0 siblings, 0 replies; 14+ messages in thread
From: Greg Kroah-Hartman @ 2026-10-27 13:12 UTC (permalink / raw)
To: Jens Axboe
Cc: Li Wang, Arnd Bergmann, Sumit Semwal, Christian König,
Pavel Begunkov, linux-media, dri-devel, linaro-mm-sig, io-uring,
linux-kernel, Mengmeng Zhao
On Tue, Oct 27, 2026 at 07:03:29AM -0600, Jens Axboe wrote:
> On 10/27/26 3:08 AM, Li Wang wrote:
> > From: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
> >
> > This patch introduces a driver that allows moving data between a GPU
> > dma-buf and a regular file on NVME without staging it through the page
> > cache. Userspace registers the GPU dma-buf with the device, then issues
> > read and write requests via the device's ioctls. Each transfer is direct
> > I/O: an ITER_DMABUF_MAP iterator is passed to the file's read_iter or
> > write_iter, and the filesystem's iomap path submits REQ_DMABUF bios that
> > already carry the dma addresses.
> >
> > Signed-off-by: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
> > Signed-off-by: Li Wang <liwang@kylinos.cn>
> > ---
> > .../userspace-api/ioctl/ioctl-number.rst | 2 +
> > drivers/dma-buf/dma-buf-io.c | 3 +
> > drivers/misc/Kconfig | 16 +
> > drivers/misc/Makefile | 1 +
> > drivers/misc/fgds.c | 506 ++++++++++++++++++
> > fs/iomap/buffered-io.c | 11 +
> > fs/iomap/direct-io.c | 23 +-
> > fs/xfs/xfs_file.c | 4 +
> > include/linux/iov_iter.h | 4 +
> > include/uapi/linux/fgds.h | 124 +++++
> > lib/iov_iter.c | 1 +
> > mm/filemap.c | 22 +-
> > 12 files changed, 713 insertions(+), 4 deletions(-)
> > create mode 100644 drivers/misc/fgds.c
> > create mode 100644 include/uapi/linux/fgds.h
>
> What's the point of this, when you can just use io_uring to do
> the same?!
Agreed, I thought that the summary of the last round of reviews was
"this isn't needed at all". Li, what changed from that?
thanks,
greg k-h
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH v2 1/1] misc: fgds: introduce GPU-File direct I/O via dma-buf
2026-10-27 9:08 ` [RFC PATCH v2 1/1] " Li Wang
2026-10-27 13:03 ` Jens Axboe
@ 2026-10-27 13:12 ` Christian König
1 sibling, 0 replies; 14+ messages in thread
From: Christian König @ 2026-10-27 13:12 UTC (permalink / raw)
To: Li Wang, Arnd Bergmann, Greg Kroah-Hartman
Cc: Sumit Semwal, Pavel Begunkov, linux-media, dri-devel,
linaro-mm-sig, io-uring, linux-kernel, Mengmeng Zhao
On 10/27/26 10:08, Li Wang wrote:
> From: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
>
> This patch introduces a driver that allows moving data between a GPU
> dma-buf and a regular file on NVME without staging it through the page
> cache. Userspace registers the GPU dma-buf with the device, then issues
> read and write requests via the device's ioctls. Each transfer is direct
> I/O: an ITER_DMABUF_MAP iterator is passed to the file's read_iter or
> write_iter, and the filesystem's iomap path submits REQ_DMABUF bios that
> already carry the dma addresses.
Why in the world is that still useful with Pavel work on io_uring?
>
> Signed-off-by: Mengmeng Zhao <zhaomengmeng@kylinos.cn>
> Signed-off-by: Li Wang <liwang@kylinos.cn>
> ---
> .../userspace-api/ioctl/ioctl-number.rst | 2 +
> drivers/dma-buf/dma-buf-io.c | 3 +
> drivers/misc/Kconfig | 16 +
> drivers/misc/Makefile | 1 +
> drivers/misc/fgds.c | 506 ++++++++++++++++++
> fs/iomap/buffered-io.c | 11 +
> fs/iomap/direct-io.c | 23 +-
> fs/xfs/xfs_file.c | 4 +
> include/linux/iov_iter.h | 4 +
> include/uapi/linux/fgds.h | 124 +++++
> lib/iov_iter.c | 1 +
> mm/filemap.c | 22 +-
> 12 files changed, 713 insertions(+), 4 deletions(-)
> create mode 100644 drivers/misc/fgds.c
> create mode 100644 include/uapi/linux/fgds.h
>
> diff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst
> index 2fc53093752d..5f93a252e773 100644
> --- a/Documentation/userspace-api/ioctl/ioctl-number.rst
> +++ b/Documentation/userspace-api/ioctl/ioctl-number.rst
> @@ -306,6 +306,8 @@ Code Seq# Include File Comments
> 'v' 20-27 arch/powerpc/include/uapi/asm/vas-api.h VAS API
> 'v' C0-FF linux/meye.h conflict!
> 'w' all CERN SCI driver
> +'x' 00-0F linux/fgds.h Fast GPU Direct Storage
> + <mailto:liwang@kylinos.cn>
> 'y' 00-1F packet based user level communications
> <mailto:zapman@interlan.net>
> 'z' 00-3F CAN bus card conflict!
> diff --git a/drivers/dma-buf/dma-buf-io.c b/drivers/dma-buf/dma-buf-io.c
> index 8440151de860..2ee4fc683c74 100644
> --- a/drivers/dma-buf/dma-buf-io.c
> +++ b/drivers/dma-buf/dma-buf-io.c
> @@ -135,6 +135,7 @@ struct dma_buf_io_map *dma_buf_io_create_map(struct dma_buf_io_ctx *ctx)
> dma_resv_unlock(ctx->dmabuf->resv);
> return map;
> }
> +EXPORT_SYMBOL_NS_GPL(dma_buf_io_create_map, "DMA_BUF");
Clear NAK to that. Those are FS layer helper functionalities for DMA-buf and not something drivers should ever need to mess with.
Regards,
Christian.
>
> static void dma_buf_io_ctx_shutdown(struct dma_buf_io_ctx *ctx)
> {
> @@ -190,6 +191,7 @@ int dma_buf_io_ctx_create(struct file *file,
> *out_ctx = ctx;
> return 0;
> }
> +EXPORT_SYMBOL_NS_GPL(dma_buf_io_ctx_create, "DMA_BUF");
>
> /**
> * dma_buf_io_ctx_release - Destroy the I/O context
> @@ -211,6 +213,7 @@ void dma_buf_io_ctx_release(struct dma_buf_io_ctx *ctx)
> dma_buf_put(ctx->dmabuf);
> kfree(ctx);
> }
> +EXPORT_SYMBOL_NS_GPL(dma_buf_io_ctx_release, "DMA_BUF");
>
> /**
> * dma_buf_io_invalidate_mappings - Invalidate the current map
> diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
> index 7364931dad3a..b85fd64d27ad 100644
> --- a/drivers/misc/Kconfig
> +++ b/drivers/misc/Kconfig
> @@ -568,6 +568,22 @@ config MCHP_LAN966X_PCI
> - lan966x-miim (MDIO_MSCC_MIIM)
> - lan966x-switch (LAN966X_SWITCH)
>
> +config FGDS
> + tristate "Fast GPU Direct Storage support"
> + depends on DMA_SHARED_BUFFER && BLOCK
> + default n
> + help
> + This driver exposes a misc device that moves data between a GPU
> + dma-buf and a regular file with synchronous read/write ioctls,
> + submitting the transfer as direct I/O (REQ_DMABUF bios) through
> + the file system's iomap DIO path without a page cache bounce
> + buffer.
> +
> + To compile this driver as a module, choose M here: the module
> + will be called fgds.
> +
> + If unsure, say N.
> +
> source "drivers/misc/c2port/Kconfig"
> source "drivers/misc/eeprom/Kconfig"
> source "drivers/misc/cb710/Kconfig"
> diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
> index e8d8d5d88c0d..cd98ef3d008e 100644
> --- a/drivers/misc/Makefile
> +++ b/drivers/misc/Makefile
> @@ -70,4 +70,5 @@ obj-$(CONFIG_MCHP_LAN966X_PCI) += lan966x-pci.o
> obj-y += keba/
> obj-y += amd-sbi/
> obj-$(CONFIG_MISC_RP1) += rp1/
> +obj-$(CONFIG_FGDS) += fgds.o
> obj-$(CONFIG_INTEL_SSEI) += issei/
> diff --git a/drivers/misc/fgds.c b/drivers/misc/fgds.c
> new file mode 100644
> index 000000000000..3929a6514d23
> --- /dev/null
> +++ b/drivers/misc/fgds.c
> @@ -0,0 +1,506 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Fast GPU Direct Storage via dma-buf.
> + *
> + * Copyright (C) 2026 KylinSoft. Co., Ltd. All rights reserved.
> + *
> + * Synchronous read/write ioctls that move data between a GPU dma-buf and
> + * a regular file.
> + */
> +
> +#include <linux/blkdev.h>
> +#include <linux/dma-buf.h>
> +#include <linux/dma-buf-io.h>
> +#include <linux/device.h>
> +#include <linux/fcntl.h>
> +#include <linux/fileattr.h>
> +#include <linux/fs.h>
> +#include <linux/fsnotify.h>
> +#include <linux/magic.h>
> +#include <linux/math.h>
> +#include <linux/miscdevice.h>
> +#include <linux/module.h>
> +#include <linux/overflow.h>
> +#include <linux/pagemap.h>
> +#include <linux/sched/xacct.h>
> +#include <linux/slab.h>
> +#include <linux/uaccess.h>
> +#include <linux/uio.h>
> +#include <linux/xarray.h>
> +
> +#include <linux/fgds.h>
> +
> +static struct miscdevice fgds_misc;
> +
> +/* One registered buffer: a dma-buf bound to a block device. */
> +struct fgds_reg {
> + struct kref kref;
> + struct dma_buf_io_ctx *ctx;
> + struct file *bdev_file;
> + struct block_device *bdev;
> + u64 size;
> +};
> +
> +/* State of one /dev/fgds open file description; handles are private to it. */
> +struct fgds_state {
> + struct xarray regs;
> + /* protects @regs and, with the reg kref, the lifetime of entries */
> + struct mutex lock;
> +};
> +
> +static void fgds_reg_release(struct kref *kref)
> +{
> + struct fgds_reg *reg = container_of(kref, struct fgds_reg, kref);
> +
> + dma_buf_io_ctx_release(reg->ctx);
> + fput(reg->bdev_file);
> + kfree(reg);
> +}
> +
> +static void fgds_reg_put(struct fgds_reg *reg)
> +{
> + kref_put(®->kref, fgds_reg_release);
> +}
> +
> +static struct fgds_state *fgds_state(struct file *file)
> +{
> + return file->private_data;
> +}
> +
> +/*
> + * File and file-system level gate.
> + */
> +static int fgds_gate(struct file *ffile)
> +{
> + struct inode *inode = file_inode(ffile);
> + struct super_block *sb = inode->i_sb;
> + struct file_kattr fa;
> +
> + if (!S_ISREG(inode->i_mode))
> + return -EINVAL;
> + if (sb->s_magic != XFS_SUPER_MAGIC)
> + return -EOPNOTSUPP;
> + if (IS_DAX(inode))
> + return -EOPNOTSUPP;
> + if (mapping_stable_writes(inode->i_mapping))
> + return -EOPNOTSUPP;
> + if (!sb->s_bdev)
> + return -EOPNOTSUPP;
> + if (bdev_is_zoned(sb->s_bdev))
> + return -EOPNOTSUPP;
> + if (!sb->s_bdev->bd_disk->fops->init_dma_buf_io_ctx)
> + return -EOPNOTSUPP;
> +
> + /*
> + * Realtime inodes live on a separate device whose DMA setup is not
> + * covered here.
> + */
> + if (vfs_fileattr_get(ffile->f_path.dentry, &fa))
> + return -EOPNOTSUPP;
> + if (fa.fsx_xflags & FS_XFLAG_REALTIME)
> + return -EOPNOTSUPP;
> + return 0;
> +}
> +
> +static unsigned int fgds_file_align(struct inode *inode, bool write)
> +{
> + unsigned int lbs = bdev_logical_block_size(inode->i_sb->s_bdev);
> +
> + if (!write)
> + return lbs;
> + return max(1U << inode->i_blkbits, lbs);
> +}
> +
> +static unsigned int fgds_dma_align(struct inode *inode)
> +{
> + return bdev_dma_alignment(inode->i_sb->s_bdev) + 1;
> +}
> +
> +static struct fgds_reg *fgds_reg_get(struct fgds_state *state, u32 handle)
> +{
> + struct fgds_reg *reg;
> +
> + mutex_lock(&state->lock);
> + reg = xa_load(&state->regs, handle);
> + if (reg)
> + kref_get(®->kref);
> + mutex_unlock(&state->lock);
> + return reg;
> +}
> +
> +static int fgds_unregister_handle(struct fgds_state *state, u32 handle)
> +{
> + struct fgds_reg *reg;
> +
> + mutex_lock(&state->lock);
> + reg = xa_erase(&state->regs, handle);
> + mutex_unlock(&state->lock);
> + if (reg)
> + fgds_reg_put(reg);
> + return reg ? 0 : -EINVAL;
> +}
> +
> +static int fgds_register(struct file *file, struct fgds_register_args __user *uarg)
> +{
> + struct fgds_register_args args;
> + struct fgds_state *state = fgds_state(file);
> + struct dma_buf_io_ctx *ctx = NULL;
> + struct block_device *bdev;
> + struct file *bdev_file;
> + struct dma_buf *dbuf;
> + struct fd bfdf;
> + struct fgds_reg *reg;
> + u32 id;
> + int ret;
> +
> + if (copy_from_user(&args, uarg, sizeof(args)))
> + return -EFAULT;
> + if (args.handle || args.pad || args.reserved)
> + return -EINVAL;
> +
> + dbuf = dma_buf_get(args.dfd);
> + if (IS_ERR(dbuf))
> + return PTR_ERR(dbuf);
> +
> + bfdf = fdget(args.bdev_fd);
> + if (fd_empty(bfdf)) {
> + ret = -EBADF;
> + goto out_dmabuf;
> + }
> + bdev_file = fd_file(bfdf);
> + if (!S_ISBLK(file_inode(bdev_file)->i_mode)) {
> + ret = -EINVAL;
> + goto out_fdput;
> + }
> + /* blkdev_init_dma_buf_io_ctx() needs O_DIRECT. */
> + if (!(bdev_file->f_flags & O_DIRECT)) {
> + ret = -EINVAL;
> + goto out_fdput;
> + }
> +
> + bdev = file_bdev(bdev_file);
> + if (!bdev->bd_disk->fops->init_dma_buf_io_ctx) {
> + ret = -EOPNOTSUPP;
> + goto out_fdput;
> + }
> +
> + ret = -ENOMEM;
> + reg = kzalloc_obj(*reg);
> + if (!reg)
> + goto out_fdput;
> +
> + get_file(bdev_file);
> +
> + ret = dma_buf_io_ctx_create(bdev_file, dbuf, DMA_BIDIRECTIONAL, &ctx);
> + if (ret) {
> + dev_err(fgds_misc.this_device,
> + "dma-buf-io context on %pg failed: %pe\n",
> + bdev, ERR_PTR(ret));
> + goto out_file;
> + }
> +
> + kref_init(®->kref);
> + reg->ctx = ctx;
> + reg->bdev_file = bdev_file;
> + reg->bdev = bdev;
> + reg->size = dbuf->size;
> +
> + mutex_lock(&state->lock);
> + ret = xa_alloc(&state->regs, &id, reg, XA_LIMIT(1, UINT_MAX),
> + GFP_KERNEL);
> + if (ret) {
> + mutex_unlock(&state->lock);
> + goto out_ctx;
> + }
> +
> + args.handle = id;
> + /* the window must stay inside the buffer and within 32 bits */
> + args.max_doffset = min_t(u64, reg->size, UINT_MAX);
> +
> + if (copy_to_user(uarg, &args, sizeof(args))) {
> + xa_erase(&state->regs, id);
> + mutex_unlock(&state->lock);
> + ret = -EFAULT;
> + goto out_ctx;
> + }
> + mutex_unlock(&state->lock);
> +
> + fdput(bfdf);
> + dma_buf_put(dbuf);
> + return 0;
> +
> +out_ctx:
> + dma_buf_io_ctx_release(ctx);
> +out_file:
> + fput(bdev_file);
> + kfree(reg);
> +out_fdput:
> + fdput(bfdf);
> +out_dmabuf:
> + dma_buf_put(dbuf);
> + return ret;
> +}
> +
> +static int fgds_unregister(struct file *file, struct fgds_unregister_args __user *uarg)
> +{
> + struct fgds_unregister_args args;
> +
> + if (copy_from_user(&args, uarg, sizeof(args)))
> + return -EFAULT;
> + if (args.reserved)
> + return -EINVAL;
> + return fgds_unregister_handle(fgds_state(file), args.handle);
> +}
> +
> +/*
> + * Launch one read_iter()/write_iter() call.
> + * Returns the number of bytes the file system moved, or a negative
> + * errno; a short read means the end of the file.
> + */
> +static ssize_t fgds_do_rw(struct fgds_state *state,
> + const struct fgds_rw_args *args, bool write)
> +{
> + struct fgds_reg *reg;
> + struct dma_buf_io_map *map;
> + struct file *ffile;
> + struct inode *inode;
> + struct iov_iter iter;
> + struct kiocb kiocb;
> + struct fd fdf;
> + ssize_t (*iter_fn)(struct kiocb *kiocb, struct iov_iter *i);
> + unsigned int file_align, dma_align;
> + u64 length = args->length;
> + u64 dbuf_end;
> + loff_t offset, end;
> + ssize_t ret;
> +
> + if (args->flags)
> + return -EINVAL;
> + if (length > MAX_RW_COUNT)
> + return -EINVAL;
> +
> + offset = args->offset;
> + if (offset < 0 || check_add_overflow(offset, (loff_t)length, &end))
> + return -EINVAL;
> + /*
> + * The block layer's dma-buf iterator truncates the segment offset to
> + * 32 bits, and the window must stay inside the registered buffer.
> + */
> + if (check_add_overflow(args->doffset, length, &dbuf_end))
> + return -EINVAL;
> + if (dbuf_end > UINT_MAX)
> + return -EINVAL;
> +
> + fdf = fdget(args->ffd);
> + if (fd_empty(fdf))
> + return -EBADF;
> + ffile = fd_file(fdf);
> + inode = file_inode(ffile);
> +
> + ret = -EINVAL;
> + if (!S_ISREG(inode->i_mode) || (ffile->f_flags & O_APPEND))
> + goto out_fdput;
> +
> + if (write) {
> + if (!(ffile->f_mode & FMODE_WRITE) ||
> + !(ffile->f_mode & FMODE_CAN_WRITE))
> + goto out_fdput;
> + iter_fn = ffile->f_op->write_iter;
> + } else {
> + if (!(ffile->f_mode & FMODE_READ) ||
> + !(ffile->f_mode & FMODE_CAN_READ))
> + goto out_fdput;
> + iter_fn = ffile->f_op->read_iter;
> + }
> + if (!iter_fn) {
> + ret = -EOPNOTSUPP;
> + goto out_fdput;
> + }
> +
> + ret = fgds_gate(ffile);
> + if (ret)
> + goto out_fdput;
> +
> + reg = fgds_reg_get(state, args->handle);
> + if (!reg) {
> + ret = -EINVAL;
> + goto out_fdput;
> + }
> +
> + if (inode->i_sb->s_bdev != reg->bdev) {
> + ret = -EXDEV;
> + goto out_reg;
> + }
> +
> + file_align = fgds_file_align(inode, write);
> + dma_align = fgds_dma_align(inode);
> + if ((offset | (loff_t)length) & (file_align - 1) ||
> + (args->doffset | (loff_t)length) & (dma_align - 1) ||
> + dbuf_end > reg->size ||
> + (!write && end > i_size_read(inode))) {
> + ret = -EINVAL;
> + goto out_reg;
> + }
> +
> + ret = rw_verify_area(write ? WRITE : READ, ffile, &offset, length);
> + if (ret)
> + goto out_reg;
> + if (write)
> + inc_syscw(current);
> + else
> + inc_syscr(current);
> +
> + if (!length) {
> + ret = 0;
> + goto out_reg;
> + }
> +
> + map = dma_buf_io_get_map(reg->ctx, false);
> + if (IS_ERR(map)) {
> + ret = PTR_ERR(map);
> + if (ret != -EAGAIN)
> + dev_err_ratelimited(fgds_misc.this_device,
> + "dma-buf mapping failed on %pg: %pe\n",
> + reg->bdev, ERR_PTR(ret));
> + goto out_reg;
> + }
> +
> + /*
> + * A direct I/O read would leave any cached pages of the range
> + * stale, so flush and drop them before submitting.
> + */
> + if (!write) {
> + ret = filemap_write_and_wait_range(ffile->f_mapping,
> + offset, end - 1);
> + if (ret)
> + goto out_map;
> + ret = invalidate_inode_pages2_range(ffile->f_mapping,
> + offset >> PAGE_SHIFT,
> + (end - 1) >> PAGE_SHIFT);
> + if (ret) {
> + /*
> + * Report a retryable error instead of letting
> + * -EBUSY escape.
> + */
> + if (ret == -EBUSY)
> + ret = -EAGAIN;
> + goto out_map;
> + }
> + }
> +
> + if (write)
> + file_start_write(ffile);
> + iov_iter_dmabuf_map(&iter, write ? ITER_SOURCE : ITER_DEST,
> + map, args->doffset, length);
> + init_sync_kiocb(&kiocb, ffile);
> + kiocb.ki_flags |= IOCB_DIRECT;
> + if (write)
> + kiocb.ki_flags |= IOCB_WRITE;
> + kiocb.ki_pos = offset;
> +
> + ret = iter_fn(&kiocb, &iter);
> + if (WARN_ON_ONCE(ret == -EIOCBQUEUED))
> + ret = -EIO;
> + if (write)
> + file_end_write(ffile);
> +
> + if (ret > 0) {
> + if (write) {
> + fsnotify_modify(ffile);
> + add_wchar(current, ret);
> + } else {
> + fsnotify_access(ffile);
> + add_rchar(current, ret);
> + }
> + }
> +
> +out_map:
> + dma_buf_io_map_drop(map);
> +out_reg:
> + fgds_reg_put(reg);
> +out_fdput:
> + fdput(fdf);
> + return ret;
> +}
> +
> +static long fgds_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
> +{
> + struct fgds_state *state = fgds_state(file);
> + struct fgds_rw_args args;
> +
> + switch (cmd) {
> + case FGDS_IOCTL_WRITE:
> + case FGDS_IOCTL_READ:
> + if (copy_from_user(&args, (void __user *)arg, sizeof(args)))
> + return -EFAULT;
> + return fgds_do_rw(state, &args, cmd == FGDS_IOCTL_WRITE);
> + case FGDS_IOCTL_REGISTER_BUF:
> + return fgds_register(file, (struct fgds_register_args __user *)arg);
> + case FGDS_IOCTL_UNREGISTER_BUF:
> + return fgds_unregister(file, (struct fgds_unregister_args __user *)arg);
> + }
> + return -ENOTTY;
> +}
> +
> +static int fgds_open(struct inode *inode, struct file *file)
> +{
> + struct fgds_state *state;
> +
> + state = kzalloc_obj(*state);
> + if (!state)
> + return -ENOMEM;
> +
> + xa_init_flags(&state->regs, XA_FLAGS_ALLOC);
> + mutex_init(&state->lock);
> + file->private_data = state;
> + return 0;
> +}
> +
> +static int fgds_release(struct inode *inode, struct file *file)
> +{
> + struct fgds_state *state = fgds_state(file);
> + struct fgds_reg *reg;
> + unsigned long id;
> +
> + xa_for_each(&state->regs, id, reg) {
> + xa_erase(&state->regs, id);
> + fgds_reg_put(reg);
> + }
> + xa_destroy(&state->regs);
> + mutex_destroy(&state->lock);
> + kfree(state);
> + return 0;
> +}
> +
> +static const struct file_operations fgds_fops = {
> + .owner = THIS_MODULE,
> + .open = fgds_open,
> + .release = fgds_release,
> + .unlocked_ioctl = fgds_ioctl,
> + .compat_ioctl = compat_ptr_ioctl,
> +};
> +
> +static struct miscdevice fgds_misc = {
> + .minor = MISC_DYNAMIC_MINOR,
> + .name = "fgds",
> + .fops = &fgds_fops,
> +};
> +
> +static int __init fgds_init(void)
> +{
> + return misc_register(&fgds_misc);
> +}
> +
> +static void __exit fgds_exit(void)
> +{
> + misc_deregister(&fgds_misc);
> +}
> +
> +module_init(fgds_init);
> +module_exit(fgds_exit);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_IMPORT_NS("DMA_BUF");
> +MODULE_AUTHOR("Mengmeng Zhao <zhaomengmeng@kylinos.cn>, Li Wang <liwang@kylinos.cn>");
> +MODULE_DESCRIPTION("Fast GPU Direct Storage");
> +MODULE_VERSION("1.0.0");
> diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
> index 0a5ebfda90f1..a7ab28edab98 100644
> --- a/fs/iomap/buffered-io.c
> +++ b/fs/iomap/buffered-io.c
> @@ -1304,6 +1304,17 @@ iomap_file_buffered_write(struct kiocb *iocb, struct iov_iter *i,
> };
> ssize_t ret;
>
> + /*
> + * The copy helpers have no source for a dma-buf iterator: the copy
> + * would move zero bytes, ->write_end() would reject the untouched
> + * folio, fault_in_iov_iter_readable() reports the iterator as
> + * readable, and the loop below would never advance. Refuse here
> + * rather than at each caller's entry point, so no path can reach
> + * the copy with such an iterator.
> + */
> + if (iov_iter_is_dmabuf_map(i))
> + return -EOPNOTSUPP;
> +
> if (iocb->ki_flags & IOCB_NOWAIT)
> iter.flags |= IOMAP_NOWAIT;
> if (iocb->ki_flags & IOCB_DONTCACHE)
> diff --git a/fs/iomap/direct-io.c b/fs/iomap/direct-io.c
> index 8b4039d16ce8..eccc07d53e9c 100644
> --- a/fs/iomap/direct-io.c
> +++ b/fs/iomap/direct-io.c
> @@ -595,8 +595,16 @@ static int iomap_dio_bio_iter(struct iomap_iter *iter, struct iomap_dio *dio)
>
> static int iomap_dio_hole_iter(struct iomap_iter *iter, struct iomap_dio *dio)
> {
> - loff_t length = iov_iter_zero(iomap_length(iter), dio->submit.iter);
> + loff_t length;
>
> + /*
> + * dmabuf maps do not support zero-filling holes or
> + * unwritten extents.
> + */
> + if (iov_iter_is_dmabuf_map(dio->submit.iter))
> + return -EOPNOTSUPP;
> +
> + length = iov_iter_zero(iomap_length(iter), dio->submit.iter);
> dio->size += length;
> if (!length)
> return -EFAULT;
> @@ -683,8 +691,8 @@ static int iomap_dio_iter(struct iomap_iter *iter, struct iomap_dio *dio)
> * the correct total number of bytes transferred; this is essential for
> * completing partial requests asynchronously.
> *
> - * Returns -ENOTBLK In case of a page invalidation invalidation failure for
> - * writes. The callers needs to fall back to buffered I/O in this case.
> + * Returns -ENOTBLK in case of a page cache invalidation failure for writes.
> + * The caller needs to fall back to buffered I/O in this case.
> */
> struct iomap_dio *
> __iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
> @@ -794,6 +802,15 @@ __iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
> * the caller tries again.
> */
> ret = -EAGAIN;
> + } else if (iov_iter_is_dmabuf_map(iter)) {
> + /*
> + * Avoid -ENOTBLK (buffered fallback)
> + * as dmabuf are not supported. Map
> + * -EBUSY to -EAGAIN for transient
> + * retires.
> + */
> + if (ret == -EBUSY)
> + ret = -EAGAIN;
> } else {
> /* fall back to buffered write */
> ret = -ENOTBLK;
> diff --git a/fs/xfs/xfs_file.c b/fs/xfs/xfs_file.c
> index 426a67b813a7..f16e9cb63e9e 100644
> --- a/fs/xfs/xfs_file.c
> +++ b/fs/xfs/xfs_file.c
> @@ -1211,6 +1211,10 @@ xfs_file_write_iter(
> return ret;
> }
>
> + /* zoned and buffered I/O is not supported for dmabuf */
> + if (iov_iter_is_dmabuf_map(from))
> + return -EOPNOTSUPP;
> +
> if (xfs_is_zoned_inode(ip))
> return xfs_file_buffered_write_zoned(iocb, from);
> return xfs_file_buffered_write(iocb, from);
> diff --git a/include/linux/iov_iter.h b/include/linux/iov_iter.h
> index 2f91884d733b..a3b99e6d8255 100644
> --- a/include/linux/iov_iter.h
> +++ b/include/linux/iov_iter.h
> @@ -376,6 +376,10 @@ size_t iterate_and_advance_kernel(struct iov_iter *iter, size_t len, void *priv,
> return iterate_folioq(iter, len, priv, priv2, step);
> if (iov_iter_is_xarray(iter))
> return iterate_xarray(iter, len, priv, priv2, step);
> + if (iov_iter_is_dmabuf_map(iter)) {
> + WARN_ON_ONCE(1);
> + return 0;
> + }
> return iterate_discard(iter, len, priv, priv2, step);
> }
>
> diff --git a/include/uapi/linux/fgds.h b/include/uapi/linux/fgds.h
> new file mode 100644
> index 000000000000..9a721c92cde2
> --- /dev/null
> +++ b/include/uapi/linux/fgds.h
> @@ -0,0 +1,124 @@
> +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
> +/*
> + * Fast GPU Direct Storage user-kernel ABI definitions.
> + *
> + * Copyright (C) 2026 KylinSoft. Co., Ltd. All rights reserved.
> + *
> + * Synchronous read/write ioctls that move data between a GPU dma-buf and
> + * a regular file.
> + */
> +#ifndef _FGDS_H
> +#define _FGDS_H
> +
> +#include <linux/types.h>
> +#include <linux/ioctl.h>
> +
> +/**
> + * struct fgds_register_args - arguments for %FGDS_IOCTL_REGISTER_BUF
> + *
> + * @dfd: dma-buf file descriptor exported by the GPU runtime.
> + * @bdev_fd: file descriptor of the underlying block device (the
> + * device the target files live on), opened with %O_DIRECT.
> + * The device is named separately from the file because:
> + * building the dma-buf I/O context needs a file for the
> + * device itself, and the device side requires %O_DIRECT
> + * on that file, which a regular file descriptor cannot
> + * provide.
> + * @handle: Returned handle for use with the other ioctls. Handles
> + * are private to the /dev/fgds file descriptor they were
> + * created on and are released automatically when that
> + * descriptor is closed. Output.
> + * @pad: Must be 0.
> + * @max_doffset: Largest accepted value of @doffset plus @length: the
> + * window must stay inside the registered dma-buf and
> + * within the 32-bit offset bound of the block-layer
> + * dma-buf iterator, so this reports the smaller of the
> + * dma-buf size and %UINT_MAX. Output.
> + * @reserved: Must be 0. Extension slot for further output fields.
> + *
> + * A registered handle keeps the device attachment alive, and once a
> + * transfer has mapped the buffer that mapping is held for the handle's
> + * lifetime.
> + *
> + * Returns 0, or a negative errno.
> + */
> +struct fgds_register_args {
> + __s32 dfd;
> + __s32 bdev_fd;
> + __u32 handle;
> + __u32 pad;
> + __u64 max_doffset;
> + __u64 reserved;
> +};
> +
> +/**
> + * struct fgds_unregister_args - arguments for %FGDS_IOCTL_UNREGISTER_BUF
> + *
> + * @handle: Handle returned by %FGDS_IOCTL_REGISTER_BUF. Input.
> + * @reserved: Must be 0.
> + *
> + * Returns 0, or -EINVAL if @handle is not registered on this file
> + * descriptor.
> + */
> +struct fgds_unregister_args {
> + __u32 handle;
> + __u32 reserved;
> +};
> +
> +/**
> + * struct fgds_rw_args - arguments for %FGDS_IOCTL_WRITE / %FGDS_IOCTL_READ
> + *
> + * @handle: Handle from %FGDS_IOCTL_REGISTER_BUF naming both the
> + * dma-buf and the block device to transfer between. Input.
> + * @ffd: Regular file descriptor. %O_DIRECT is not required on
> + * it, fgds forces the direct I/O path itself. Its file
> + * system's device must match the one registered with
> + * @handle or the transfer fails with %EXDEV. Input.
> + * @offset: Byte offset inside the file. Input.
> + * @length: Transfer length in bytes. Unlike pread() and pwrite(),
> + * which silently truncate an oversized request, fgds rejects
> + * it with %EINVAL. Input.
> + * @doffset: Byte offset inside the dma-buf, i.e. the start of the
> + * window the transfer reads from or writes to. Input.
> + * @flags: Must be 0. Input.
> + *
> + * Like pread() and pwrite(), the ioctl returns the number of bytes
> + * transferred or a negative errno. A return value below @length means a
> + * read stopped at the end of the file, or a write clamped by the calling
> + * process' RLIMIT_FSIZE.
> + *
> + * Contract and failure modes:
> + *
> + * - Alignment is enforced on two independent axes, both answered by
> + * statx() with %STATX_DIOALIGN. The file side applies to @offset and
> + * @length: the device logical block size for a read, the max of that
> + * and the file system block size for a write. The buffer side applies
> + * to @doffset and @length: the device DMA alignment. A transfer must
> + * satisfy both axes at once.
> + * - Ranges backed by file holes or unwritten extents are not supported
> + * for reads and fail with %EOPNOTSUPP: there is no page cache bounce
> + * to serve zeros from, and GPU memory is generally not CPU accessible.
> + * - Writes past the end of the file go through the file system's own
> + * direct I/O extend path; no preallocation is performed by fgds. An
> + * application that wants the space reserved must call fallocate()
> + * itself first.
> + * - Reads flush and drop any page cache of the transferred range before
> + * submitting, so subsequent buffered reads observe the fresh data.
> + */
> +struct fgds_rw_args {
> + __u32 handle;
> + __s32 ffd;
> + __u64 offset;
> + __u64 length;
> + __u64 doffset;
> + __u64 flags;
> +};
> +
> +#define FGDS_IOCTL_MAGIC 'x'
> +
> +#define FGDS_IOCTL_WRITE _IOW(FGDS_IOCTL_MAGIC, 1, struct fgds_rw_args)
> +#define FGDS_IOCTL_READ _IOW(FGDS_IOCTL_MAGIC, 2, struct fgds_rw_args)
> +#define FGDS_IOCTL_REGISTER_BUF _IOWR(FGDS_IOCTL_MAGIC, 3, struct fgds_register_args)
> +#define FGDS_IOCTL_UNREGISTER_BUF _IOW(FGDS_IOCTL_MAGIC, 4, struct fgds_unregister_args)
> +
> +#endif /* _FGDS_H */
> diff --git a/lib/iov_iter.c b/lib/iov_iter.c
> index 9b46f9ada8f9..b19bf7f5e4cf 100644
> --- a/lib/iov_iter.c
> +++ b/lib/iov_iter.c
> @@ -790,6 +790,7 @@ void iov_iter_dmabuf_map(struct iov_iter *i, unsigned int direction,
> .iov_offset = off,
> };
> }
> +EXPORT_SYMBOL_GPL(iov_iter_dmabuf_map);
>
> /**
> * iov_iter_discard - Initialise an I/O iterator that discards data
> diff --git a/mm/filemap.c b/mm/filemap.c
> index 6afec636881f..ab8a5391a699 100644
> --- a/mm/filemap.c
> +++ b/mm/filemap.c
> @@ -3013,6 +3013,14 @@ generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
> return retval;
> }
>
> + /*
> + * Buffered I/O read is not supported for dmabuf; a short direct
> + * read before this point stands on its own, as in
> + * direct_write_fallback().
> + */
> + if (iov_iter_is_dmabuf_map(iter))
> + return retval > 0 ? retval : -EOPNOTSUPP;
> +
> return filemap_read(iocb, iter, retval);
> }
> EXPORT_SYMBOL(generic_file_read_iter);
> @@ -4300,8 +4308,14 @@ generic_file_direct_write(struct kiocb *iocb, struct iov_iter *from)
> */
> written = kiocb_invalidate_pages(iocb, write_len);
> if (written) {
> - if (written == -EBUSY)
> + if (written == -EBUSY) {
> + /*
> + * Avoid fallback to buffered write for dmabuf.
> + */
> + if (iov_iter_is_dmabuf_map(from))
> + return -EAGAIN;
> return 0;
> + }
> return written;
> }
>
> @@ -4353,6 +4367,12 @@ ssize_t generic_perform_write(struct kiocb *iocb, struct iov_iter *i)
> long status = 0;
> ssize_t written = 0;
>
> + /*
> + * Buffered write is not supported for dmabuf.
> + */
> + if (iov_iter_is_dmabuf_map(i))
> + return -EOPNOTSUPP;
> +
> do {
> struct folio *folio;
> size_t offset; /* Offset into folio */
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH v2 0/1] misc: fgds: introduce GPU-File direct I/O via dma-buf
2026-10-27 9:08 ` [RFC PATCH v2 0/1] misc: fgds: introduce GPU-File direct I/O via dma-buf Li Wang
2026-10-27 9:08 ` [RFC PATCH v2 1/1] " Li Wang
@ 2026-10-27 13:54 ` Pavel Begunkov
1 sibling, 0 replies; 14+ messages in thread
From: Pavel Begunkov @ 2026-10-27 13:54 UTC (permalink / raw)
To: Li Wang, Arnd Bergmann, Greg Kroah-Hartman
Cc: Sumit Semwal, Christian König, linux-media, dri-devel,
linaro-mm-sig, io-uring, linux-kernel
Hey Li,
On 10/27/26 09:08, Li Wang wrote:
> This patch introduces a driver named fgds that allows moving data between
> a GPU dma-buf and a regular file on NVME without staging it through the
> page cache. This is particularly useful for LLM workloads, such as model
> loading, KV cache offloading, and checkpointing. The fgds device
> corresponds one-to-one with the PCIe GPU on the machine. The usage is
> straightforward: an application first calls ioctl on the corresponding
> fgds device with REGISTER_BUF, passing the target GPU memory buffer
> address (represented as a dma-buf fd). Subsequently, the application can
> perform direct data transfer between a regular file on NVMe and GPU memory
> by invoking ioctl on the fgds device with READ/WRITE, passing the file
> descriptor and the handle returned by REGISTER_BUF.
>
> This solution allows both applications and the fgds device to work
> seamlessly with GPUs from different vendors without any modifications.
> Furthermore, applications no longer need to call vendor-specific
> proprietary APIs (such as NVIDIA's cuFile API) or install vendor-specific
> kernel modules (such as NVIDIA's nvidia-fs.ko) for different GPU vendors.
> We have tested fgds on NVDIA and AMD GPU cards, and theoretically, it also
> works on GPUs from other vendors that support exporting GPU memory via
> dma-buf. Some performance test results can be found in [1].
>
> This implementation offers several advantages:
> - Simplicity and Efficiency: It provides a lightweight and intuitive
> interface. The single ioctl call mimics the parameter structure of
> pread/pwrite, and its return value semantics are fully compatible with
> pread/pwrite
> - Isolation and Security: The interface is independent and has a minimal
> impact scope. This isolation facilitates fine-grained permission control
> on the device node, enhancing security management
>
> Changes since v1:
> - Refactor implementation per Christian's feedback, on top of Pavel's
> DMA-buf support for io_uring [2]
People already replied, but I'm going to elaborate a bit. That series
[2] already exposes a uapi via io_uring, it's device agnostic and all
that. The framework was made to allow multiple users, perhaps folks
have some kernel-internal users in mind, but a completely new
read/write uAPI looks unnecessary, especially an ioctl() based one.
It sounds like [1] is already using io_uring, so it doesn't sound
like a problem that the entire uapi is io_uring based(?).
> - Add validation checks in the XFS I/O submission path to ensure that
> bios with the REQ_DMABUF flag are only processed via direct I/O, and
> prevents the kernel from attempting to fall back to buffered I/O, which
> is incompatible with DMA-buf backed GPU memory regions. While currently
> supports only XFS, the underlying mechanism is filesystem-agnostic and
> can be adapted for other filesystems with similar minor changes
xfs support it a it orthogonal to the uapi question, but it should be
implemented in xfs and not side stepped. There is an RFC on the mailing
list from Anuj and Nitesh that is trying to do that.
> - Address the comments from Greg and the Sashiko AI
>
> [1] https://github.com/Storage-and-OS-for-AI/fgds
> [2] https://git.kernel.org/pub/scm/linux/kernel/git/axboe/linux.git/
> tag/?h=for-7.4/io_uring-dmabuf-20261023
>
> Mengmeng Zhao (1):
> misc: fgds: introduce GPU-File direct I/O via dma-buf
>
> .../userspace-api/ioctl/ioctl-number.rst | 2 +
> drivers/dma-buf/dma-buf-io.c | 3 +
> drivers/misc/Kconfig | 16 +
> drivers/misc/Makefile | 1 +
> drivers/misc/fgds.c | 506 ++++++++++++++++++
> fs/iomap/buffered-io.c | 11 +
> fs/iomap/direct-io.c | 23 +-
> fs/xfs/xfs_file.c | 4 +
> include/linux/iov_iter.h | 4 +
> include/uapi/linux/fgds.h | 124 +++++
> lib/iov_iter.c | 1 +
> mm/filemap.c | 22 +-
> 12 files changed, 713 insertions(+), 4 deletions(-)
> create mode 100644 drivers/misc/fgds.c
> create mode 100644 include/uapi/linux/fgds.h
>
--
Pavel Begunkov
^ permalink raw reply [flat|nested] 14+ messages in thread
end of thread, other threads:[~2026-10-27 13:55 UTC | newest]
Thread overview: 14+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-08 13:15 [RFC PATCH] misc: fgds: enable GPU-NVMe direct I/O via POSIX and io_uring Li Wang
2026-09-09 6:10 ` Greg Kroah-Hartman
2026-09-09 10:42 ` Li Wang
2026-09-09 13:35 ` Greg Kroah-Hartman
2026-09-09 13:45 ` Christian König
2026-09-10 3:57 ` Li Wang
2026-09-10 8:35 ` Christian König
2026-09-12 12:45 ` Pavel Begunkov
2026-10-27 9:08 ` [RFC PATCH v2 0/1] misc: fgds: introduce GPU-File direct I/O via dma-buf Li Wang
2026-10-27 9:08 ` [RFC PATCH v2 1/1] " Li Wang
2026-10-27 13:03 ` Jens Axboe
2026-10-27 13:12 ` Greg Kroah-Hartman
2026-10-27 13:12 ` Christian König
2026-10-27 13:54 ` [RFC PATCH v2 0/1] " Pavel Begunkov
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®