homi#
homi, Host-Orchestrated Multi-path I/O, opens a set of NVMe devices and holds them open until it is told to stop. It does no I/O of its own.
The name borrows from ANA deliberately. Where ANA has the controller report the paths to a namespace and the host choose among them, this arranges them from the host: each connected client gets queues of its own to the same namespace, and the controller is never asked which clients exist. The parallel is in who orchestrates rather than in the topology: ANA spans controllers, and this shares one.
Its purpose is to provide the long-lived server process that controller sharing needs. The server owns the DMA hugepages and the controller hardware state, and it must outlive every client connected to it. Running homi puts that responsibility in a process of its own, so its clients can come and go freely. A client may be another process or an accelerator.
Controller sharing is a backend capability. The uPCIe
backend and its GPU variants provide it through their control plane, see
uPCIe (control plane), and the SPDK backend
through SPDK’s multi-process mode. homi is therefore useful where one of
those is built: upcie, upcie-cuda and upcie-hip on Linux, and spdk on
Linux and FreeBSD. On Windows it exits with -ENOTSUP, since no
backend that can share a controller is available there. On other platforms it
runs, but opening a device with a non-zero homi_id fails with -ENOTSUP.
Usage: homi <command> [<args>]
Where <command> is one of:
serve | Open the given devices and hold them open
status | Report whether a server is holding devices
See 'homi <command> --help' for the description of [<args>]
See 'homi --version' for the library version and the revision it was built from
homi - Host-Orchestrated Multi-path I/O -- ver: {major: 0, minor: 7, patch: 5, vcs: '94a52e5'}
serve: Hold devices open#
Usage: homi serve [<uri>...] [<args>]
Open the given devices and hold them open
Positional arguments:
[uri ...] ; Device URI e.g. '/dev/nvme0n1', '0000:01:00.1', '10.9.8.1:8888', '\\.\PhysicalDrive1'
Where <args> include:
--homi-id NUM ; The HOMI identifier to share controllers under
With <args> for backend:
[ --be STRING ] ; xNVMe backend, e.g. 'spdk', 'libvfn', 'upcie'
[ --host-heap-size NUM ] ; For be={upcie,upcie-cuda,upcie-hip}, host DMA heap size in bytes
[ --device-heap-size NUM ] ; For be={upcie-cuda,upcie-hip}, GPU device heap size in bytes
[ --gpu-id NUM ] ; For be={upcie-cuda,upcie-hip}, GPU ordinal to use
[ --no-cuse ] ; Disable the NVMe-driver ioctl() CUSE mimic
[ --help ] ; Show usage / help
See 'homi --help' for other commands
homi - Host-Orchestrated Multi-path I/O -- ver: {major: 0, minor: 7, patch: 5, vcs: '94a52e5'}
Opens every device given as a positional argument, then blocks until signalled.
Every process that should share these controllers must be started with the same id.
Roles are not assigned by the tool: whichever process claims a given
homi_id first becomes the server (see
Process model). Starting homi before any client
is therefore what makes it the server.
The heap is allocated once per process, on the first device open, so
--host-heap-size covers every device homi holds rather than being per
device.
Rather than the 1 GiB the backend would otherwise use, homi defaults to 80 MiB per device held. The heap is the pool every client draws from, since a client brings no memory of its own, and 64 MiB of that is for them. The rest is what homi itself needs: the admin queue and the sync queue pair that opening a device creates, each carrying a request pool costing 4 MiB. The two are budgeted separately so that a client can be handed a buffer the size of the pool from a server holding a single device. Claiming the backend default instead would leave nothing in the hugepage pool for clients of anything else, so size it for the buffers the clients will ask for:
homi serve 0000:03:00.0 --be upcie --homi-id 1 --host-heap-size 134217728
When every device given sits on one NUMA node, as sysfs reports it, homi binds its memory and threads to that node before opening anything, so the heap the controllers fetch from is local to them; otherwise it leaves placement alone. It says which when it starts. The binding is strict, so the hugepages have to be reserved on that node: a node short of them fails the open rather than spilling to another, and homi names the node when it does.
If any device fails to open, homi closes the devices it has already opened
and exits with an error, rather than holding a partial set open. Once all
devices are open it reports that it has started, then waits for SIGINT
(Ctrl+C) or SIGTERM. On either signal it closes all devices and exits.
Example: hold a single device open as the server for homi_id 1:
homi serve 0000:03:00.0 --be upcie --homi-id 1
Example: hold several devices under the same homi_id:
homi serve 0000:03:00.0 0000:04:00.0 --be upcie --homi-id 1
For a controller reached over a PCIe-attached backend (upcie, spdk), whose
URI is therefore its bus:device.function address, homi also exposes a
/dev/xnvme/<bdf> character device answering the Linux kernel NVMe driver’s
ioctl() interface, the same mimic qublk serves as /dev/ublkb<N>-nvme (see
qublk). A controller reached any other way
(fabrics, a bare device-file path) has no such address and is left
unexposed, with no fallback name. Pass --no-cuse to skip it entirely. The
mimic itself is Linux-only; on another platform homi still runs, quietly
skipping the character device for every controller.
While it runs, clients connect by passing the same
--homi-id:
xnvme info 0000:03:00.0 --be upcie --homi-id 1
status: Report whether a server is running#
Usage: homi status [<args>]
Report whether a server is holding devices. Asks the server over its socket and opens no device, so it disturbs no I/O. Exits non-zero until a server is up and its devices are ready.
Where <args> include:
--homi-id NUM ; The HOMI identifier to share controllers under
[ --help ] ; Show usage / help
See 'homi --help' for other commands
homi - Host-Orchestrated Multi-path I/O -- ver: {major: 0, minor: 7, patch: 5, vcs: '94a52e5'}
Answers whether anything is holding controllers, without opening a device:
homi status --homi-id 1
It reports whether a server is holding controllers under that id and, when one is, what it holds: how many connections each controller has, how many I/O submission and completion queues are in use, and how many the controller allocated. It exits zero only when a server is running and every controller it holds has finished coming up, so it can be used as a readiness or health check without parsing its output.
It reports on the uPCIe backend only, and exists only where that backend does,
which is Linux and only when it was built in. Elsewhere the subcommand refuses
rather than reporting: homi itself still runs, since --be spdk is able
to share a controller on other platforms, but its bookkeeping is not somewhere
this can read. Where uPCIe is present and a runtime belongs to another
backend, that runtime likewise reads as absent rather than as running.
Output is YAML, matching the rest of xNVMe, since the caller is as likely to be a monitoring loop as a person:
homi_id: 9
server_running: true
connections: 3
controllers:
- uri: '0000:03:00.0'
readable: true
initialized: true
connections: 2
nsq_used: 6
ncq_used: 6
nsq_total: 16
ncq_total: 16
ready: true
The top-level connections counts the sockets the server has open, including
the one homi status is using to ask, so a server serving two clients reads
as three. There is one per client process, however many controllers that
process reaches, since a request names the controller it is about. The per-
controller connections is that controller’s own: the clients that asked the
server for it, whether or not they went on to take a queue, and not the one
asking. ready is what the exit status follows: it is false while a server
holds the role but its controllers are still coming up, which is a window a
service manager would otherwise mistake for being up.
nsq_used and ncq_used are necessarily equal: a queue pair is created as
one submission and one completion queue sharing a queue identifier, and the
identifier is what is tracked. They are reported separately so each reads
against its own total. The totals are the queues the controller has allocated,
which need not agree, and the smaller is what limits queue pairs; both are
given rather than their minimum so it is visible which one binds. They are
omitted entirely when the controller did not report them, since absent says
unknown where zero would say none.
What it inspects is whoever is serving that identifier. It connects to the runtime’s socket and asks, which answers most of the question by succeeding: a socket that answers has the process holding the controllers behind it. Nothing is opened, nothing is locked, and nothing is inferred from what a process left in memory.
That removes a distinction the previous arrangement needed. A server that was killed left a shared segment behind which read exactly like a live runtime, so status had to report whether what it found was debris. There is nothing to tell apart now: the process that holds the socket is the only thing that answers on it, and when it dies the name stops working.
The queue totals are read once when the server brings a controller up, with Get Features (Number of Queues). Set Features would let a driver ask for a larger allocation, but the drives tested here reject it, so what is reported is the allocation the controller made on its own.
Running several servers#
A host can run more than one server, each holding a disjoint set of
controllers under its own homi_id. Clients reach the one they want by
passing the matching id:
homi serve 0000:03:00.0 0000:04:00.0 --be upcie --homi-id 1
homi serve 0000:05:00.0 0000:06:00.0 --be upcie --homi-id 2
Two things are shared across all of them and have to be budgeted rather than
assumed. The hugepage pool is one: each server allocates a heap, and every
client of it draws from that heap rather than bringing one of its own, so a
server’s --host-heap-size has to cover the buffers all of its clients hold
at once. Reserve hugepages for the sum of every server’s heap, and size each
for its clients rather than for the server.
The controllers are the other. A controller can be held by one server only, so the sets must not overlap; the second server to claim one fails rather than sharing it.
Running as a service#
A server is only useful while it is running, which makes it a service rather than a command someone remembers to start. A unit template for that is installed with xNVMe, along with an example configuration file, so enabling an instance is all that remains:
cp /etc/xnvme/homi.conf.example /etc/xnvme/homi-1.conf
systemctl enable --now xnvme-homi@1
The unit is installed where systemd says units belong, and its ExecStart
names the homi that this build installs, so a prefixed build points at its
own binary rather than at whichever one is first on PATH. Two build options
govern it:
-Dsystemd=auto is the default. It installs the unit where systemd is
present and stays quiet where it is not, which is any non-Linux platform and
any build configured with -Dtools=false. Use -Dsystemd=disabled to keep a
build from writing to the unit directory at all, and -Dsystemd=enabled to
have configuration fail instead of skipping it silently.
The unit reads its configuration from the sysconfdir of the same build, so
a prefixed install writes the example and reads the configuration under that
prefix rather than writing to one path and reading from another. Pass
--sysconfdir=/etc to keep it at the usual place regardless of prefix.
-Dsystemd_unitdir= overrides the location. Left empty, it is whatever
systemd reports through pkg-config, resolved against the configured prefix
rather than taken as the absolute path systemd reports, so --prefix and
DESTDIR are both honoured. Set it to place units somewhere else, such as
-Dsystemd_unitdir=/etc/systemd/system for a machine-local override.
The unit is templated on homi_id, matching the model above, so several
instances can run side by side. It does not bind controllers or reserve
hugepages: those are prerequisites it checks for and refuses to start without,
because inferring a machine’s driver bindings from a unit file is worse than
failing with a clear message. systemctl start returns only once a client
could actually connect, which it establishes with homi status.
Backends#
homi works with any backend that can share a controller, which is spdk,
upcie, upcie-cuda and upcie-hip:
homi serve 0000:03:00.0 --be spdk --homi-id 1
With upcie-cuda and upcie-hip, homi additionally caps the GPU device
heap at 2 MiB rather than the backend default of 1 GiB. It never allocates
data buffers, which is all the device heap is used for, so claiming the
default would take VRAM away from the clients:
homi serve 0000:03:00.0 --be upcie-cuda --homi-id 1
homi status is the exception: it asks over the socket uPCIe serves, so a
server started with --be spdk reads as absent rather than as running, and on
a platform without uPCIe the subcommand refuses outright. The same applies to
the systemd unit, whose readiness gate is that command.
See also
uPCIe (control plane) covers the process model, startup handshake, admin-queue serialization, and the known limitations of the control plane.