From: Yufeng Wang <wangyufeng@kylinos.cn>
This RFC proposes a new virtio transport feature, VIRTIO_F_SQCQ_POLL,
that eliminates VM exits on both submission and completion paths for
vhost-scsi by using shared-memory doorbells and kernel polling
threads, following the io_uring SQPOLL model.
This is an early RFC to gather design feedback. The implementation is
functional and has been tested on arm64 and x86_64. We are not
requesting merge at this time.
Problem
-------
vhost-scsi uses MMIO writes (Guest -> Host) and MSI-X interrupts
(Host -> Guest) for notification. Each notification involves a VM exit,
which becomes a bottleneck at high IOPS:
- 4K random read, QD32, 8 jobs: 341K IOPS baseline
- With ~340K VM exits/second, the exit overhead dominates
Existing mitigations (vhost-net's tx polling, blk-mq iopoll) only
address one direction or require the submitting task to poll. Neither
eliminates VM exits on both paths simultaneously.
Solution
--------
Introduce two cache-line-aligned doorbell structures, SQ (Submission
Queue) and CQ (Completion Queue), placed alongside the standard split
virtqueue:
- Guest writes sq->idx instead of MMIO kick; Host poll thread
detects the change and processes submissions.
- Host writes cq->idx instead of MSI-X interrupt; Guest poll
thread detects the change and invokes completion callbacks.
A NEED_WAKEUP protocol (mirroring io_uring's SQ_NEED_WAKEUP) allows
either side to sleep when idle, with the other side responsible for
waking it via eventfd.
Feature negotiation via VIRTIO_F_SQCQ_POLL (bit 42) ensures zero
overhead when not negotiated — the driver falls back to traditional
MMIO kick + MSI-X interrupt.
Performance
-----------
Benchmark: fio, 4K random I/O
Test configuration:
arm64:
CPU: Kunpeng 920 (2.6GHz), 8 vCPUs
Disk: NVMe INTEL SSDPED1K375GA (375GB)
x86_64:
CPU: Intel Xeon E5-2680 v4 @ 2.40GHz, 8 vCPUs
Disk: NVMe SAMSUNG MZ1LB960HAJQ-000MV (960GB)
Backend: vhost-scsi with TCM loopback to NVMe device
QEMU: vhost-scsi-pci with VIRTIO_F_SQCQ_POLL negotiated
arm64 results:
Test Baseline SQ/CQ Poll Change
----------- ---------- ---------- -------
randread QD1 22,427 28,289 +26%
randread QD32 NJ1 89,910 75,665 -16%
randread QD32 NJ4 186,763 379,549 +103%
randread QD32 NJ8 199,967 550,633 +175%
randwrite QD1 21,912 27,261 +24%
randwrite QD32 NJ1 85,349 81,389 -5%
randwrite QD32 NJ4 190,443 355,811 +87%
randwrite QD32 NJ8 196,552 566,640 +188%
x86_64 results:
Test Baseline SQ/CQ Poll Change
----------- ---------- ---------- -------
randread QD1 8,263 9,552 +16%
randread QD32 NJ1 127,412 162,805 +28%
randread QD32 NJ4 303,208 375,056 +24%
randread QD32 NJ8 341,625 371,193 +9%
randwrite QD1 20,773 30,332 +46%
randwrite QD32 NJ1 133,316 159,207 +19%
randwrite QD32 NJ4 233,373 229,224 -2%
randwrite QD32 NJ8 231,442 231,676 +0%
Multi-queue workloads (NJ4/NJ8) see significant improvement on arm64
(87-188%) and moderate improvement on x86_64 (9-24%). Single-VQ
high-queue-depth workloads show a minor regression on arm64 due to
polling overhead vs. VM-exit savings trade-off, while x86_64 shows
improvement across most configurations (16-46% for QD1, 19-28%
for QD32-NJ1) due to lower per-VM-exit cost on x86.
Why Not vDPA?
-------------
vhost-vDPA already provides doorbell mmap and polling. A reasonable
reviewer would ask: why not extend vhost-vDPA instead?
Three reasons:
1. No vdpa-scsi device exists. The vDPA framework
(drivers/vdpa/) currently has hardware devices for net (mlx5,
ifcvf, etc.) and software devices for net and blk (vdpa_sim).
There is no virtio-scsi vDPA device, hardware or software.
Building one means re-implementing vhost-scsi's TCM integration
(SCSI CDB processing, ALUA, persistent reservations) under the
vDPA device abstraction — 3-5x the work of extending vhost-scsi.
2. vhost-scsi is a deployed interface. libvirt, QEMU, and
OpenStack have vhost-scsi configuration APIs and operational
tooling. Switching to vhost-vdpa requires a new backend, user
migration, and toolchain updates. SQ/CQ poll as a vhost-scsi
feature is fully backward-compatible — no existing deployments
break.
3. The protocol is transport-agnostic. The SQ/CQ doorbell design
(struct vring_sq, struct vring_cq, NEED_WAKEUP handshake) is
orthogonal to vhost vs. vDPA. The same UAPI can be consumed by
vhost-scsi today and a future vdpa-scsi device. Implementing in
vhost-scsi first does not block future vDPA integration.
We acknowledge that vDPA is the long-term direction for virtio
backends. If this SQ/CQ poll protocol is accepted, it can be ported
to the vDPA framework; a vdpa-scsi device is independent work.
Patch Structure
---------------
Patch 1: UAPI definitions (virtio_config.h, virtio_ring.h,
virtio_pci.h) — shared interface for all components
Patch 2: vhost kernel support (vhost.c, vhost.h, scsi.c,
vhost.h UAPI, vhost_types.h UAPI) — Host poll thread
Patch 3: virtio guest driver (virtio_ring.c, virtio_sqcq_poll.c,
virtio_pci_modern.c, virtio.c, virtio_scsi.c) — Guest
poll thread and submission path
Patch 4: QEMU support (virtio-pci.c, vhost.c) — PCI config
forwarding and vhost ioctl bridge
Patches 1-3 apply to the Linux kernel tree. Patch 4 applies to
the QEMU tree separately.
Spec Status
-----------
A virtio-spec format document has been prepared and will be submitted
to the OASIS virtio TC as a proposal. This RFC stage seeks design
feedback before initiating the formal spec process.
Known Limitations (Future Work)
-------------------------------
- CPU hotplug: no notifier registered; poll thread may be
migrated when its CPU goes offline. Planned: kthread_park +
dynamic rebind.
- Live migration: no explicit stop/flush coordination during
migration. Planned: VHOST_BACKEND_F_SUSPEND/RESUME integration.
- SMAP overhead: Host uses get_user/put_user for doorbell access.
Future optimization: GUP + kmap to map pages into kernel space.
- Backpressure: no "slow down" signal from device to driver.
Future: CQ throttle flag or avail_event reuse.
- Packed ring: not supported; explicitly rejected at feature
negotiation. Future: add packed ring doorbell support.
RFC Goals
---------
1. Validate the overall design direction (doorbell + polling
model, NEED_WAKEUP protocol)
2. Get feedback on the feature bit allocation (42) and UAPI
structure design
3. Understand whether vDPA concern is a blocker or can be
addressed with the transport-agnostic argument
4. Collect guidance on prioritizing future work items
(hotplug, migration, spec process)
We welcome all feedback, especially on:
- Whether the NEED_WAKEUP protocol design is sound
- Whether the per-device Guest poll thread model (vs per-VQ)
is acceptable
- Whether feature bit 42 is appropriate or if a different
allocation is needed
- Any concerns about the SMAP overhead in the Host poll path
How to Test
-----------
Patch usage:
Guest Kernel: apply patch 1 (UAPI) + patch 3 (guest driver)
Host Kernel: apply patch 1 (UAPI) + patch 2 (vhost support)
QEMU: apply patch 4 (vhost-scsi bridge)
1. Set up vhost-scsi target on the host (see:
https://wiki.libvirt.org/Vhost-scsi_target.html#Host_Setup)
using targetcli to create a TCM loopback device, e.g.:
targetcli /backstores/loopback create dev=/dev/sda
targetcli /vhost create naa.5001405376e34400
targetcli /vhost/naa.5001405376e34400/lun create \
/backstores/loopback/dev,/dev/sda
2. Boot VM with vhost-scsi using patched QEMU:
qemu-system-aarch64 ... \
-device vhost-scsi-pci,wwpn=naa.5001405376e34400
3. Verify SQ/CQ poll mode is active on the host:
dmesg | grep "vhost-sqcq"
# Expected: "vhost-sqcq: vq[N] poll thread bound to cpuN"
# and 10s stats: "vhost-sqcq: vq[N] cq=... ema_lat=... interval=..."
4. Verify feature negotiated in guest:
dmesg | grep "VIRTIO_F_SQCQ_POLL"
# Expected: "VIRTIO_F_SQCQ_POLL negotiated, starting poll thread"
# and 10s stats: "io_stats: cq=... avg_lat=... interval=..."
5. Run fio benchmark (compare with unpatched QEMU/kernel baseline):
fio --name=randread --rw=randread --bs=4k --iodepth=32 \
--numjobs=4 --runtime=60 --time_based --direct=1 \
--filename=/dev/sda
Test scripts (run-sqcq-compare.sh, compare-sqcq-results.sh) are
available and will be sent as a follow-up to this RFC.
Thank you for your time.
---
Yufeng Wang (4):
common: add UAPI for SQ/CQ doorbell polling
vhost: host kernel support for SQ/CQ polling
virtio: guest driver support for SQ/CQ polling
qemu: add SQ/CQ polling mode support for vhost-scsi
Patch 1 (UAPI, 3 files): +41 -1
Patch 2 (vhost, 5 files): +794 -20
Patch 3 (virtio guest, 10 files): +840 -3
Patch 4 (QEMU, 14 files): +179 -3
Total: 32 files changed, 1854 insertions(+), 27 deletions(-)
--
2.34.1
On Mon, Jul 20, 2026 at 4:19 PM rom.wang <r4o5m6e8o@163.com> wrote: > > From: Yufeng Wang <wangyufeng@kylinos.cn> > > This RFC proposes a new virtio transport feature, VIRTIO_F_SQCQ_POLL, > that eliminates VM exits on both submission and completion paths for > vhost-scsi by using shared-memory doorbells and kernel polling > threads, following the io_uring SQPOLL model. > > This is an early RFC to gather design feedback. The implementation is > functional and has been tested on arm64 and x86_64. We are not > requesting merge at this time. > > > Problem > ------- > > vhost-scsi uses MMIO writes (Guest -> Host) and MSI-X interrupts > (Host -> Guest) for notification. Each notification involves a VM exit, > which becomes a bottleneck at high IOPS: > > - 4K random read, QD32, 8 jobs: 341K IOPS baseline > - With ~340K VM exits/second, the exit overhead dominates > > Existing mitigations (vhost-net's tx polling, blk-mq iopoll) only > address one direction or require the submitting task to poll. Neither > eliminates VM exits on both paths simultaneously. > > > Solution > -------- > > Introduce two cache-line-aligned doorbell structures, SQ (Submission > Queue) and CQ (Completion Queue), placed alongside the standard split > virtqueue: > > - Guest writes sq->idx instead of MMIO kick; Host poll thread > detects the change and processes submissions. > - Host writes cq->idx instead of MSI-X interrupt; Guest poll > thread detects the change and invokes completion callbacks. > > A NEED_WAKEUP protocol (mirroring io_uring's SQ_NEED_WAKEUP) allows > either side to sleep when idle, with the other side responsible for > waking it via eventfd. > > Feature negotiation via VIRTIO_F_SQCQ_POLL (bit 42) ensures zero > overhead when not negotiated — the driver falls back to traditional > MMIO kick + MSI-X interrupt. > > > Performance > ----------- > > Benchmark: fio, 4K random I/O > > Test configuration: > > arm64: > CPU: Kunpeng 920 (2.6GHz), 8 vCPUs > Disk: NVMe INTEL SSDPED1K375GA (375GB) > > x86_64: > CPU: Intel Xeon E5-2680 v4 @ 2.40GHz, 8 vCPUs > Disk: NVMe SAMSUNG MZ1LB960HAJQ-000MV (960GB) > > Backend: vhost-scsi with TCM loopback to NVMe device > QEMU: vhost-scsi-pci with VIRTIO_F_SQCQ_POLL negotiated > > arm64 results: > > Test Baseline SQ/CQ Poll Change > ----------- ---------- ---------- ------- > randread QD1 22,427 28,289 +26% > randread QD32 NJ1 89,910 75,665 -16% > randread QD32 NJ4 186,763 379,549 +103% > randread QD32 NJ8 199,967 550,633 +175% > randwrite QD1 21,912 27,261 +24% > randwrite QD32 NJ1 85,349 81,389 -5% > randwrite QD32 NJ4 190,443 355,811 +87% > randwrite QD32 NJ8 196,552 566,640 +188% > > x86_64 results: > > Test Baseline SQ/CQ Poll Change > ----------- ---------- ---------- ------- > randread QD1 8,263 9,552 +16% > randread QD32 NJ1 127,412 162,805 +28% > randread QD32 NJ4 303,208 375,056 +24% > randread QD32 NJ8 341,625 371,193 +9% > randwrite QD1 20,773 30,332 +46% > randwrite QD32 NJ1 133,316 159,207 +19% > randwrite QD32 NJ4 233,373 229,224 -2% > randwrite QD32 NJ8 231,442 231,676 +0% > > Multi-queue workloads (NJ4/NJ8) see significant improvement on arm64 > (87-188%) and moderate improvement on x86_64 (9-24%). Single-VQ > high-queue-depth workloads show a minor regression on arm64 due to > polling overhead vs. VM-exit savings trade-off, while x86_64 shows > improvement across most configurations (16-46% for QD1, 19-28% > for QD32-NJ1) due to lower per-VM-exit cost on x86. > > > Why Not vDPA? > ------------- > > vhost-vDPA already provides doorbell mmap and polling. A reasonable > reviewer would ask: why not extend vhost-vDPA instead? > > Three reasons: > > 1. No vdpa-scsi device exists. The vDPA framework > (drivers/vdpa/) currently has hardware devices for net (mlx5, > ifcvf, etc.) and software devices for net and blk (vdpa_sim). > There is no virtio-scsi vDPA device, hardware or software. > Building one means re-implementing vhost-scsi's TCM integration > (SCSI CDB processing, ALUA, persistent reservations) under the > vDPA device abstraction — 3-5x the work of extending vhost-scsi. > I don't have a lot of experience in vhost-scsi, but maybe using generic vdpa device and ~passthrough all those calls to something similar to vhost-scsi helps? Live migration is still a challenge that way but you already mention that in Known Limitations, so maybe going to generic saves a significant amount of work. https://patchew.org/QEMU/20221215134944.2809-1-longpeng2@huawei.com/ Also, I'm failing to see the advantage of the Send / Completion queues over a more agressive usage of event_idx, VIRTQ_USED_F_NO_NOTIFY / VIRTQ_AVAIL_F_NO_INTERRUPT or VIRTIO_F_NOTIFICATION_DATA combined. Can we explore what does these lacks compared with the send and completion queues? > 2. vhost-scsi is a deployed interface. libvirt, QEMU, and > OpenStack have vhost-scsi configuration APIs and operational > tooling. Switching to vhost-vdpa requires a new backend, user > migration, and toolchain updates. SQ/CQ poll as a vhost-scsi > feature is fully backward-compatible — no existing deployments > break. > > 3. The protocol is transport-agnostic. The SQ/CQ doorbell design > (struct vring_sq, struct vring_cq, NEED_WAKEUP handshake) is > orthogonal to vhost vs. vDPA. The same UAPI can be consumed by > vhost-scsi today and a future vdpa-scsi device. Implementing in > vhost-scsi first does not block future vDPA integration. > > We acknowledge that vDPA is the long-term direction for virtio > backends. If this SQ/CQ poll protocol is accepted, it can be ported > to the vDPA framework; a vdpa-scsi device is independent work. > > > Patch Structure > --------------- > > Patch 1: UAPI definitions (virtio_config.h, virtio_ring.h, > virtio_pci.h) — shared interface for all components > Patch 2: vhost kernel support (vhost.c, vhost.h, scsi.c, > vhost.h UAPI, vhost_types.h UAPI) — Host poll thread > Patch 3: virtio guest driver (virtio_ring.c, virtio_sqcq_poll.c, > virtio_pci_modern.c, virtio.c, virtio_scsi.c) — Guest > poll thread and submission path > Patch 4: QEMU support (virtio-pci.c, vhost.c) — PCI config > forwarding and vhost ioctl bridge > > Patches 1-3 apply to the Linux kernel tree. Patch 4 applies to > the QEMU tree separately. > > > Spec Status > ----------- > > A virtio-spec format document has been prepared and will be submitted > to the OASIS virtio TC as a proposal. This RFC stage seeks design > feedback before initiating the formal spec process. > > > Known Limitations (Future Work) > ------------------------------- > > - CPU hotplug: no notifier registered; poll thread may be > migrated when its CPU goes offline. Planned: kthread_park + > dynamic rebind. > - Live migration: no explicit stop/flush coordination during > migration. Planned: VHOST_BACKEND_F_SUSPEND/RESUME integration. > - SMAP overhead: Host uses get_user/put_user for doorbell access. > Future optimization: GUP + kmap to map pages into kernel space. > - Backpressure: no "slow down" signal from device to driver. > Future: CQ throttle flag or avail_event reuse. > - Packed ring: not supported; explicitly rejected at feature > negotiation. Future: add packed ring doorbell support. > > > RFC Goals > --------- > > 1. Validate the overall design direction (doorbell + polling > model, NEED_WAKEUP protocol) > 2. Get feedback on the feature bit allocation (42) and UAPI > structure design > 3. Understand whether vDPA concern is a blocker or can be > addressed with the transport-agnostic argument > 4. Collect guidance on prioritizing future work items > (hotplug, migration, spec process) > > We welcome all feedback, especially on: > - Whether the NEED_WAKEUP protocol design is sound > - Whether the per-device Guest poll thread model (vs per-VQ) > is acceptable > - Whether feature bit 42 is appropriate or if a different > allocation is needed > - Any concerns about the SMAP overhead in the Host poll path > > > How to Test > ----------- > > Patch usage: > Guest Kernel: apply patch 1 (UAPI) + patch 3 (guest driver) > Host Kernel: apply patch 1 (UAPI) + patch 2 (vhost support) > QEMU: apply patch 4 (vhost-scsi bridge) > > 1. Set up vhost-scsi target on the host (see: > https://wiki.libvirt.org/Vhost-scsi_target.html#Host_Setup) > using targetcli to create a TCM loopback device, e.g.: > targetcli /backstores/loopback create dev=/dev/sda > targetcli /vhost create naa.5001405376e34400 > targetcli /vhost/naa.5001405376e34400/lun create \ > /backstores/loopback/dev,/dev/sda > > 2. Boot VM with vhost-scsi using patched QEMU: > qemu-system-aarch64 ... \ > -device vhost-scsi-pci,wwpn=naa.5001405376e34400 > > 3. Verify SQ/CQ poll mode is active on the host: > dmesg | grep "vhost-sqcq" > # Expected: "vhost-sqcq: vq[N] poll thread bound to cpuN" > # and 10s stats: "vhost-sqcq: vq[N] cq=... ema_lat=... interval=..." > > 4. Verify feature negotiated in guest: > dmesg | grep "VIRTIO_F_SQCQ_POLL" > # Expected: "VIRTIO_F_SQCQ_POLL negotiated, starting poll thread" > # and 10s stats: "io_stats: cq=... avg_lat=... interval=..." > > 5. Run fio benchmark (compare with unpatched QEMU/kernel baseline): > fio --name=randread --rw=randread --bs=4k --iodepth=32 \ > --numjobs=4 --runtime=60 --time_based --direct=1 \ > --filename=/dev/sda > > Test scripts (run-sqcq-compare.sh, compare-sqcq-results.sh) are > available and will be sent as a follow-up to this RFC. > > > Thank you for your time. > > --- > Yufeng Wang (4): > common: add UAPI for SQ/CQ doorbell polling > vhost: host kernel support for SQ/CQ polling > virtio: guest driver support for SQ/CQ polling > qemu: add SQ/CQ polling mode support for vhost-scsi > > Patch 1 (UAPI, 3 files): +41 -1 > Patch 2 (vhost, 5 files): +794 -20 > Patch 3 (virtio guest, 10 files): +840 -3 > Patch 4 (QEMU, 14 files): +179 -3 > > Total: 32 files changed, 1854 insertions(+), 27 deletions(-) > > -- > 2.34.1 >
From: Yufeng Wang <wangyufeng@kylinos.cn>
Thank you for the detailed feedback. We've completed benchmark
testing comparing the existing notification mechanisms against the
proposed SQ/CQ polling. The results are informative.
Test Modes Explained
--------------------
To answer your second question with data, we tested four modes:
1. **baseline**: Default vhost-scsi with event_idx auto-negotiated.
I/O engine: libaio. Submission uses MMIO kick (VM exit), completion
uses MSI-X interrupt (VM exit). The guest driver dynamically toggles
NO_NOTIFY/NO_INTERRUPT during completion processing, but these are
short windows — most I/O still incurs VM exits on both paths.
2. **poll-queue**: This mode combines the existing notification features
you mentioned:
- VIRTIO_RING_F_EVENT_IDX: auto-negotiated (same as baseline)
- VIRTQ_AVAIL_F_NO_INTERRUPT: effectively permanent — poll queue
VQs have callback=NULL and no MSI-X vector, so the guest never
receives completion interrupts regardless of the flag state
- VIRTQ_USED_F_NO_NOTIFY: vhost dynamically toggles this during
active processing (vhost_disable_notify/enable_notify)
- VIRTIO_F_NOTIFICATION_DATA: negotiated but vhost kernel does not
process notification data, so no practical effect
Implemented via virtio-scsi poll queues (virtscsi_poll_queues=16,
callback=NULL, no MSI-X vector) + io_uring IOPOLL mode (--hipri).
blk-mq polls the used ring for completions via virtscsi_mq_poll().
This eliminates completion-side VM exits. However, submission still
uses MMIO kick (VM exit). Note: libaio cannot use this mode because
aio.c strips IOCB_HIPRI ("no one is going to poll for this I/O").
3. **sqcq-poll**: The proposed SQ/CQ doorbell polling (this RFC) with
libaio. Guest writes sq->idx instead of MMIO kick (no VM exit).
Guest poll thread checks used ring via more_used() (no interrupt).
NEED_WAKEUP protocol enables adaptive sleep when idle. Eliminates
VM exits on BOTH submission and completion paths.
4. **sqcq-uring**: Same SQ/CQ polling but with io_uring + --hipri.
Included to isolate the I/O engine variable from the polling
mechanism.
Results
-------
Test configuration:
x86_64: Intel Xeon E5-2680 v4 @ 2.40GHz, 16 vCPUs
NVMe SAMSUNG MZ1LB960HAJQ-000MV (960GB)
arm64: Kunpeng 920 (2.6GHz), 16 vCPUs
NVMe INTEL SSDPED1K375GA (375GB)
Backend: vhost-scsi with TCM loopback to NVMe
arm64 IOPS (best of 3 runs, fio 4K random I/O):
Test baseline poll-queue sqcq-poll sqcq-uring
───────────── ──────── ────────── ───────── ──────────
randread QD1 23,751 29,892 29,680 26,427
randread QD32 NJ1 93,221 162,849 * 84,967 70,271
randread QD32 NJ4 184,939 186,859 340,780 235,310
randread QD32 NJ8 190,562 188,382 520,946 518,197
randwrite QD1 24,131 — 25,520 25,485
randwrite QD32 NJ1 84,725 — 70,652 68,518
randwrite QD32 NJ4 204,745 — 323,449 312,204
randwrite QD32 NJ8 193,802 — 522,322 516,448
* poll-queue NJ1 read shows high IOPS but hangs at NJ≥4 (see below)
x86_64 IOPS (best of 3 runs):
Test baseline poll-queue sqcq-poll sqcq-uring
───────────── ──────── ────────── ───────── ──────────
randread QD1 8,316 9,022 9,533 9,489
randread QD32 NJ1 124,909 146,744 165,761 165,929
randread QD32 NJ4 305,951 332,139 378,305 379,546
randread QD32 NJ8 344,223 356,328 374,932 374,976
randwrite QD1 20,106 24,464 27,265 27,768
randwrite QD32 NJ1 131,836 153,203 162,145 161,402
randwrite QD32 NJ4 232,908 234,559 232,976 231,336
randwrite QD32 NJ8 231,242 229,858 231,828 231,465
Key Findings
------------
1. **poll-queue excels at NJ=1 (arm64)**: On arm64 4K randread QD32
NJ1, poll-queue achieved 163K IOPS — significantly higher than
baseline (93K, +75%) and SQ/CQ polling (85K). io_uring IOPOLL
mode is very efficient for single-queue workloads because the
submitting task polls for completion directly, avoiding both
interrupt latency and poll thread scheduling overhead.
2. **SQ/CQ dramatically outperforms poll-queue at NJ≥4 (arm64)**:
At 4K randread QD32, poll-queue and SQ/CQ diverge sharply:
- NJ4: poll-queue 187K vs SQ/CQ 341K (+82% over poll-queue)
- NJ8: poll-queue 188K vs SQ/CQ 521K (+177% over poll-queue)
SQ/CQ also outperforms baseline by +84% (NJ4) and +173% (NJ8).
3. **libaio incompatibility**: The existing poll queue mechanism
requires io_uring IOPOLL mode. libaio explicitly strips IOCB_HIPRI
(fs/aio.c: "no one is going to poll for this I/O"), so poll queues
are inaccessible to libaio users. SQ/CQ works with any I/O engine
because the poll thread is independent of the submitting task.
Development Insights
--------------------
Beyond eliminating VM exits, two engineering decisions were critical
to achieving the performance shown above:
1. **TCM completion CPU steering**: Our initial goal was 5-10% IOPS
improvement at 4K random I/O QD1 NJ1. After implementing the basic
polling and NEED_WAKEUP protocol, this target was not met. The
bottleneck was that TCM completions were being scheduled on the same
CPU as the busy poll thread, starving the completion workqueue. We
added vhost_sqcq_pick_completion_cpu() to steer TCM completions to
a free CPU (avoiding busy poll cores), with fallback to next-CPU
when all cores are occupied. This closed the gap and achieved the
QD1 target.
2. **Adaptive idle policy with EMA**: After QD1 targets were met,
we tested higher concurrency (QD32 NJ4/NJ8) and found that
aggressive polling caused performance degradation under
multi-queue workloads — likely due to the poll thread consuming
excessive CPU cycles that competing vCPUs needed. We solved this
by implementing an EMA-based adaptive spin budget: the poll thread
spins within a time window derived from recent I/O latency, then
yields. Combined with the NEED_WAKEUP protocol, this allows the
poll thread to busy-spin when active but sleep when idle, avoiding
CPU contention with other vCPUs.
On the vDPA Question
--------------------
The generic vDPA device approach you suggested is indeed a better
direction. The QEMU-side zero-code advantage (leveraging existing
vdpa-dev.c) is very attractive. Our concern is that vhost-scsi's
TCM integration (SCSI CDB, ALUA, persistent reservations) would
need to be wrapped as a vDPA backend. This feels like a 0-to-1
engineering effort, and we're uncertain about the actual scope of
work involved. If there are additional guidelines or examples
available, we'd be very willing to explore this direction.
One More Thing: SQ/CQ + poll-queue Combined
--------------------------------------------
During arm64 testing, we accidentally ran the poll-queue benchmark
(--hipri, io_uring, virtscsi_poll_queues=16) on a kernel that also
had VIRTIO_F_SQCQ_POLL negotiated. This "poll-sqcq" mode combines
both mechanisms simultaneously:
- Submission: SQ/CQ doorbell (sq->idx write, no VM exit)
- Completion: both SQ/CQ poll thread AND blk-mq poll via io_uring
The results were surprisingly strong (arm64, 4K random I/O):
Test baseline poll-queue poll-sqcq sqcq-poll
───────────── ──────── ────────── ───────── ─────────
randread QD1 23,751 29,892 41,683 29,680
randread QD32 NJ1 93,221 162,849 193,029 84,967
randread QD32 NJ4 184,939 186,859 585,445 340,780
randread QD32 NJ8 190,562 188,382 560,727 520,946
randwrite QD1 24,131 — 38,591 25,520
randwrite QD32 NJ1 84,725 — 187,421 70,652
randwrite QD32 NJ4 204,745 — 598,419 323,449
randwrite QD32 NJ8 193,802 — 569,890 522,322
poll-sqcq outperforms both individual mechanisms across all test
cases. We haven't fully analyzed why, but suspect the combination
benefits from:
- SQ/CQ eliminating submission VM exits
- io_uring IOPOLL providing low-latency completion detection via
blk-mq (directly in submitting task context)
- SQ/CQ poll thread as a secondary completion poller covering VQs
that blk-mq doesn't reach
This is preliminary and accidental data, but we think it's worth
sharing as it suggests the two mechanisms are complementary rather
than mutually exclusive.
Benchmark scripts and raw fio JSON data are available and can be
sent as a follow-up upon request.
Best regards,
Yufeng
Signed-off-by: Yufeng Wang <wangyufeng@kylinos.cn>
© 2016 - 2026 Red Hat, Inc.