Skip to content

Commit 4756d45

Browse files
committed
docs: document virtio-net data plane tuning
1 parent 5029e0c commit 4756d45

2 files changed

Lines changed: 85 additions & 20 deletions

File tree

docs/bridge-networking.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,6 @@ vmm-cli.py deploy --name my-vm --image dstack-0.5.6 --compose app.yaml --net use
210210

211211
### vhost-net and multiqueue
212212

213-
Bridge NICs use the host kernel's vhost-net data plane by default, and can expose several virtio-net queue pairs. Both are configurable per node and per VM — see [network-data-plane.md](network-data-plane.md) for the knobs, the mode support matrix, and how to pick a queue count.
213+
Bridge NICs can run on the host kernel's vhost-net data plane and expose several virtio-net queue pairs. Both are off by default and enabled per node or per VM — see [network-data-plane.md](network-data-plane.md) for the knobs, the enablement checklist, the mode support matrix, and how to pick a queue count.
214214

215215
vhost-net works in a TDX guest: the virtio rings and buffers live in shared, unencrypted memory so that a host-side backend can reach them, which is the same mechanism `vhost-vsock-pci` has always relied on.

docs/network-data-plane.md

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ cost twice over.
3030
it relocates the ceiling rather than removing
3131
it: packets now arrive faster than a single guest receive queue can drain, and
3232
the drops reappear at a higher rate. More queue pairs is what removes them,
33-
which is why both are defaults: vhost everywhere, and a queue count that follows
34-
the VM's vCPU count. The knob you are more likely to reach for is the other
35-
direction — see [Choosing a queue count](#choosing-a-queue-count).
33+
which is why enabling vhost also enables a queue count that follows the VM's
34+
vCPU count — the two travel together.
35+
36+
vhost is **off by default** and enabled per node (or per VM). Two things make
37+
it an opt-in rather than a default: turning it on changes the virtio-net device
38+
of every bridge/macvtap VM on its next boot, and it requires `/dev/vhost-net`
39+
to be accessible to the account QEMU runs under, which the VMM cannot verify on
40+
the operator's behalf — see [Enabling vhost on a node](#enabling-vhost-on-a-node).
3641

3742
## Configuration
3843

@@ -47,22 +52,24 @@ bridge = "dstack-br0"
4752
vhost = true
4853
```
4954

50-
Queue pairs are not a node setting. They default to the VM's vCPU count, capped
51-
at 16, because the useful number follows the VM rather than the host — the guest
52-
driver uses at most one queue pair per vCPU. A deployment overrides that per VM,
53-
up to `max_net_queues`.
55+
Queue pairs are not a node setting. With vhost on they default to the VM's vCPU
56+
count, capped at 16, because the useful number follows the VM rather than the
57+
host — the guest driver uses at most one queue pair per vCPU. A deployment
58+
overrides that per VM, up to `max_net_queues`.
5459

5560
Raising `max_net_queues` above 16 widens what a deployment may ask for without
5661
moving the default's cap, so a larger VM never silently acquires a worse
5762
default. Lowering it below 16 does lower the default too, because a node that
5863
refuses a request for four queue pairs should not hand out sixteen by itself.
5964
The hard ceiling from any source is 64.
6065

61-
Turning vhost off also turns the multiqueue default off. Without vhost the QEMU
62-
main loop drains every queue on one thread, so extra queues buy little while
63-
still costing a netd interface, more MSI-X vectors, and a changed guest device.
64-
An explicit queue count is still honoured without vhost, since that combination
65-
is a deliberate request rather than a default.
66+
Without vhost the default is a single queue pair. The QEMU main loop drains
67+
every queue on one thread, so extra queues buy little while still costing a
68+
netd interface, more MSI-X vectors, and a changed guest device. An explicit
69+
queue count is still honoured without vhost, since that combination is a
70+
deliberate request rather than a default. The two defaults travelling together
71+
also means a node that never sets `vhost` keeps building the device its VMs
72+
have always had.
6673

6774
A VM overrides either value at deploy time, and `UpdateVm` changes them
6875
afterwards — the new values apply from the VM's next boot:
@@ -128,6 +135,31 @@ vhost state are not part of it, so retuning a NIC does not change app identity
128135
or require an on-chain update. Adding or removing a NIC does: the NIC count
129136
changes the guest's ACPI tables and therefore RTMR0.
130137

138+
## Enabling vhost on a node
139+
140+
Setting `vhost = true` in `[cvm.networking]` is a node-wide behaviour change:
141+
every bridge or macvtap VM that has not pinned its own data plane gets a
142+
different virtio-net device on its next boot — `vhost=on`, `mq=on` with
143+
vCPU-scaled queue pairs, and the matching MSI-X vector count. The device is not
144+
measured, so attestation and app identity are unaffected. Before flipping it:
145+
146+
1. **Verify `/dev/vhost-net` is accessible to the account QEMU runs under.**
147+
It is `root:kvm 0660` on Debian-family hosts, where adding the account to
148+
the `kvm` group suffices, and `root:root 0600` on several others. If the
149+
account lacks access, QEMU exits at launch and every affected VM stops
150+
restarting. The VMM warns at startup when its own access fails, but it
151+
cannot refuse on that basis — QEMU need not share its credentials.
152+
153+
2. **Restart `netd` before or together with the VMM.** Multiqueue bridge NICs
154+
are prepared by `netd`, and the VMM checks that `netd` echoes the queue
155+
count it built. An older `netd` fails that check; the launch is rolled back
156+
and fails with the reason in the VMM log, but the VM does not start until
157+
`netd` is upgraded.
158+
159+
3. **Roll back by setting `vhost = false`.** The node value reaches every VM
160+
that did not pin `vhost` explicitly, from its next boot; a VM that pinned
161+
`vhost = true` keeps it until updated.
162+
131163
## What each mode supports
132164

133165
| Mode | netdev | vhost | queues > 1 |
@@ -144,8 +176,8 @@ vhost switches bridge mode to a `tap` netdev driven by the same setuid
144176
no compiled-in default path for the `tap` netdev, so the VMM probes the known
145177
distribution locations; set `cvm.qemu_bridge_helper` if yours is elsewhere. If
146178
no helper is found the NIC falls back to the non-vhost `bridge` netdev with a
147-
warning, because vhost is a default and a default must not stop a node from
148-
booting VMs.
179+
warning, because a node-wide setting must not stop a node from booting VMs
180+
that never asked for it.
149181

150182
The helper returns exactly one descriptor, which is why more than one queue
151183
pair in bridge mode is created by `netd` instead: it adds a persistent
@@ -204,6 +236,21 @@ A VM with fewer vCPUs than queues leaves the extra pairs idle — `ethtool -l
204236
eth0` reports the smaller number. An explicit over-provision is not rejected at
205237
deployment, because `vmm-cli.py resize` can raise the vCPU count later.
206238

239+
Queue pairs also cost guest memory, and the default follows vCPUs while memory
240+
does not: each RX ring keeps 256 page-sized buffers posted, so 16 queue pairs
241+
pin roughly 16 MB of pages plus per-queue NAPI and socket state. On a 1 GB
242+
guest with 16 queue pairs under sustained load, RX refill failed with atomic
243+
order-0 page-allocation errors (`try_fill_recv`/`skb_page_frag_refill` in the
244+
guest log); the same shape with 2 GB ran clean. A many-vCPU VM given very
245+
little memory should also be given fewer queue pairs — as a working floor,
246+
keep at least ~128 MB of guest RAM per queue pair beyond the application's own
247+
needs. The TDX bounce-buffer pool is not the constraint: the guest kernel
248+
sizes swiotlb at 6% of RAM clamped to [64 MB, 1 GB], while peak demand is
249+
bounded by ring size at about 2 MB per queue pair — a deliberately undersized
250+
32 MB pool sustained full multiqueue line rate with zero `swiotlb buffer is
251+
full` events, so adding a `swiotlb=` boot parameter would only take memory
252+
away from the allocator that actually runs out.
253+
207254
`vectors` is derived, never configured: `2N + 2`, one vector per queue
208255
direction plus config and control. One queue pair emits no `mq=on` or
209256
`vectors=` at all, leaving the guest device line byte for byte identical to the
@@ -223,11 +270,29 @@ interface to describe, so it reports what the next launch would build instead --
223270
the same calculation, against the node configuration and manifest as they stand
224271
now, rather than the ones a finished boot ran under.
225272

226-
If that account lacks access, QEMU exits at startup and the VM never boots. The
227-
VMM does not pre-check this: QEMU need not share the VMM's credentials, so
228-
refusing a launch on the VMM's own access would block deployments the host can
229-
run. It only warns when the device node is missing outright, which is a fact
230-
about the host rather than about either account.
273+
If that account lacks access, QEMU exits at startup and the VM never boots —
274+
there is no fallback to the userspace backend at this point, on any QEMU
275+
version (verified on 8.2.2 and 10.2). What the per-VM launcher log shows
276+
depends on the version: QEMU 8.2 prints `warning: tap: open vhost char device
277+
failed: Permission denied` (once per queue) and then dies on `net/net.c:1185:
278+
net_client_init1: Assertion 'nc' failed` — an upstream bug
279+
([qemu#1486](https://gitlab.com/qemu-project/qemu/-/issues/1486)); later
280+
versions exit cleanly with `Could not open '/dev/vhost-net'`. Grep for either.
281+
The VMM does not refuse a launch over this: QEMU need not share the VMM's
282+
credentials, so a refusal based on the VMM's own access would block deployments
283+
the host can run. It warns instead — when the device node is missing outright,
284+
and when the VMM's own open is denied, since QEMU usually does share its
285+
account.
286+
287+
QEMU does have a *runtime* fallback, at a different failure point: once the
288+
netdev initialized with vhost, a later `vhost_net_start()` failure at guest
289+
driver activation logs `unable to start vhost net: <errno>: falling back on
290+
userspace virtio` and keeps the NIC working on the userspace data path. That
291+
path is reachable only after `/dev/vhost-net` was opened successfully at
292+
launch, so an access problem never lands there. If it does fire, it is the one
293+
case where `GetInfo` can overstate the data plane — the interface reports the
294+
vhost state the launch settled while the packets take the userspace path — and
295+
that QEMU log line is the indicator.
231296

232297
vhost-net works normally in a TDX guest: the virtio rings and buffers live in
233298
shared, unencrypted memory precisely so a host-side backend can reach them.

0 commit comments

Comments
 (0)