Prometheus Monitoring and Grafana Dashboard#
rocm-ernic ships a Prometheus exporter
(ernic-exporter) that reads the same data sources
as ernicctl – the instances.json manifest and
per-instance *.stats files under ERNIC_RUN_DIR
– and exposes them as Prometheus metrics on an HTTP
endpoint. A pre-built Grafana dashboard is included
for visualising cluster state, network traffic, RDMA
vs TCP/IP ratios, FLR events, and VM lifecycle.
Architecture#
┌─────────────────── Host ───────────────────────┐
│ │
│ rocm-ernic *.stats (text, ~1 Hz) │
│ server instances ──────────────► ernic-exporter │
│ │ :9840/metrics │
│ └──► instances.json ──────► │ │
│ │ │
└────────────────────────────────────────┼─────────┘
│
Prometheus scrape
│
┌────▼────┐
│ Grafana │
└─────────┘
The exporter runs as a systemd service alongside
rocm-ernic. Prometheus scrapes /metrics at a
configurable interval (default 5 s); each scrape reads
the current stat files on-demand so no internal polling
loop is needed.
Quick Start#
Manual (single host):
# 1. Install the exporter's Python dependency. The
# requirements are in /usr/share/rocm-ernic with the
# rocm-ernic-exporter package, and in
# /usr/local/share/rocm-ernic with a default CMake
# install. On Ubuntu, python3 -m venv needs
# python3-venv, which the package depends on.
python3 -m venv /opt/ernic-exporter-venv
/opt/ernic-exporter-venv/bin/pip install \
-r /usr/share/rocm-ernic/requirements-exporter.txt
# 2. Start the exporter (uses system Python if
# prometheus_client is installed system-wide,
# otherwise point the unit at the venv -- see
# Ansible section below)
sudo systemctl daemon-reload
sudo systemctl enable --now ernic-exporter
# 3. Verify
curl -s http://localhost:9840/metrics | head -20
Note
ansible/ used to carry a monitoring-setup.yml
play that did the above, plus a Prometheus file_sd
scrape config and the Grafana dashboard JSON. It was
removed in 0.2.0 along with the rest of the automation
that was not on the guest-setup path. The manual steps
above are the supported route.
Configuration#
All settings come from the same
/etc/rocm-ernic/rocm-ernic.env file that the main
service uses, plus two exporter-specific variables:
Variable |
Default |
Description |
|---|---|---|
|
|
Directory containing |
|
|
HTTP port for the |
Override via environment or in the env file:
echo "ERNIC_EXPORTER_PORT=9841" | \
sudo tee -a /etc/rocm-ernic/rocm-ernic.env
sudo systemctl restart ernic-exporter
Ansible variables (group_vars/all.yml):
Variable |
Default |
Description |
|---|---|---|
|
|
Feature gate; set |
|
|
Maps to |
|
|
Path for the Python venv. |
|
|
Prometheus scrape interval. |
|
|
Directory for Prometheus |
|
|
Grafana provisioning dashboards directory. |
Metrics Reference#
All metric names start with ernic_. Labels use
ernic_id for the numeric server instance ID (1, 2,
…), role for the instance role, and qp for
the QP handle.
Note
The instance label is ernic_id, not instance.
Prometheus reserves instance for the scrape
target (host:port), so every exporter metric
would collide on it. Nearly all per-instance
metrics carry role as well, and the shipped
dashboard filters on both.
Every metric whose name ends in _total is a counter
– a value that only ever climbs, until the server
restarts and it resets to zero. Query those with
rate() or increase(), never as a raw value.
Everything else is a gauge and can be read directly.
Note
The exporter reads absolute totals out of the
*.stats files, so it builds its counters through
a custom collector. prometheus_client’s own
Counter class only offers inc(), which cannot
express “the server says the total is now N”.
Cluster and Instance Metrics#
Metric |
Type |
Description |
|---|---|---|
|
Gauge |
Number of rocm-ernic server instances. |
|
Gauge |
Whether each instance process is alive
(1 = running, 0 = dead).
Labels: |
|
Gauge |
Process uptime in seconds.
Labels: |
VM Lifecycle Metrics#
Metric |
Type |
Description |
|---|---|---|
|
Gauge |
Number of attached VMs. |
|
Gauge |
Whether a VM is attached (1 = yes).
Labels: |
|
Gauge |
VM uptime in seconds.
Labels: |
|
Gauge |
Whether GPU passthrough is enabled (1 = yes).
Labels: |
Network Traffic#
All per-instance, labelled
{ernic_id="<id>", role="<role>"}:
Metric |
Type |
Description |
|---|---|---|
|
Counter |
Total IP/Ethernet bytes transmitted. |
|
Counter |
Total IP/Ethernet bytes received. |
|
Counter |
Total bytes sent via RDMA SEND operations. |
|
Counter |
Total bytes received via RDMA RECV operations. |
|
Counter |
Total bytes via RDMA Read. |
|
Counter |
Total bytes via RDMA Write. |
|
Counter |
Aggregate: send + recv + read + write. |
|
Counter |
Aggregate: IP TX + RX. |
Device Health and Events#
Metric |
Type |
Description |
|---|---|---|
|
Counter |
Cumulative FLR / device reset count.
Labels: |
|
Counter |
Total admin queue commands processed.
Labels: |
|
Counter |
Total interrupts delivered.
Labels: |
|
Gauge |
Connection state (1 = connected).
Labels: |
|
Counter |
Total MMIO read operations.
Labels: |
|
Counter |
Total MMIO write operations.
Labels: |
|
Counter |
How many times the server has flushed stats.
Labels: |
Per-QP Metrics#
All labelled
{ernic_id="<id>", role="<role>", qp="<handle>"}
except ernic_qp_count, which has no qp label:
Metric |
Type |
Description |
|---|---|---|
|
Gauge |
Number of active Queue Pairs.
Labels: |
|
Counter |
Bytes sent via SEND on this QP. |
|
Counter |
Bytes received via RECV on this QP. |
|
Counter |
RDMA Read bytes on this QP. |
|
Counter |
RDMA Write bytes on this QP. |
|
Counter |
Total WQEs processed on this QP. |
|
Counter |
Total CQEs posted on this QP. |
|
Counter |
Send doorbell rings on this QP. |
|
Counter |
Receive doorbell rings on this QP. |
LIF Counters#
The emulated NIC keeps the ionic LIF statistics block,
the 1 KiB struct ionic_lif_stats it writes into
guest memory and the guest reads for ip -s link.
This is a different counter set from the guest’s RDMA
hw_counters, which the device serves from the admin
queue; the next section covers those. The exporter
publishes the whole block as a single metric, with the
counter in a label:
Metric |
Type |
Description |
|---|---|---|
|
Counter |
One LIF statistics counter, under its
|
This is one metric with a counter label rather than
one metric per counter because the set belongs to the
device, not to the exporter: a counter the emulator
starts keeping is published the moment the server
writes it, with no exporter release. The guest’s
ionic_rdma driver makes the same bargain – it takes
its counter names from a table the device hands back.
The label values are exactly the field names of
struct ionic_lif_stats, so a counter can be looked
up in the kernel’s ionic_if.h.
A counter the emulator does not yet keep reads zero rather than disappearing; the server prints the whole block on every flush.
The block has 74 named counters:
Ethernet traffic: 12 counters, from
rx_ucast_bytestotx_bcast_packets.Drop and DMA error: 14 counters, from
rx_ucast_drop_bytestotx_dma_error.Ring error: 10 counters, from
rx_queue_disabledtotx_queue_empty.RDMA traffic: 11 counters, from
tx_rdma_ucast_bytestorx_rdma_ecn_packets.RDMA error: 27 counters, from
rdma_req_rx_pkt_seq_errtordma_resp_tx_rnr_retry_err.
Every one of them, with its qword index in
struct ionic_lif_stats:
Qword |
Counter |
Group |
|---|---|---|
0 |
|
Ethernet traffic |
1 |
|
Ethernet traffic |
2 |
|
Ethernet traffic |
3 |
|
Ethernet traffic |
4 |
|
Ethernet traffic |
5 |
|
Ethernet traffic |
8 |
|
Drop and DMA error |
9 |
|
Drop and DMA error |
10 |
|
Drop and DMA error |
11 |
|
Drop and DMA error |
12 |
|
Drop and DMA error |
13 |
|
Drop and DMA error |
14 |
|
Drop and DMA error |
16 |
|
Ethernet traffic |
17 |
|
Ethernet traffic |
18 |
|
Ethernet traffic |
19 |
|
Ethernet traffic |
20 |
|
Ethernet traffic |
21 |
|
Ethernet traffic |
24 |
|
Drop and DMA error |
25 |
|
Drop and DMA error |
26 |
|
Drop and DMA error |
27 |
|
Drop and DMA error |
28 |
|
Drop and DMA error |
29 |
|
Drop and DMA error |
30 |
|
Drop and DMA error |
32 |
|
Ring error |
33 |
|
Ring error |
34 |
|
Ring error |
35 |
|
Ring error |
36 |
|
Ring error |
40 |
|
Ring error |
41 |
|
Ring error |
42 |
|
Ring error |
43 |
|
Ring error |
44 |
|
Ring error |
48 |
|
RDMA traffic |
49 |
|
RDMA traffic |
50 |
|
RDMA traffic |
51 |
|
RDMA traffic |
52 |
|
RDMA traffic |
56 |
|
RDMA traffic |
57 |
|
RDMA traffic |
58 |
|
RDMA traffic |
59 |
|
RDMA traffic |
60 |
|
RDMA traffic |
61 |
|
RDMA traffic |
96 |
|
RDMA error |
97 |
|
RDMA error |
98 |
|
RDMA error |
99 |
|
RDMA error |
100 |
|
RDMA error |
101 |
|
RDMA error |
102 |
|
RDMA error |
103 |
|
RDMA error |
104 |
|
RDMA error |
105 |
|
RDMA error |
106 |
|
RDMA error |
107 |
|
RDMA error |
108 |
|
RDMA error |
112 |
|
RDMA error |
113 |
|
RDMA error |
114 |
|
RDMA error |
115 |
|
RDMA error |
116 |
|
RDMA error |
117 |
|
RDMA error |
118 |
|
RDMA error |
119 |
|
RDMA error |
120 |
|
RDMA error |
121 |
|
RDMA error |
122 |
|
RDMA error |
123 |
|
RDMA error |
124 |
|
RDMA error |
125 |
|
RDMA error |
Note
Not every counter in this block is live. The
Ethernet, drop, ring and RDMA traffic counters are,
and tests/py/test_ionic_hw_stats.py drives
traffic that moves them. The rdma_req_* and
rdma_resp_* error counters at qwords 96 to 125
are copied in from the data path, which also serves
them to the guest through the admin queue (see
Guest hw_counters and real silicon below), so
ernic_lif_counter_total and the guest’s
hw_counters agree. Those with no emulator event
behind them yet – packet sequence, duplicates,
flushes, CNP and ECN – read zero whatever happens:
a zero there means “not counted”, not “did not
happen”.
Guest hw_counters and real silicon#
A guest running the in-tree ionic_rdma driver gets
its own view of the device’s counters, independent of
the exporter, under
/sys/class/infiniband/<dev>/ports/1/hw_counters:
rdma statistic show link ionic_0/1
rdma statistic qp set link ionic_0/1 auto type on
rdma statistic qp show
Those come from four admin commands –
STATS_HDRS and QP_STATS_HDRS hand back a table
naming the counters, STATS_VALS and
QP_STATS_VALS fill in the values. The driver takes
the names, value types and offsets from that table, so
the device decides what a guest sees.
The values behind those tables are the server’s own
device and per-QP counters, not the LIF block, and the
exporter has always published them – under different
names. What a guest reads as total_bytes_sent is
ernic_rdma_bytes_sent_total here, and what it reads
per QP as wqes_processed is
ernic_qp_wqes_processed_total. The RDMA error
counters are the exception: the data path keeps them
per device and per QP, and the exporter carries only
the device totals, as ernic_lif_counter_total under
their ionic_if.h names; per-QP, only this view
reports them. A device reset zeroes both. The
tables are defined in dev_stats and qp_stats
in src/ionic/ionic_adminq_priv.h, the first entries
written out and the rest generated from
IONIC_DP_RDMA_COUNTERS in src/ionic/ionic_datapath.h:
The device table, 35 counters:
Counter |
|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The per-QP table, 38 counters:
Counter |
|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
So the device exports two counter sets, and they stand differently against the silicon the emulator models:
The LIF statistics block matches. All 74 named counters are at the qword offsets, and under the names, that
struct ionic_lif_statsgives them in the kernel’sionic_if.h, which is the layout the Pensando DSC firmware maintains. A tool that readsip -s linkor the LIF block directly sees what it would see on hardware.The hw_counters tables do not. The device and per-QP counters the emulator advertises (
commands,total_bytes_sent,doorbell_sendand the rest) are its own: they describe the emulator’s accounting, and the real firmware’s names for its tables are not published in the kernel tree or anywhere else we can check. The wire format is right and the driver is happy, but a dashboard keyed on these names is keyed on rocm-ernic, not on the DSC.
This is a naming difference, not a protocol one. The
ABI is firmware-defined on both sides: the driver never
assumes a counter set, so the emulator is free to
advertise its own, and there is no guest-visible
breakage either way. Anyone with DSC hardware can
settle the question by comparing the output of
rdma statistic show there with dev_stats.
Grafana Dashboard#
The dashboard JSON is at
prometheus/grafana/ernic-dashboard.json in the
source tree and
is installed to the Grafana provisioning directory by
the Ansible playbook. It can also be imported manually
via the Grafana UI (Dashboards > Import > Upload JSON).
Dashboard rows:
Cluster Overview – stat panels for instance count, running instances, attached VMs, FLR resets, total QPs, and GPU-passthrough VMs; tables showing server and VM details.
Network Traffic – time-series panels for IP and RDMA traffic rates; a pie chart comparing RDMA vs IP total bytes; a per-server traffic totals table.
RDMA Detail – separate Send/Recv and Read/Write rate panels; a per-QP traffic table.
Server Health – FLR reset count over time, commands/s, interrupts/s, MMIO read/write rates, and stats write count.
LIF Counters (ionic_lif_stats) – the ionic LIF statistics block: RDMA packet rate, RDMA error rate, Ethernet drop and ring-error rate, and a table of every counter that has moved. The two rate panels filter out flat-zero series, so a line appearing is itself the signal.
VM Metrics – guest-side panels for VM CPU, memory and disk I/O, TCP/IP rates on the
rocm-ernic0and management interfaces, RDMA port data and packet rates, and GPU temperature, utilisation, power, clock and PCIe bandwidth.Unlike rows 1-5, this row does not read from
ernic-exporter. Its panels querynode_*,rdma_port_*andgpu_*series from separateernic-vm-node,ernic-vm-rdmaandernic-vm-gpuscrape jobs that run inside the guest, so it stays empty unless those are configured.
The dashboard defines three template variables:
$instance filters by ernic_id, $role by
instance role, and $disk_device selects the guest
block device for the VM disk panel. Every
ernic-exporter panel filters on both $instance
and $role, so leaving $role unset hides all
data. Default refresh is 10 s with a 1 h time window.
Useful PromQL Examples#
Aggregate RDMA throughput across all instances:
sum(rate(ernic_rdma_bytes_total[5m]))
Per-instance IP vs RDMA ratio:
rate(ernic_rdma_bytes_total[5m])
/ (rate(ernic_rdma_bytes_total[5m])
+ rate(ernic_ip_bytes_total[5m]))
Any RDMA error the device reported, across the cluster:
sum by (counter) (
rate(ernic_lif_counter_total{
counter=~"rdma_(req|resp)_.*"}[5m])
) > 0
One LIF counter by name, as a guest would read it from
hw_counters:
ernic_lif_counter_total{counter="tx_rdma_ucast_bytes"}
Frames the emulated datapath dropped:
sum by (ernic_id) (
rate(ernic_lif_counter_total{
counter=~".*_drop_packets"}[5m])
)
Alert when any instance is down:
ernic_instance_up == 0
Alert on FLR reset:
increase(ernic_flr_reset_total[5m]) > 0