Skip to content

Commit 06b984e

Browse files
committed
feat(vmm): run virtio-net on vhost-net with configurable queue pairs
1 parent 027c62b commit 06b984e

16 files changed

Lines changed: 1642 additions & 206 deletions

File tree

dstack/vmm/rpc/proto/vmm_rpc.proto

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ message NetworkInterfaceStatus {
5252
optional string bridge_name = 4;
5353
// QEMU netdev id, e.g. "net0".
5454
optional string netdev_id = 5;
55+
// Effective vhost-net data plane state for this interface.
56+
bool vhost = 6;
57+
// Effective virtio-net queue pairs.
58+
uint32 queues = 7;
5559
}
5660

5761
// Structured log or lifecycle event emitted by the guest or runtime.
@@ -135,6 +139,12 @@ message NetworkingConfig {
135139
// Effective macvtap forwarding mode in responses. Deployment requests must
136140
// leave this empty because the mode is controlled by node configuration.
137141
string macvtap_mode = 4;
142+
// Move packet processing into the host kernel vhost-net data plane. Unset
143+
// inherits the node default. User mode has no vhost backend and ignores it.
144+
optional bool vhost = 5;
145+
// virtio-net queue pairs. Unset inherits the node default. Bounded by the
146+
// node's cvm.max_net_queues.
147+
optional uint32 queues = 6;
138148
}
139149

140150
// Requested GPU layout for a CVM.
@@ -301,6 +311,8 @@ message NetworkingCapabilities {
301311
reserved "forward_service_enabled";
302312
// Default bridge configured in vmm.toml [cvm.networking].bridge.
303313
string default_bridge = 4;
314+
// Largest virtio-net queue pair count a deployment request may ask for.
315+
uint32 max_queues = 5;
304316
}
305317

306318
// Aggregated metadata exposed through GetMeta.

dstack/vmm/src/app.rs

Lines changed: 115 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ use tracing::{debug, error, info, warn};
4242

4343
pub use image::{Image, ImageInfo};
4444
pub(crate) use network::{
45-
resolve_networking, resolved_networks, validate_resolved_network, validate_resolved_networks,
45+
clamp_queues_without_netd, needs_netd_interface, netd_available, resolve_networking,
46+
resolved_networks, validate_resolved_network, validate_resolved_networks,
4647
};
4748
pub use qemu::VmConfig;
4849
pub use workdir::VmWorkDir;
@@ -355,7 +356,7 @@ impl App {
355356
let vm_id = manifest.id.clone();
356357
let mut runtime_networks = vm_work_dir.runtime_networks();
357358
if runtime_networks.is_empty() && cids_assigned.contains_key(&vm_id) {
358-
runtime_networks = resolved_networks(&manifest, &self.config.cvm);
359+
runtime_networks = self.runtime_networks(&manifest);
359360
if let Err(err) = vm_work_dir.set_runtime_networks(&runtime_networks) {
360361
warn!(id = %vm_id, "failed to persist inferred runtime networks: {err}");
361362
}
@@ -454,7 +455,7 @@ impl App {
454455
append_boot_separator(&path);
455456
}
456457

457-
let mut runtime_networks = resolved_networks(&vm_config.manifest, &self.config.cvm);
458+
let mut runtime_networks = self.runtime_networks(&vm_config.manifest);
458459
let devices = self.try_allocate_gpus(&vm_config.manifest)?;
459460
let gpu_host_config = self.config.cvm.gpu.clone();
460461
let devices_to_sanitize = devices.clone();
@@ -547,19 +548,16 @@ impl App {
547548
vm: &VmConfig,
548549
networks: &mut [Networking],
549550
) -> Result<()> {
550-
if self.config.cvm.network_filter.mode == NetworkFilterMode::None
551-
&& !networks
552-
.iter()
553-
.any(|network| network.mode == NetworkingMode::Macvtap)
551+
if !networks
552+
.iter()
553+
.any(|network| needs_netd_interface(network, &self.config.cvm))
554554
{
555555
return Ok(());
556556
}
557557
let qemu_uid = Uid::effective().as_raw();
558558
let mut prepared = Vec::new();
559559
for (nic_index, network) in networks.iter_mut().enumerate() {
560-
if network.mode == NetworkingMode::Bridge
561-
&& self.config.cvm.network_filter.mode == NetworkFilterMode::None
562-
{
560+
if !needs_netd_interface(network, &self.config.cvm) {
563561
continue;
564562
}
565563
let identity = InterfaceIdentity {
@@ -572,21 +570,35 @@ impl App {
572570
&network.mac_prefix_bytes(),
573571
nic_index,
574572
);
573+
let queues = network.queue_pairs();
574+
let filtered = self.filters_bridge_traffic(network);
575575
let request = match network.mode {
576576
NetworkingMode::Bridge => NetdRequest::PrepareBridge(PrepareBridgeRequest {
577577
identity: identity.clone(),
578578
bridge: network.bridge.clone(),
579579
mac,
580580
qemu_uid,
581-
filter: self.config.cvm.network_filter.filter.clone(),
582-
parameters: self.config.cvm.network_filter.parameters.clone(),
581+
// An unfiltered TAP is only created for multiqueue, where
582+
// the node may not run libvirt at all.
583+
filter: if filtered {
584+
self.config.cvm.network_filter.filter.clone()
585+
} else {
586+
String::new()
587+
},
588+
parameters: if filtered {
589+
self.config.cvm.network_filter.parameters.clone()
590+
} else {
591+
BTreeMap::new()
592+
},
593+
queues,
583594
}),
584595
NetworkingMode::Macvtap => NetdRequest::PrepareMacvtap(PrepareMacvtapRequest {
585596
identity: identity.clone(),
586597
parent: network.parent.clone(),
587598
mac,
588599
qemu_uid,
589600
mode: network.macvtap_mode.clone(),
601+
queues,
590602
}),
591603
NetworkingMode::User | NetworkingMode::Custom => continue,
592604
};
@@ -600,73 +612,127 @@ impl App {
600612
&self.config.netd.socket,
601613
&NetdRequest::Remove {
602614
identity: identity.clone(),
615+
filtered,
603616
},
604617
)
605618
.await
606619
{
607620
warn!(%cleanup_error, "failed to roll back in-flight filtered network");
608621
}
609-
for identity in prepared.into_iter().rev() {
610-
if let Err(cleanup_error) = netd::request(
611-
&self.config.netd.socket,
612-
&NetdRequest::Remove { identity },
613-
)
614-
.await
615-
{
616-
warn!(%cleanup_error, "failed to roll back prepared filtered network");
617-
}
618-
}
619-
return Err(error).context("failed to prepare libvirt-filtered networking");
622+
self.roll_back_prepared_networks(prepared).await;
623+
return Err(error).context("failed to prepare netd-managed networking");
620624
}
621625
};
622-
if network.mode == NetworkingMode::Macvtap {
623-
network.device = response
624-
.device
625-
.context("netd response omitted macvtap device")?;
626+
prepared.push((identity.clone(), filtered));
627+
// Everything below runs after netd already built a host interface,
628+
// so a failure has to unwind the same way a failed Prepare does.
629+
let accepted = (|| {
630+
if network.mode == NetworkingMode::Macvtap {
631+
network.device = response
632+
.device
633+
.clone()
634+
.context("netd response omitted macvtap device")?;
635+
}
636+
// QEMU refuses a TAP whose IFF_MULTI_QUEUE state disagrees with
637+
// its own `queues=`, and reports it from inside the per-VM
638+
// launcher. netd echoes what it built, so a netd too old to
639+
// understand the request fails here, where the reason is
640+
// legible.
641+
if queues > 1 && response.queues != Some(queues) {
642+
bail!(
643+
"netd prepared interface {nic_index} with {} queue pairs instead of \
644+
{queues}; its version may predate multiqueue support",
645+
response.queues.map_or_else(
646+
|| "an unreported number of".to_string(),
647+
|q| q.to_string()
648+
)
649+
);
650+
}
651+
Ok(())
652+
})();
653+
if let Err(error) = accepted {
654+
self.roll_back_prepared_networks(prepared).await;
655+
return Err(error);
626656
}
627-
prepared.push(identity);
628657
}
629658
Ok(())
630659
}
631660

661+
/// Launch-time view of a VM's NICs: node defaults merged in, the
662+
/// vCPU-scaled queue count made concrete, and multiqueue dropped when this
663+
/// node has no netd to build the interface.
664+
pub(crate) fn runtime_networks(&self, manifest: &Manifest) -> Vec<Networking> {
665+
let requested = if manifest.networks.is_empty() {
666+
vec![self.config.cvm.networking.clone()]
667+
} else {
668+
manifest.networks.clone()
669+
};
670+
let mut resolved = resolved_networks(manifest, &self.config.cvm);
671+
clamp_queues_without_netd(
672+
&requested,
673+
&mut resolved,
674+
&self.config.cvm,
675+
netd_available(&self.config.netd.socket),
676+
);
677+
resolved
678+
}
679+
680+
/// Removes interfaces netd already built for a launch that then failed.
681+
async fn roll_back_prepared_networks(&self, prepared: Vec<(InterfaceIdentity, bool)>) {
682+
for (identity, filtered) in prepared.into_iter().rev() {
683+
if let Err(cleanup_error) = netd::request(
684+
&self.config.netd.socket,
685+
&NetdRequest::Remove { identity, filtered },
686+
)
687+
.await
688+
{
689+
warn!(%cleanup_error, "failed to roll back prepared network interface");
690+
}
691+
}
692+
}
693+
694+
/// Whether this NIC's host interface carries a libvirt nwfilter binding.
695+
/// Macvtap never does, and a bridge NIC only does when the node filters.
696+
fn filters_bridge_traffic(&self, network: &Networking) -> bool {
697+
network.mode == NetworkingMode::Bridge
698+
&& self.config.cvm.network_filter.mode == NetworkFilterMode::Libvirt
699+
}
700+
632701
pub(crate) async fn remove_filtered_networks(
633702
&self,
634703
vm_id: &str,
635704
networks: &[Networking],
636705
) -> Result<()> {
637-
if self.config.cvm.network_filter.mode == NetworkFilterMode::None
638-
&& !networks
639-
.iter()
640-
.any(|network| network.mode == NetworkingMode::Macvtap)
706+
if !networks
707+
.iter()
708+
.any(|network| needs_netd_interface(network, &self.config.cvm))
641709
{
642710
return Ok(());
643711
}
644712
let mut first_error = None;
645713
for (nic_index, network) in networks.iter().enumerate().rev() {
646-
if network.mode == NetworkingMode::Bridge
647-
&& self.config.cvm.network_filter.mode == NetworkFilterMode::None
648-
{
649-
continue;
650-
}
651-
if !matches!(
652-
network.mode,
653-
NetworkingMode::Bridge | NetworkingMode::Macvtap
654-
) {
714+
if !needs_netd_interface(network, &self.config.cvm) {
655715
continue;
656716
}
657717
let identity = InterfaceIdentity {
658718
instance_id: self.config.cvm.instance_id.clone(),
659719
vm_id: vm_id.to_string(),
660720
nic_index,
661721
};
662-
if let Err(error) =
663-
netd::request(&self.config.netd.socket, &NetdRequest::Remove { identity }).await
722+
if let Err(error) = netd::request(
723+
&self.config.netd.socket,
724+
&NetdRequest::Remove {
725+
identity,
726+
filtered: self.filters_bridge_traffic(network),
727+
},
728+
)
729+
.await
664730
{
665731
first_error.get_or_insert(error);
666732
}
667733
}
668734
if let Some(error) = first_error {
669-
return Err(error).context("failed to remove libvirt-filtered networking");
735+
return Err(error).context("failed to remove netd-managed networking");
670736
}
671737
Ok(())
672738
}
@@ -1059,7 +1125,7 @@ impl App {
10591125
let already_running = cids_assigned.contains_key(&vm_id);
10601126
let mut runtime_networks = vm_work_dir.runtime_networks();
10611127
if runtime_networks.is_empty() && already_running {
1062-
runtime_networks = resolved_networks(&manifest, &self.config.cvm);
1128+
runtime_networks = self.runtime_networks(&manifest);
10631129
if let Err(err) = vm_work_dir.set_runtime_networks(&runtime_networks) {
10641130
warn!(id = %vm_id, "failed to persist inferred runtime networks: {err}");
10651131
}
@@ -2241,6 +2307,8 @@ mod tests {
22412307
dhcp_start: String::new(),
22422308
restrict: false,
22432309
netdev: String::new(),
2310+
vhost: None,
2311+
queues: None,
22442312
}];
22452313

22462314
workdir.put_manifest(&manifest)?;
@@ -2506,6 +2574,8 @@ mod tests {
25062574
dhcp_start: String::new(),
25072575
restrict: false,
25082576
netdev: String::new(),
2577+
vhost: None,
2578+
queues: None,
25092579
}];
25102580
let user_manifest = test_manifest(2048);
25112581
let image = test_tdx_image(true);

0 commit comments

Comments
 (0)