qemu-tool 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. qemu_tool-1.4.0/PKG-INFO +47 -0
  2. qemu_tool-1.4.0/compose/vfio-user-ernic-2vm/README.md +116 -0
  3. qemu_tool-1.4.0/compose/vfio-user-ernic-2vm/docker-compose.yml +180 -0
  4. qemu_tool-1.4.0/compose/vfio-user-ernic-rocjitsu-scale-out/README.md +276 -0
  5. qemu_tool-1.4.0/compose/vfio-user-ernic-rocjitsu-scale-out/docker-compose.yml +762 -0
  6. qemu_tool-1.4.0/compose/vfio-user-ernic-rocjitsu-scale-out/prometheus.yml +56 -0
  7. qemu_tool-1.4.0/compose/vfio-user-ernic-rocjitsu-vm/README.md +103 -0
  8. qemu_tool-1.4.0/compose/vfio-user-ernic-rocjitsu-vm/docker-compose.yml +93 -0
  9. qemu_tool-1.4.0/compose/vfio-user-ernic-vm/docker-compose.yml +69 -0
  10. qemu_tool-1.4.0/compose/vfio-user-rocjitsu-vm/docker-compose.yml +81 -0
  11. qemu_tool-1.4.0/env.example +133 -0
  12. qemu_tool-1.4.0/env.scale-out +255 -0
  13. qemu_tool-1.4.0/packages.d/packages-default +32 -0
  14. qemu_tool-1.4.0/packages.d/packages-minimal +6 -0
  15. qemu_tool-1.4.0/pyproject.toml +97 -0
  16. qemu_tool-1.4.0/qemu-tool/__init__.py +6 -0
  17. qemu_tool-1.4.0/qemu-tool/__main__.py +4 -0
  18. qemu_tool-1.4.0/qemu-tool/caps.py +102 -0
  19. qemu_tool-1.4.0/qemu-tool/cli.py +541 -0
  20. qemu_tool-1.4.0/qemu-tool/compose.py +102 -0
  21. qemu_tool-1.4.0/qemu-tool/config.py +107 -0
  22. qemu_tool-1.4.0/qemu-tool/envfile.py +143 -0
  23. qemu_tool-1.4.0/qemu-tool/ernic_stats.py +241 -0
  24. qemu_tool-1.4.0/qemu-tool/gen_compose.py +610 -0
  25. qemu_tool-1.4.0/qemu-tool/gen_vm.py +866 -0
  26. qemu_tool-1.4.0/qemu-tool/identity.py +44 -0
  27. qemu_tool-1.4.0/qemu-tool/libvirt_xml.py +717 -0
  28. qemu_tool-1.4.0/qemu-tool/list_vms.py +357 -0
  29. qemu_tool-1.4.0/qemu-tool/man/qemu-tool.1 +683 -0
  30. qemu_tool-1.4.0/qemu-tool/py.typed +0 -0
  31. qemu_tool-1.4.0/qemu-tool/resources.py +48 -0
  32. qemu_tool-1.4.0/qemu-tool/run_vm.py +564 -0
  33. qemu_tool-1.4.0/qemu_tool.egg-info/PKG-INFO +47 -0
  34. qemu_tool-1.4.0/qemu_tool.egg-info/SOURCES.txt +38 -0
  35. qemu_tool-1.4.0/qemu_tool.egg-info/dependency_links.txt +1 -0
  36. qemu_tool-1.4.0/qemu_tool.egg-info/entry_points.txt +2 -0
  37. qemu_tool-1.4.0/qemu_tool.egg-info/top_level.txt +1 -0
  38. qemu_tool-1.4.0/setup.cfg +4 -0
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: qemu-tool
3
+ Version: 1.4.0
4
+ Summary: CLI tool for running and generating QEMU VMs
5
+ Author: Stephen Bates
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sbates130272/qemu-minimal
8
+ Project-URL: Repository, https://github.com/sbates130272/qemu-minimal
9
+ Project-URL: Changelog, https://github.com/sbates130272/qemu-minimal/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/sbates130272/qemu-minimal/issues
11
+ Keywords: qemu,kvm,vm,vfio,cloud-init,libvirt,rocm
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: System :: Emulators
23
+ Classifier: Topic :: System :: Systems Administration
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+
28
+ # qemu-tool
29
+
30
+ A single CLI for creating and running QEMU virtual machines: cloud-init image
31
+ generation for x86_64, ARM64 and RISC-V, a flexible runner with NVMe, VFIO,
32
+ CXL, VirtFS and QMP support, and libvirt domain XML round-tripping.
33
+
34
+ ```bash
35
+ pipx install qemu-tool
36
+ qemu-tool gen-vm --vm-name vm1 --arch x86_64
37
+ qemu-tool run-vm --vm-name vm1 --ssh-port 2222
38
+ ```
39
+
40
+ The wheel bundles its compose stacks and package manifests, so a rootless
41
+ install is fully functional. Settings come from
42
+ `$XDG_CONFIG_HOME/qemu-tool/env` and images default to
43
+ `$XDG_DATA_HOME/qemu-tool/images`. Installing the Debian package instead
44
+ puts both at their system locations.
45
+
46
+ Full documentation, the Docker Compose stacks and the Ansible playbooks live
47
+ in the [qemu-minimal repository](https://github.com/sbates130272/qemu-minimal).
@@ -0,0 +1,116 @@
1
+ # vfio-user ernic 2-VM mesh — Docker Compose stack
2
+
3
+ Spins up two VMs connected via a rocm-ernic TCP manager/worker mesh.
4
+ rocjitsu GPU emulators are optional per VM via Compose profiles
5
+ (`rocjitsu-vm1`, `rocjitsu-vm2`). The ernic-hub relays L2 Ethernet
6
+ frames between VMs so they share a private network; RDMA operations go
7
+ peer-to-peer over the same TCP connections.
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ ernic-hub (tcp:manager:listen:6320) ──── VM 1 (qemu-1, SSH :2222)
13
+ │ └─ ernic-1.sock [+ rocjitsu-1.sock with profile rocjitsu-vm1]
14
+ │ TCP mesh
15
+ ernic-worker (tcp:worker:ernic-hub) ──── VM 2 (qemu-2, SSH :2223)
16
+ └─ ernic-2.sock [+ rocjitsu-2.sock with profile rocjitsu-vm2]
17
+ ```
18
+
19
+ ## Prerequisites
20
+
21
+ - Docker with Compose v2
22
+ - `/dev/kvm` on the host (recommended)
23
+ - Two qcow2 VM images — each VM must have its own overlay (they can share a
24
+ backing file; see below)
25
+
26
+ ## Quick start
27
+
28
+ ```sh
29
+ # 1. Configure
30
+ cp qemu/env.example qemu/.env
31
+ $EDITOR qemu/.env # set VM1_NAME, VM2_NAME, VM_IMAGES_DIR
32
+
33
+ # 2. Create a second VM overlay if you only have one image
34
+ # (both VMs need distinct qcow2 files)
35
+ qemu-tool gen-vm --vm-name qemu-minimal-2 --backing-file \
36
+ /var/lib/qemu-tool/images/qemu-minimal-backing.qcow2
37
+
38
+ # 3a. Start ernic-only (no GPUs)
39
+ qemu-tool compose --stack vfio-user-ernic-2vm --vm-name qemu-minimal up
40
+
41
+ # 3b. Both VMs with GPUs
42
+ qemu-tool compose --stack vfio-user-ernic-2vm --vm-name qemu-minimal \
43
+ --profile rocjitsu-vm1 --profile rocjitsu-vm2 up
44
+
45
+ # 3c. VM 1 with GPU, VM 2 without
46
+ qemu-tool compose --stack vfio-user-ernic-2vm --vm-name qemu-minimal \
47
+ --profile rocjitsu-vm1 up
48
+
49
+ # 4. Connect to each VM
50
+ ssh -p 2222 ubuntu@localhost # VM 1
51
+ ssh -p 2223 ubuntu@localhost # VM 2
52
+ ```
53
+
54
+ ## Configuring the NIC in each guest
55
+
56
+ Run `vm-ernic.yml --tags configure` once per guest, and **pass `vm_index`** —
57
+ it is what decides the address the guest takes on `rocm-ernic0`. The default is
58
+ `1`, so running the play twice unchanged puts both guests on `192.168.200.10`
59
+ and the mesh silently has an address collision rather than a working pair.
60
+
61
+ ```sh
62
+ ansible-playbook -i "vm1," ansible/playbooks/vm-ernic.yml --tags configure \
63
+ -e ansible_host=localhost -e ansible_port=2222 -e ansible_user=ubuntu \
64
+ -e vm_index=1 # -> 192.168.200.10
65
+
66
+ ansible-playbook -i "vm2," ansible/playbooks/vm-ernic.yml --tags configure \
67
+ -e ansible_host=localhost -e ansible_port=2223 -e ansible_user=ubuntu \
68
+ -e vm_index=2 # -> 192.168.200.20
69
+ ```
70
+
71
+ The address is derived as `192.168.200.<10 * vm_index>`, which is the same
72
+ arithmetic upstream's `vm-register.yml` applies to its instance manifest, so
73
+ these are the addresses the rocm-ernic system test expects. `-e vm_ip=<addr>`
74
+ overrides the result outright if you need a different scheme.
75
+
76
+ ## Environment variables
77
+
78
+ | Variable | Default | Description |
79
+ |---|---|---|
80
+ | `VM1_NAME` | `qemu-minimal` | VM 1 image basename (no extension) |
81
+ | `VM2_NAME` | `qemu-minimal-2` | VM 2 image basename (no extension) |
82
+ | `VM_IMAGES_DIR` | `/var/lib/qemu-tool/images` | Directory containing both VM images |
83
+ | `VM1_SSH_PORT` | `2222` | Host port forwarded to VM 1 SSH |
84
+ | `VM2_SSH_PORT` | `2223` | Host port forwarded to VM 2 SSH |
85
+ | `VM_VCPUS` | `4` | vCPU count **per VM** (so 8 total); 4 is the floor for `ionic_rdma` |
86
+ | `VM_VMEM` | `8192` | RAM in MiB **per VM** (so 16 GiB total) |
87
+ | `ERNIC_TCP_PORT` | `6320` | TCP port for ernic manager/worker mesh |
88
+ | `ROCJITSU_CONFIG` | `gfx1250_mi455x.json` | rocjitsu GPU config (used when rocjitsu profiles active) |
89
+ | `VM_SHM_SIZE` | `8g` | Container shared memory (must be >= VM_VMEM MiB) |
90
+
91
+ ## Compose profiles
92
+
93
+ | Profile | Effect |
94
+ |---|---|
95
+ | *(none)* | ernic mesh only — no GPU services |
96
+ | `rocjitsu-vm1` | add rocjitsu GPU for VM 1 |
97
+ | `rocjitsu-vm2` | add rocjitsu GPU for VM 2 |
98
+ | `rocjitsu-vm1 rocjitsu-vm2` | both VMs get a GPU |
99
+
100
+
101
+ ## VM image requirements
102
+
103
+ Each VM needs its **own qcow2 overlay** — two VMs cannot safely share a
104
+ single overlay file. They can however share the same backing file:
105
+
106
+ ```sh
107
+ # Create VM2's overlay from VM1's existing backing file
108
+ qemu-img create -f qcow2 -b /path/to/vm1-backing.qcow2 \
109
+ -F qcow2 /path/to/qemu-minimal-2.qcow2
110
+ ```
111
+
112
+ ## Known issues
113
+
114
+ See [../vfio-user-ernic-rocjitsu-vm/README.md](../vfio-user-ernic-rocjitsu-vm/README.md) for the amdgpu
115
+ driver panic workaround — apply it to both VM images before starting the
116
+ stack.
@@ -0,0 +1,180 @@
1
+ ---
2
+ # 2-VM ernic mesh stack with optional rocjitsu GPUs
3
+ #
4
+ # Always starts: ernic-hub (VM 1 NIC) + ernic-worker (VM 2 NIC) + qemu-1 + qemu-2.
5
+ # rocjitsu GPUs are opt-in per VM via Compose profiles:
6
+ # --profile rocjitsu-vm1 add GPU to VM 1
7
+ # --profile rocjitsu-vm2 add GPU to VM 2
8
+ #
9
+ # Usage:
10
+ # cp qemu/env.example qemu/.env && $EDITOR qemu/.env
11
+ # # ernic-only:
12
+ # qemu-tool compose --stack vfio-user-ernic-2vm --vm-name vm1-name up
13
+ # # both VMs with GPUs:
14
+ # qemu-tool compose --stack vfio-user-ernic-2vm --vm-name vm1-name \
15
+ # --profile rocjitsu-vm1 --profile rocjitsu-vm2 up
16
+ # ssh -p ${VM1_SSH_PORT:-2222} ubuntu@localhost # VM 1
17
+ # ssh -p ${VM2_SSH_PORT:-2223} ubuntu@localhost # VM 2
18
+
19
+ x-gpu-common: &gpu-common
20
+ restart: on-failure
21
+
22
+ x-qemu-common: &qemu-common
23
+ image: "${QEMU_IMAGE:-docker.io/sbates130272/batesste-ci-images-ubuntu-qemu-libvfio-user:20260929.g2bdbd16-qemu11.1.2-vfu.8039244}"
24
+ volumes:
25
+ - vfu-sockets:/run/vfu:ro
26
+ - "${VM_IMAGES_DIR:-/var/lib/qemu-tool/images}:${VM_IMAGES_DIR:-/var/lib/qemu-tool/images}"
27
+ - "${QEMU_TOOL_SRC:-../../..}:/qemu-tool-src:ro"
28
+ devices:
29
+ - /dev/kvm
30
+ shm_size: "${VM_SHM_SIZE:-8g}"
31
+ restart: "no"
32
+
33
+ services:
34
+ # ---- rocm-ernic NIC for VM 1: TCP manager (hub) ----
35
+ ernic-hub:
36
+ <<: *gpu-common
37
+ image: "${ERNIC_IMAGE:-docker.io/sbates130272/batesste-ci-images-ubuntu-rocm-ernic:20260929.g2bdbd16-ernic.1484557-vfu.8039244}"
38
+ environment:
39
+ - ERNIC_DEBUG_MESH
40
+ command:
41
+ - rocm-ernic
42
+ - -s
43
+ - /run/vfu/ernic-1.sock
44
+ - -b
45
+ - "tcp:manager:listen:${ERNIC_TCP_PORT:-6320}"
46
+ - -m
47
+ - "${ERNIC1_MAC:-72:6f:63:6d:00:01}"
48
+ volumes:
49
+ - vfu-sockets:/run/vfu
50
+ healthcheck:
51
+ test: ["CMD", "sh", "-c", "test -S /run/vfu/ernic-1.sock"]
52
+ interval: 2s
53
+ timeout: 5s
54
+ retries: 30
55
+ start_period: 5s
56
+
57
+ # ---- rocm-ernic NIC for VM 2: TCP worker (spoke) ----
58
+ ernic-worker:
59
+ <<: *gpu-common
60
+ image: "${ERNIC_IMAGE:-docker.io/sbates130272/batesste-ci-images-ubuntu-rocm-ernic:20260929.g2bdbd16-ernic.1484557-vfu.8039244}"
61
+ environment:
62
+ - ERNIC_DEBUG_MESH
63
+ command:
64
+ - rocm-ernic
65
+ - -s
66
+ - /run/vfu/ernic-2.sock
67
+ - -b
68
+ - "tcp:worker:ernic-hub:${ERNIC_TCP_PORT:-6320}"
69
+ - -m
70
+ - "${ERNIC2_MAC:-72:6f:63:6d:00:02}"
71
+ volumes:
72
+ - vfu-sockets:/run/vfu
73
+ depends_on:
74
+ ernic-hub:
75
+ condition: service_healthy
76
+ healthcheck:
77
+ test: ["CMD", "sh", "-c", "test -S /run/vfu/ernic-2.sock"]
78
+ interval: 2s
79
+ timeout: 5s
80
+ retries: 30
81
+ start_period: 5s
82
+
83
+ # ---- rocjitsu GPU for VM 1 (opt-in: --profile rocjitsu-vm1) ----
84
+ rocjitsu-1:
85
+ <<: *gpu-common
86
+ profiles: [rocjitsu-vm1]
87
+ image: "${ROCJITSU_IMAGE:-docker.io/sbates130272/batesste-ci-images-ubuntu-rocm-rocjitsu:20260929.g2bdbd16-rocjitsu.f2d13fb}"
88
+ command:
89
+ - rocjitsu
90
+ - --config
91
+ - "/usr/local/share/rocjitsu/configs/${ROCJITSU_CONFIG:-gfx1250_mi455x.json}"
92
+ - --vfio-socket
93
+ - /run/vfu/rocjitsu-1.sock
94
+ volumes:
95
+ - vfu-sockets:/run/vfu
96
+ healthcheck:
97
+ test: ["CMD", "sh", "-c", "test -S /run/vfu/rocjitsu-1.sock"]
98
+ interval: 2s
99
+ timeout: 5s
100
+ retries: 30
101
+ start_period: 5s
102
+
103
+ # ---- rocjitsu GPU for VM 2 (opt-in: --profile rocjitsu-vm2) ----
104
+ rocjitsu-2:
105
+ <<: *gpu-common
106
+ profiles: [rocjitsu-vm2]
107
+ image: "${ROCJITSU_IMAGE:-docker.io/sbates130272/batesste-ci-images-ubuntu-rocm-rocjitsu:20260929.g2bdbd16-rocjitsu.f2d13fb}"
108
+ command:
109
+ - rocjitsu
110
+ - --config
111
+ - "/usr/local/share/rocjitsu/configs/${ROCJITSU_CONFIG:-gfx1250_mi455x.json}"
112
+ - --vfio-socket
113
+ - /run/vfu/rocjitsu-2.sock
114
+ volumes:
115
+ - vfu-sockets:/run/vfu
116
+ healthcheck:
117
+ test: ["CMD", "sh", "-c", "test -S /run/vfu/rocjitsu-2.sock"]
118
+ interval: 2s
119
+ timeout: 5s
120
+ retries: 30
121
+ start_period: 5s
122
+
123
+ # ---- VM 1: always connected to ernic-hub; GPU added when rocjitsu-vm1 profile active ----
124
+ qemu-1:
125
+ <<: *qemu-common
126
+ depends_on:
127
+ ernic-hub:
128
+ condition: service_healthy
129
+ rocjitsu-1:
130
+ condition: service_healthy
131
+ required: false
132
+ ports:
133
+ - "${VM1_SSH_PORT:-2222}:2222"
134
+ entrypoint:
135
+ - /bin/sh
136
+ - -c
137
+ - |
138
+ cp -r /qemu-tool-src/qemu /tmp/qemu-tool-build
139
+ PIPX_BIN_DIR=/usr/local/bin pipx install --force /tmp/qemu-tool-build
140
+ socks=$(ls /run/vfu/ernic-1.sock /run/vfu/rocjitsu-1.sock 2>/dev/null | tr '\n' ',' | sed 's/,$//')
141
+ exec qemu-tool run-vm \
142
+ --images "${VM_IMAGES_DIR:-/var/lib/qemu-tool/images}" \
143
+ --vm-name "${VM1_NAME:-qemu-minimal}" \
144
+ --vcpus "${VM_VCPUS:-4}" \
145
+ --vmem "${VM_VMEM:-8192}" \
146
+ --ssh-port 2222 \
147
+ --vfio-userdev "$$socks"
148
+
149
+ # ---- VM 2: always connected to ernic-worker; GPU added when rocjitsu-vm2 profile active ----
150
+ qemu-2:
151
+ <<: *qemu-common
152
+ depends_on:
153
+ ernic-worker:
154
+ condition: service_healthy
155
+ rocjitsu-2:
156
+ condition: service_healthy
157
+ required: false
158
+ ports:
159
+ - "${VM2_SSH_PORT:-2223}:2222"
160
+ entrypoint:
161
+ - /bin/sh
162
+ - -c
163
+ - |
164
+ cp -r /qemu-tool-src/qemu /tmp/qemu-tool-build
165
+ PIPX_BIN_DIR=/usr/local/bin pipx install --force /tmp/qemu-tool-build
166
+ socks=$(ls /run/vfu/ernic-2.sock /run/vfu/rocjitsu-2.sock 2>/dev/null | tr '\n' ',' | sed 's/,$//')
167
+ exec qemu-tool run-vm \
168
+ --images "${VM_IMAGES_DIR:-/var/lib/qemu-tool/images}" \
169
+ --vm-name "${VM2_NAME:-qemu-minimal-2}" \
170
+ --vcpus "${VM_VCPUS:-4}" \
171
+ --vmem "${VM_VMEM:-8192}" \
172
+ --ssh-port 2222 \
173
+ --vfio-userdev "$$socks"
174
+
175
+ volumes:
176
+ vfu-sockets:
177
+ driver: local
178
+ driver_opts:
179
+ type: tmpfs
180
+ device: tmpfs
@@ -0,0 +1,276 @@
1
+ # vfio-user-ernic-rocjitsu-scale-out
2
+
3
+ An N-VM rocm-ernic mesh, one emulated RDMA NIC and one rocjitsu GPU per VM.
4
+ Same topology as `vfio-user-ernic-2vm`, sized by a variable instead of by hand.
5
+
6
+ **`docker-compose.yml` and `prometheus.yml` in this directory are generated.**
7
+ Edit [`qemu/env.scale-out`](../../env.scale-out) and regenerate; do not edit
8
+ them directly. Both are committed so the stack stays reviewable in a diff and
9
+ usable with plain `docker compose`, but a hand edit is lost on the next
10
+ regeneration and `gen-compose --check` will fail in CI.
11
+
12
+ ```bash
13
+ qemu-tool gen-compose --env-file qemu/env.scale-out
14
+ ```
15
+
16
+ ## Why generated
17
+
18
+ The 2-VM stack hand-writes every socket path, MAC, SSH port and qcow2 name.
19
+ That does not survive 8 VMs, let alone the mesh's 64-node ceiling
20
+ (`rdma_backend_tcp.c`), and hand-editing 24 services is exactly how two VMs end
21
+ up sharing a MAC — which presents as an intermittent RDMA fault, not as a
22
+ config error.
23
+
24
+ Per-VM values in the settings file are comma-lists, not numbered variables:
25
+ `VM1_NAME`/`VM2_NAME` does not scale. A bare value broadcasts to every VM, a
26
+ list of length `VM_COUNT` applies positionally, and any other length is an
27
+ error. The exception is a key naming a field that is *already* a list
28
+ (`VM_EXTRA_HOSTFWD` and friends), where the commas mean several values for one
29
+ VM and the whole value goes to every VM. `env.scale-out` documents this at
30
+ length because getting it backwards fails silently.
31
+
32
+ ## Bringing it up
33
+
34
+ The VMs need qcow2 overlays. Fetch the published backing image once and
35
+ overlay the rest on it — `gen-compose` prints these commands with the names
36
+ filled in:
37
+
38
+ ```bash
39
+ qemu-tool gen-vm --env-file qemu/env.scale-out \
40
+ --vm-name stebates-fleet-1 --backing-image "$VM_BACKING_IMAGE"
41
+ for n in $(seq 2 8); do
42
+ qemu-tool gen-vm --env-file qemu/env.scale-out \
43
+ --vm-name stebates-fleet-$n \
44
+ --backing-file images/stebates-fleet-1-backing.qcow2
45
+ done
46
+
47
+ qemu-tool compose --env-file qemu/env.scale-out \
48
+ --stack vfio-user-ernic-rocjitsu-scale-out --profile metrics up -d
49
+ ```
50
+
51
+ One backing file serves the whole fleet, which is why `VM_BACKING_SHARED=true`
52
+ is not optional here: all N VMs open it at once, and that needs
53
+ `backing.file.locking=off`.
54
+
55
+ ## Topology
56
+
57
+ | Service | Role |
58
+ |---|---|
59
+ | `ernic-1` | mesh manager, `tcp:manager:listen:$ERNIC_TCP_PORT` |
60
+ | `ernic-2..N` | workers, dialling the manager's **address**, not its name |
61
+ | `rocjitsu-1..N` | one emulated GPU per VM |
62
+ | `qemu-1..N` | the guests, each attached to its own two sockets |
63
+ | `ernic-stats-exporter` | `--profile metrics`, serves `:9840/metrics` |
64
+ | `prometheus` | `--profile metrics`, scrapes both sources |
65
+
66
+ Derived per VM `n`: name `<VM_NAME_PREFIX>-<n>`, SSH port
67
+ `VM_SSH_PORT_BASE + n - 1`, MAC `<ERNIC_MAC_PREFIX>:<n>>8>:<n&0xff>`, guest IP
68
+ `<ERNIC_GUEST_SUBNET>.<10+n>`. Guests start at `.11`: the manager claims `.1`
69
+ as its DHCP `server_ip` and intercepts ARP for it, so a VM placed there is
70
+ unreachable in a way that looks like a driver fault.
71
+
72
+ ## Addressing
73
+
74
+ Every service has a fixed address on the `fleet` network —
75
+ `$FLEET_NET_PREFIX.<family>.<n>`, family `0` infra, `1` ernic, `2` rocjitsu,
76
+ `3` qemu — and **nothing in the stack resolves a service name at run time**.
77
+ Workers dial the manager's address, Prometheus scrapes addresses.
78
+
79
+ That is a scaling fix, not tidiness; see the ramp below.
80
+
81
+ Sockets (`/run/vfu/{ernic,rocjitsu}-<n>.sock`) are derived only, with no
82
+ override — they are internal wiring between services generated together, so an
83
+ override could only desynchronise them. Each guest is passed its own two
84
+ explicitly rather than globbing the directory, which the single-VM stacks do
85
+ and which would attach every device to every VM here.
86
+
87
+ ## `ernic-1` does not restart
88
+
89
+ Deliberate, and the one thing worth knowing before reading the logs. If the
90
+ mesh manager restarts, every guest driver enters a DSR initialization timeout
91
+ on the next `modprobe ionic_rdma`, and the vfio-user sessions QEMU holds are
92
+ stale — reloading guest modules does not recover them. The only fix is
93
+ `docker compose down && up` for the whole stack. `restart: on-failure` would
94
+ turn that into a quiet wedge instead of a visible crash. See AGENTS.md issue 7.
95
+
96
+ ## Metrics
97
+
98
+ Opt-in with `--profile metrics`. Two sources:
99
+
100
+ - **Guest node-exporter**, scraped at the qemu container's fleet address on
101
+ `:9100`. The guests are behind
102
+ QEMU SLIRP inside their containers and have no address on this network, so
103
+ the *container* is the target and `VM_EXTRA_HOSTFWD=tcp::9100-:9100` forwards
104
+ through to the guest. Each container has its own netns, so every VM uses the
105
+ same port with no arithmetic and nothing published to the host.
106
+ - **ernic stats**, from the dumps each `rocm-ernic -S` writes to the shared
107
+ `ernic-stats` tmpfs about once a second: per-QP bytes, doorbells, CQEs, WQEs
108
+ by opcode, MMIO and interrupt counts. `qemu-tool ernic-stats` parses them;
109
+ its metric and label names match rocm-ernic's own exporter, so that project's
110
+ `grafana/ernic-dashboard.json` works here unmodified.
111
+
112
+ The in-guest GPU exporters (`amd-metrics-exporter` on :5000,
113
+ `hsa-snoop-prometheus` on :9488) are deliberately *not* scraped: they poll the
114
+ emulated device, and rocjitsu serves single-threaded, so every scrape comes out
115
+ of the same thread as the workload being measured.
116
+
117
+ ## Staleness
118
+
119
+ `docker-compose.yml` carries an `# env-sha256:` header recording the settings
120
+ file it was generated from. `qemu-tool compose` recomputes it and warns on
121
+ stderr when they disagree, naming the regeneration command. It never blocks —
122
+ `down` against a stale tree is how you recover from a bad edit.
123
+
124
+ ## Ceilings
125
+
126
+ ### Static limits
127
+
128
+ | Limit | Value |
129
+ |---|---|
130
+ | **mesh registration, measured** | **~40 nodes — the real ceiling** |
131
+ | rocm-ernic mesh topology payload | 64 nodes (`rdma_backend_tcp.c:2570`) |
132
+ | `VM_SHM_SIZE` × `VM_COUNT` ≤ `/dev/shm` | ~30 VMs at 8g on a 252G host |
133
+ | `VM_VCPUS` × `VM_COUNT` ≤ `nproc` | 32 VMs at 4 vCPU on 128 cores |
134
+
135
+ The vCPU row is the one that actually binds; see *Measured* below. It tolerates
136
+ about 25% of overcommit and then the fleet starts losing guests.
137
+
138
+ `VM_VCPUS` must not drop below 4 on an ernic stack: `ionic_lif_size()` derives
139
+ its EQ count from `num_online_cpus()` and `ionic_create_rdma_admin()` rejects
140
+ fewer than `IONIC_EQ_COUNT_MIN`, so `ionic_rdma` never probes. The only symptom
141
+ is "Failed to register ibdev" in the guest log.
142
+
143
+ ### Measured
144
+
145
+ Ramped on a 128-core / 503G / 252G-`/dev/shm` host, 4 vCPU and 8G per VM, timed
146
+ from `compose up` to every guest's sshd banner. Nothing was overcommitted at
147
+ the top of this range: 48 VMs used 107G of 503G.
148
+
149
+ | VMs | containers | vCPU allocated | guests at sshd | lost |
150
+ |---|---|---|---|---|
151
+ | 4 | 12 | 16 | 26s | 0 |
152
+ | 8 | 24 | 32 | 28s | 0 |
153
+ | 16 | 48 | 64 | 31s | 0 |
154
+ | 32 | 96 | 128 | 46s | 0 |
155
+ | **40** | **120** | **160** | **117s** | **0** |
156
+ | 40 (repeat) | 122 | 160 | 37 of 40 | 3 |
157
+ | 44 | 132 | 176 | 41 of 44 | 3 |
158
+ | 48 | 144 | 192 | 41 of 48 | 7 |
159
+ | 64 | 196 | 256 | 40 of 64 (12 min) | 24 |
160
+
161
+ Sub-linear to 32 — 8× the VMs for 1.8× the time, with vCPU allocation exactly
162
+ matching the host's 128 cores. **32 VMs is the largest size that booted clean
163
+ every time. 40 is marginal**: the same stack on the same host booted 40/40
164
+ once and lost 3 on a repeat, so treat the 40 row as a coin flip rather than a
165
+ supported size. The boot burst is what decides it — load average peaked at
166
+ **417** on 128 cores during the losing run, and fell back to 54 once the
167
+ survivors were up.
168
+
169
+ Memory is not what runs out: 48 VMs used 107G of 503G, and the fleet at 44 had
170
+ 396G free. What runs out is CPU. Every VM needs 4 vCPU (`ionic_rdma` will not
171
+ probe below `IONIC_EQ_COUNT_MIN`), so 32 VMs already saturate a 128-core host
172
+ and 40 is 25% over. Guests idle after boot, which is why 40 works at all — but
173
+ the boot burst is not idle, and load peaked at 174 during the 40-VM start.
174
+
175
+ ### What breaks first, and why it is not memory
176
+
177
+ Two of the three failure modes found at 48 were name resolution, and both are
178
+ fixed above; the third is upstream's and is not.
179
+
180
+ 1. *(fixed — static addressing)* Docker runs one embedded resolver per network
181
+ and a 144-container start burst saturates it. Workers died with `Failed to
182
+ resolve host 'ernic-1': Temporary failure in name resolution`. Worse, the
183
+ mesh advertises each node by `gethostname()` and peers resolve that name to
184
+ dial it, so every one of the fleet's N×(N−1) peer links went through the
185
+ same resolver. Each container is now named after its own address, which
186
+ makes those a numeric `getaddrinfo`. Measured effect at 48: **4497 → 0**
187
+ resolution failures.
188
+ 2. *(fixed — retry)* The entrypoint's `pipx install` reaches pypi.org, and 48
189
+ of those racing killed 12 guests before QEMU started. Retried five times.
190
+ 3. *(upstream, unfixed)* The manager's heartbeat timeout is 20 s and is not
191
+ tunable — `rocm-ernic --help` exposes no knob. During a 48-VM start the
192
+ manager slips past it and declares live nodes dead (`Node N failed health
193
+ check (last heartbeat: 20 seconds ago)`). The evicted worker exits, its
194
+ vfio-user socket disappears, and the guest that had attached to it dies
195
+ with `vfio-user: timed out waiting for reply`. At both 44 and 48 the lost
196
+ guests matched the restarted ernics one for one — 3 and 7 respectively.
197
+
198
+ This is the CPU limit wearing a protocol's clothes. The heartbeat is a wall
199
+ clock, so oversubscribing the host does not merely slow the fleet down, it
200
+ makes the manager mistake slow nodes for dead ones. Which is why 40 VMs
201
+ boot cleanly in 117 s and 44 lose three: nothing is short of memory at
202
+ either size.
203
+
204
+ Failure 3 compounds because **the mesh allocates a new node id on every
205
+ reconnect and never reclaims the old one**. The 64-node cap is therefore a cap
206
+ on *connection events*, not on VMs. The same 48-VM fleet reached id 61 before
207
+ the fixes above and 50 after them — so churn, not `VM_COUNT`, is what consumes
208
+ the budget, and a fleet that flaps its way past 64 stops working for reasons
209
+ that have nothing to do with its size.
210
+
211
+ ### What actually kills the fleet
212
+
213
+ The node id is not the only thing a reconnect consumes. **The manager does not
214
+ close the socket of a node it evicts**, so each eviction leaks a file
215
+ descriptor, and Docker's default soft `nofile` is 1024. On the 40-VM fleet the
216
+ manager evicted eighteen live workers before its own guest had even attached;
217
+ at roughly one leaked fd per eviction the limit arrives well inside a normal
218
+ run. The first thing to fail is the manager reopening its own stats file, then
219
+ every `accept()`:
220
+
221
+ ```
222
+ ERROR: rdma: Failed to open stats file /run/ernic-stats/ernic-1.stats: Too many open files
223
+ ERROR: rdma: TCP: Failed to accept connection: Too many open files
224
+ ```
225
+
226
+ There is no backoff on that path — it spins, and produced 3.3 GB of log in 12
227
+ hours. The manager then goes unhealthy, and being `restart: "no"` it stays
228
+ down, so its guest dies on an assertion in QEMU's own error handling
229
+ (`error_setv: Assertion *errp == NULL failed`).
230
+
231
+ Every ernic service therefore gets `ulimits.nofile` from `ERNIC_NOFILE`
232
+ (default 65536). **This does not fix the leak** — that is upstream's — it moves
233
+ the wall far enough out that a fleet-length run finishes first. Raise it
234
+ further, or reduce churn, for a fleet that flaps.
235
+
236
+ Re-measured at 40 VMs with the limit raised: manager steady at 165 fds, **0**
237
+ EMFILE, max node id 31 of the 64 budget (it reached 50–61 before). So the
238
+ long-run death is gone. The *boot* ceiling is unchanged — that run still lost
239
+ 3 guests to 24 heartbeat evictions, and the three lost guests were exactly the
240
+ three ernics that restarted. Raising the fd limit buys longevity, not scale.
241
+
242
+ ### The mesh saturates at ~40 nodes, and 64 is not reachable
243
+
244
+ Asking for 64 VMs on this host settles at **40 of 64**. The other 24 workers
245
+ never register; they sit in a permanent restart loop on
246
+
247
+ ```
248
+ ERROR: rdma: TCP: Registration timeout
249
+ Failed to realize PVRDMA device with backend 'tcp:worker:172.31.1.1:6320'
250
+ ```
251
+
252
+ This is **not** the boot burst. Load peaked at 843 on 128 cores — 64 VMs ask
253
+ for 256 vCPU — and the fleet took 12 minutes to settle. But the stragglers
254
+ still fail identically with the host back at load 16, and stopping a healthy
255
+ worker to free a slot lets others in. It is a capacity wall, not a delay.
256
+
257
+ The cause is that this is a **full mesh**. A worker holds about N+2 sockets
258
+ where N is the *registered* size, not `VM_COUNT` — 42 sockets measured on both
259
+ the 40-VM and the 64-VM fleet. Registering node N+1 therefore costs N new
260
+ connections and touches every existing node, through a single-threaded manager,
261
+ against a fixed wall-clock registration timeout. The cost per registration
262
+ climbs with the mesh until registration stops completing.
263
+
264
+ The 64-node topology cap never came into it: **max node id reached 30**. Plan
265
+ around ~40, expect it to move with host speed, and note two consequences:
266
+
267
+ - **A stuck worker retries forever; its guest gets one chance.** `ernic` is
268
+ `restart: on-failure`, `qemu` is `restart: "no"`. By the time a worker on its
269
+ fifteenth attempt registers, the VM it exists to serve has been dead for ten
270
+ minutes. The 24 stuck workers matched the 24 dead guests exactly.
271
+ - **Failed registrations leak manager fds too, and faster than evictions do.**
272
+ Measured over a six-hour run parked above the wall: 8006 worker restarts and
273
+ 50 fds/min at the manager, reaching 18164 open descriptors. That is Docker's
274
+ 1024 default in 20 minutes and `ERNIC_NOFILE=65536` in about 22 hours — a
275
+ working day, not an indefinite reprieve. Size the fleet below the wall; no
276
+ fd limit makes sitting above it survivable.