Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions docs/api-reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,49 @@ runtime binding.
- type: `string`
- values:
- `huge`: CPU hugepages (recommended for CPU buffers)
- `device`: GPU VRAM (discrete GPUs only, requires GPUDirect via peermem or DMA-BUF)
- `device`: GPU memory (requires GPUDirect via peermem or DMA-BUF). On a system such as
IGX Thor with both an integrated and a discrete GPU, set `affinity` to the discrete GPU's
process-local CUDA ordinal.
- `host_pinned`: Pinned CPU pages allocated via `cudaHostAlloc`. **Recommended on
integrated GPUs (e.g. NVIDIA GB10 / DGX Spark)**, where the NIC cannot peer-DMA
into device memory and CUDA reports DMA-BUF unsupported. On discrete-GPU systems,
into device memory and CUDA reports DMA-BUF unsupported. Use this kind when remaining on
the integrated GPU in a hybrid-GPU system such as IGX Thor. On discrete-GPU systems,
prefer `device` for high-throughput RX/TX paths.
- `host`: Regular CPU memory (not recommended)
- **`affinity`**: GPU ID for `device` memory, or NUMA node ID for CPU memory.
- **`affinity`**: Process-local CUDA ordinal for `device` and `host_pinned` memory, or NUMA
node ID for `huge` and `host` memory. CUDA ordinals reflect only the devices visible to the
process and need not match host-wide GPU indices.

On a mixed integrated/discrete GPU host, select the intended discrete GPU by its stable UUID
when starting a privileged NVIDIA container. For example, after identifying the UUID with
`nvidia-smi --query-gpu=uuid,name --format=csv,noheader`, pass it to both visibility variables:

```bash
docker run --privileged --runtime=nvidia \
-e NVIDIA_VISIBLE_DEVICES=<discrete-GPU-UUID> \
-e CUDA_VISIBLE_DEVICES=<discrete-GPU-UUID> ...
```

When that is the only CUDA-visible GPU, it has process-local ordinal `0`, even if it has a
different host-wide index. Configure `affinity` using the process-local ordinal, not the host
index. On the tested IGX Thor privileged-container setup, UUID-based selection prevents the
integrated GPU from remaining selected.
- type: `integer`
- **`access`**: Memory access permissions.
- type: `list`
- values: `local`, `rdma_read`, `rdma_write`
- **`num_bufs`**: Number of buffers in this region. Higher values give more processing
headroom but consume more memory (GPU BAR1 for `device`). Too low risks dropped packets
on RX or higher latency on TX. Rule of thumb: 3x-5x `batch_size`. For Raw Ethernet
(`stream_type: "raw"`), `num_bufs` below 1.5x the NIC ring size deadlocks the worker;
`daqiri_init` auto-bumps such MRs to 3x the ring (24576 with the default 8192) and
logs a `WARN`.
on RX or a TX stall. Raw DPDK queue regions use a floor of
`max(1.5 * ring, ring + 2 * batch_size)`. DAQIRI bumps values below that floor to
`max(3 * ring, ring + 4 * batch_size)` and warns with the exact `num_bufs` to configure.
With the default 8192-descriptor ring and `batch_size: 10240`, the floor is 28672 and the
bump target is 49152; the shipped `num_bufs: 51200` is sufficient.

Raw ibverbs uses a separate hardware limit. A scheduled packet can consume two send work
requests, so usable TX slots are capped at `max_qp_wr / 2`. DAQIRI warns when configured
storage exceeds that cap and fails initialization with the exact maximum when `batch_size`
exceeds it.
- type: `integer`
- **`buf_size`**: Size of each buffer in bytes. Should match the expected packet size, or
the segment size when using header-data split.
Expand Down
6 changes: 3 additions & 3 deletions docs/tutorials/configuration-walkthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,9 @@ bench_tx: # (25)!
3. :material-wrench: **`master_core`** · `integer (CPU core ID)` · *required*: Core used for DAQIRI setup. Does not need to be isolated, and it is recommended to differ from the `cpu_core` fields below that poll the NIC.
4. **`loopback`** · `string` · *default: `""`*: Loopback mode. **Supported:** `""` (disabled), `"sw"` (DPDK software loopback, no NIC), and `"hw"` (single-port mlx5 hardware loopback for the raw ibverbs engine). Hardware loopback requires one interface with both TX and RX queues and packets addressed to that port's own MAC.
5. The `memory_regions` section lists where the NIC will write/read data from/to when bypassing the OS kernel. Tip: when using GPU buffer regions, keeping the sum of their buffer sizes below 80% of your BAR1 size is generally a good rule of thumb.
6. :material-package-variant: **`kind`** · `string` · *required*: Type of memory backing the region. **Supported:** `device` (GPU VRAM via GPUDirect, preferred on discrete GPUs), `host_pinned` (CPU pinned memory, required on integrated GPUs like NVIDIA GB10/DGX Spark where peer-DMA isn't available), `huge` (hugepages, CPU), `host` (CPU unpinned). See the [memory regions reference](../api-reference/configuration.md#memory-regions). Choose based on whether packets are processed on the GPU or CPU and on the GPU class.
7. :material-wrench: **`affinity`** · `integer (GPU ID / NUMA node)` · *required*: GPU device ID when `kind: device` or `kind: host_pinned`. NUMA node ID for CPU memory regions (`huge`, `host`).
8. :material-package-variant: **`num_bufs`** · `integer` · *required*: Number of buffers in the region. Higher gives more time to process packets but uses more BAR1 space. Too low risks NIC drops (RX) or buffering latency (TX). A good starting point is 3×–5× the queue `batch_size`. For Raw Ethernet (`stream_type: "raw"`), `num_bufs` below 1.5× the NIC ring size deadlocks the worker, so `daqiri_init` auto-bumps such regions to 3× the ring (24576 with the default 8192) and logs a `WARN`.
6. :material-package-variant: **`kind`** · `string` · *required*: Type of memory backing the region. **Supported:** `device` (GPU memory via GPUDirect, preferred on discrete GPUs), `host_pinned` (CPU pinned memory, required on integrated GPUs like NVIDIA GB10/DGX Spark where peer-DMA isn't available), `huge` (hugepages, CPU), `host` (CPU unpinned). IGX Thor has an integrated GPU and a discrete GPU: use `kind: device` with the discrete GPU, or `kind: host_pinned` when remaining on the integrated GPU. See the [memory regions reference](../api-reference/configuration.md#memory-regions).
7. :material-wrench: **`affinity`** · `integer (CUDA ordinal / NUMA node)` · *required*: Process-local CUDA ordinal when `kind: device` or `kind: host_pinned`. The ordinal is relative to the GPUs visible to the process, not necessarily the host-wide GPU index. Use the visible discrete GPU's ordinal for `kind: device`. Use the selected integrated GPU's ordinal with `kind: host_pinned`. On mixed-GPU container hosts, the [memory regions reference](../api-reference/configuration.md#memory-regions) shows how to select the discrete GPU by UUID. For `huge` and `host`, use a NUMA node ID.
8. :material-package-variant: **`num_bufs`** · `integer` · *required*: Number of buffers in the region. Higher gives more time to process packets but uses more memory. Raw DPDK applies a ring-and-batch floor and bump target; raw ibverbs separately caps usable TX slots at `max_qp_wr / 2`. See the [memory regions reference](../api-reference/configuration.md#memory-regions) for the formulas, examples, and warning behavior.
9. :material-package-variant: **`buf_size`** · `integer (bytes)` · *required*: Size of each buffer in the region. Should equal your maximum packet size, or smaller when chaining regions per packet (e.g. header-data split, see the [HDS walkthrough](#header-data-split-hds) below).
10. The `interfaces` section lists the NIC interfaces that will be configured for the application.
11. :material-wrench: **`address`** · `string (PCIe BDF)` · *required*: PCIe bus address of this interface. **Must be changed for your system.** Both `tx_port` and `rx_port` may point to the same physical NIC for single-port closed-loop benches.
Expand Down
30 changes: 18 additions & 12 deletions scripts/build-container.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,24 @@ BUILD_SHARED_LIBS="${BUILD_SHARED_LIBS:-ON}"
DAQIRI_ENABLE_OTEL_METRICS="${DAQIRI_ENABLE_OTEL_METRICS:-OFF}"
AWS_SDK_CPP_VERSION="${AWS_SDK_CPP_VERSION:-1.11.822}"

case "${BASE_IMAGE}" in
cuda)
DAQIRI_OS_BASE_IMAGE="nvcr.io/nvidia/cuda:13.1.0-devel-ubuntu24.04"
;;
torch)
DAQIRI_OS_BASE_IMAGE="nvcr.io/nvidia/pytorch:26.01-py3"
;;
*)
echo "ERROR: invalid BASE_IMAGE='${BASE_IMAGE}'. Choose from: cuda, torch" >&2
exit 1
;;
esac
if [[ -z "${DAQIRI_OS_BASE_IMAGE:-}" ]]; then
case "${BASE_IMAGE}" in
cuda)
CUDA_VERSION="${CUDA_VERSION:-13.1.0}"
UBUNTU_VERSION="${UBUNTU_VERSION:-ubuntu24.04}"
DAQIRI_OS_BASE_IMAGE="nvcr.io/nvidia/cuda:${CUDA_VERSION}-devel-${UBUNTU_VERSION}"
;;
torch)
DAQIRI_OS_BASE_IMAGE="nvcr.io/nvidia/pytorch:26.01-py3"
;;
*)
echo "ERROR: invalid BASE_IMAGE='${BASE_IMAGE}'. Choose from: cuda, torch" >&2
exit 1
;;
esac
fi

echo "Building ${IMAGE_TAG} on ${DAQIRI_OS_BASE_IMAGE} (target ${BASE_TARGET})"

docker build \
--target runtime \
Expand Down
20 changes: 15 additions & 5 deletions src/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -860,13 +860,19 @@ Status get_memory_region_requirements(const NetworkConfig& config,
return Status::NOT_SUPPORTED;
}

std::unordered_set<std::string> queue_mrs;
std::unordered_map<std::string, size_t> queue_mr_batches;
const auto record_queue = [&queue_mr_batches](const CommonQueueConfig& queue) {
const size_t batch = static_cast<size_t>(std::max(0, queue.batch_size_));
for (const auto& name : queue.mrs_) {
queue_mr_batches[name] = std::max(queue_mr_batches[name], batch);
}
};
for (const auto& intf : config.ifs_) {
for (const auto& queue : intf.rx_.queues_) {
queue_mrs.insert(queue.common_.mrs_.begin(), queue.common_.mrs_.end());
record_queue(queue.common_);
}
for (const auto& queue : intf.tx_.queues_) {
queue_mrs.insert(queue.common_.mrs_.begin(), queue.common_.mrs_.end());
record_queue(queue.common_);
}
}

Expand All @@ -882,8 +888,12 @@ Status get_memory_region_requirements(const NetworkConfig& config,
#else
return Status::NOT_SUPPORTED;
#endif
if (queue_mrs.count(name) != 0 && req.num_bufs < 8192UL * 3 / 2) {
req.num_bufs = 8192UL * 3;
if (const auto batch = queue_mr_batches.find(name); batch != queue_mr_batches.end()) {
constexpr size_t ring_size = 8192;
const auto sizing = dpdk_memory_region_sizing(ring_size, batch->second);
if (req.num_bufs < sizing.floor) {
req.num_bufs = sizing.target;
}
}
break;
case EngineType::IBVERBS:
Expand Down
157 changes: 128 additions & 29 deletions src/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,110 @@ bool validate_flow_action_config(const FlowAction& action, const std::string& fl

} // namespace

bool Engine::select_cuda_device(int ordinal, const std::string& operation) {
const cudaError_t result = cudaSetDevice(ordinal);
if (result == cudaSuccess) {
return true;
}
DAQIRI_LOG_CRITICAL("Could not select CUDA device {} before {}: {}", ordinal, operation,
cudaGetErrorString(result));
return false;
}

Engine::CudaDeviceInfo Engine::get_cuda_device_info(int ordinal) {
CudaDeviceInfo info;
info.ordinal = ordinal;
cudaDeviceProp properties{};
if (cudaGetDeviceProperties(&properties, ordinal) != cudaSuccess) {
return info;
}
info.name = properties.name;
info.classification =
properties.integrated != 0 ? CudaDeviceClass::INTEGRATED : CudaDeviceClass::DISCRETE;
return info;
}

bool Engine::get_cuda_dmabuf_support(int ordinal, bool* supported) {
CUdevice device;
const CUresult device_result = cuDeviceGet(&device, ordinal);
int value = 0;
const CUresult attribute_result =
device_result == CUDA_SUCCESS
? cuDeviceGetAttribute(&value, CU_DEVICE_ATTRIBUTE_DMA_BUF_SUPPORTED, device)
: device_result;
if (attribute_result != CUDA_SUCCESS) {
const char* error_string = nullptr;
cuGetErrorString(attribute_result, &error_string);
DAQIRI_LOG_CRITICAL("Failed to query DMA-BUF support for CUDA device {}: {}", ordinal,
error_string != nullptr ? error_string : "unknown CUDA error");
return false;
}
*supported = value != 0;
return true;
}

void Engine::log_cuda_dmabuf_unavailable(const MemoryRegionConfig& mr) {
const CudaDeviceInfo selected = get_cuda_device_info(mr.affinity_);
const char* classification = "unknown";
if (selected.classification == CudaDeviceClass::INTEGRATED) {
classification = "integrated";
} else if (selected.classification == CudaDeviceClass::DISCRETE) {
classification = "discrete";
}
DAQIRI_LOG_CRITICAL("CUDA device {} ('{}', {}) does not support DMA-BUF export.",
selected.ordinal, selected.name, classification);

int count = 0;
bool found_discrete = false;
bool classification_unknown = selected.classification == CudaDeviceClass::UNKNOWN;
const cudaError_t count_result = cudaGetDeviceCount(&count);
if (count_result == cudaSuccess) {
for (int ordinal = 0; ordinal < count; ++ordinal) {
if (ordinal == selected.ordinal) {
continue;
}
const CudaDeviceInfo candidate = get_cuda_device_info(ordinal);
if (candidate.classification == CudaDeviceClass::UNKNOWN) {
classification_unknown = true;
continue;
}
if (candidate.classification != CudaDeviceClass::DISCRETE) {
continue;
}
found_discrete = true;
DAQIRI_LOG_CRITICAL(
"CUDA-visible discrete GPU {} ('{}') is available. To use device memory on that GPU, "
"configure memory region '{}' with:\n\n kind: device\n affinity: {}",
candidate.ordinal, candidate.name, mr.name_, candidate.ordinal);
}
}
if (count_result != cudaSuccess) {
DAQIRI_LOG_CRITICAL("Could not enumerate other CUDA-visible devices: {}",
cudaGetErrorString(count_result));
} else if (!found_discrete && classification_unknown) {
DAQIRI_LOG_CRITICAL(
"No other CUDA-visible GPU could be confirmed as discrete because a device "
"classification query failed. Set memory region '{}' affinity only after confirming a "
"discrete GPU's process-local CUDA ordinal.",
mr.name_);
} else if (!found_discrete && selected.classification == CudaDeviceClass::DISCRETE) {
DAQIRI_LOG_CRITICAL(
"No other CUDA-visible discrete GPU candidate is available for memory region '{}'.",
mr.name_);
} else if (!found_discrete) {
DAQIRI_LOG_CRITICAL(
"No discrete GPU is CUDA-visible. The discrete GPU must first be exposed to the runtime "
"container; on mixed-GPU container hosts, select the intended GPU by UUID with both "
"NVIDIA_VISIBLE_DEVICES and CUDA_VISIBLE_DEVICES. Then set memory region '{}' affinity "
"to its process-local CUDA ordinal.",
mr.name_);
}
DAQIRI_LOG_CRITICAL(
"To continue using the selected {} GPU, configure memory region '{}' with:\n\n"
" kind: host_pinned\n affinity: {}",
classification, mr.name_, selected.ordinal);
}

Engine::~Engine() {
free_memory_regions();
}
Expand Down Expand Up @@ -590,7 +694,10 @@ Status Engine::allocate_memory_regions() {
ar.deallocator_ = AllocRegion::Deallocator::FREE;
break;
case MemoryKind::HOST_PINNED:
cudaSetDevice(mr.second.affinity_);
if (!select_cuda_device(mr.second.affinity_,
"allocating pinned host memory region '" + mr.first + "'")) {
return Status::NULL_PTR;
}
if (cudaHostAlloc(&ptr, mr.second.ttl_size_, 0) != cudaSuccess) {
DAQIRI_LOG_CRITICAL("Failed to allocate CUDA pinned host memory!");
return Status::NULL_PTR;
Expand All @@ -610,7 +717,6 @@ Status Engine::allocate_memory_regions() {
unsigned int flag = 1;
const auto align = align_ceil(mr.second.ttl_size_, GPU_PAGE_SIZE);
CUdeviceptr cuptr;
CUcontext current = nullptr;

const auto driver_init_res = cuInit(0);
if (driver_init_res != CUDA_SUCCESS) {
Expand All @@ -620,12 +726,11 @@ Status Engine::allocate_memory_regions() {
err_str != nullptr ? err_str : "unknown error");
return Status::NULL_PTR;
}
const auto current_res = cuCtxGetCurrent(&current);
if (current_res != CUDA_SUCCESS) {
CUcontext previous = nullptr;
if (cuCtxGetCurrent(&previous) != CUDA_SUCCESS) {
DAQIRI_LOG_CRITICAL("Could not query the current CUDA context");
return Status::NULL_PTR;
}
const CUcontext previous = current;
const auto restore_previous = [&]() {
CUcontext active = nullptr;
if (cuCtxGetCurrent(&active) != CUDA_SUCCESS ||
Expand All @@ -637,32 +742,26 @@ Status Engine::allocate_memory_regions() {
}
return true;
};
bool select_device = current == nullptr;
if (current != nullptr) {
CUdevice current_device;
if (cuCtxGetDevice(&current_device) != CUDA_SUCCESS) {
DAQIRI_LOG_CRITICAL("Could not query the device for the current CUDA context");
return Status::NULL_PTR;
}
select_device = current_device != mr.second.affinity_;

if (!select_cuda_device(mr.second.affinity_,
"allocating device memory region '" + mr.first + "'")) {
return Status::NULL_PTR;
}
if (select_device) {
const auto set_res = cudaSetDevice(mr.second.affinity_);
if (set_res != cudaSuccess) {
DAQIRI_LOG_CRITICAL("Could not select CUDA device {}: {}", mr.second.affinity_,
cudaGetErrorString(set_res));
restore_previous();
return Status::NULL_PTR;
}
const auto init_res = cudaFree(0); // Create the primary context if needed.
if (init_res != cudaSuccess || cuCtxGetCurrent(&current) != CUDA_SUCCESS ||
current == nullptr) {
DAQIRI_LOG_CRITICAL("Could not initialize the CUDA primary context for device {}",
mr.second.affinity_);
restore_previous();
return Status::NULL_PTR;
}
const auto init_res = cudaFree(0); // Create the selected device's primary context.
if (init_res != cudaSuccess) {
DAQIRI_LOG_CRITICAL("Could not initialize the CUDA primary context for device {}: {}",
mr.second.affinity_, cudaGetErrorString(init_res));
restore_previous();
return Status::NULL_PTR;
}
CUcontext current = nullptr;
if (cuCtxGetCurrent(&current) != CUDA_SUCCESS || current == nullptr) {
DAQIRI_LOG_CRITICAL("Could not query the CUDA context for device {}",
mr.second.affinity_);
restore_previous();
return Status::NULL_PTR;
}

ar.cuda_context_ = current;
const auto alloc_res = cuMemAlloc(&cuptr, align);

Expand Down
Loading
Loading