How to Configure Multi-Node B300 GPU Droplets on the Rail-Optimized Fabric

Last verified 4 Nov 2026

DigitalOcean Droplets are Linux-based virtual machines (VMs) that run on top of virtualized hardware. Each Droplet you create is a new server you can use, either standalone or as part of a larger, cloud-based infrastructure.

Multi-node NVIDIA B300 GPU Droplets with eight GPUs in the MKC1 datacenter connect to a rail-optimized GPU fabric. On this fabric, DigitalOcean assigns each Remote Direct Memory Access (RDMA) network interface controller (NIC) an IPv6 address and route, and you configure the NICs with a DigitalOcean-provided script instead of assigning addresses yourself.

This page covers only the RDMA over Converged Ethernet (RoCE) network configuration of 8-GPU B300 Droplets in MKC1. For other multi-node GPU Droplets, and for the multi-node prerequisites and NCCL settings that apply to all of them, see How to Configure Multi-Node GPU Droplets.

Prerequisites

  • Multi-node GPU Droplets enabled for your account. To request them, contact support.
  • doctl installed and authenticated, and jq and curl installed on your local machine, to create Droplets with the user data example.
  • A personal access token with permission to create Droplets.

Fabric Network Planes

The rail-optimized fabric is a Layer 3 routed network organized into two independent network planes. Each Droplet has 16 fabric NICs, eight on each plane:

  • Plane 0: The even-numbered interfaces, eth2, eth4, eth6, eth8, eth10, eth12, eth14, and eth16.
  • Plane 1: The odd-numbered interfaces, eth3, eth5, eth7, eth9, eth11, eth13, eth15, and eth17.

Each GPU connects to one fabric NIC on each plane, with PCI affinity:

GPU Plane 0 NIC Plane 1 NIC
0 eth2 eth3
1 eth4 eth5
2 eth6 eth7
3 eth8 eth9
4 eth10 eth11
5 eth12 eth13
6 eth14 eth15
7 eth16 eth17

Within a plane, any two Droplets in MKC1 can communicate across all rails. The two planes are isolated from each other, so a NIC on Plane 0 cannot reach a NIC on Plane 1.

As on other multi-node GPU Droplets, eth0 is for public internet connectivity and eth1 is for private connectivity to other Droplets in the same VPC network.

Configure the RDMA NICs

DigitalOcean assigns each fabric NIC an IPv6 Unique Local Address (ULA), one /64 per interface in the fd02::/24 range, along with its routing information. A Droplet can read this configuration from the metadata service at http://169.254.169.254/v1/gpu-fabric-nics.

The gen-netplan.sh script queries the metadata service, then writes a Netplan configuration to /etc/netplan/90-rdma.yaml and applies it. For each fabric NIC, the configuration sets the assigned IPv6 address and an MTU of 4200, and places the NIC in its own virtual routing and forwarding (VRF) instance with a dedicated routing table. Each routing table has a summary route to fd02::/24 through the NIC’s leaf switch. This gives each NIC an isolated routing context for connectivity and path selection.

The script requires jq and curl on the Droplet. You can customize it to fit your deployment workflow. For example, set the SKIP_APPLY environment variable to write the Netplan file without applying it, or NETPLAN_OUT to write it to a different path. The header of the script lists every supported override.

View the gen-netplan.sh script
#!/usr/bin/env bash
#
# Generate and apply a netplan config for RDMA devices.
#
# Usage:
#   gen-netplan.sh
#
# Script overrides:
#   METADATA_URL   GPU fabric NICs endpoint
#                  (default: http://169.254.169.254/v1/gpu-fabric-nics)
#   CONFIG_FILE    skip metadata; read NIC JSON from this file instead
#   RDMA_DEV_FILE  file containing `rdma dev` output   (default: run `rdma dev`)
#   NETDEV_MAP     file with "<ibdev> <netdev>" lines  (default: sysfs lookup)
#   RENDERER       netplan renderer                    (default: networkd)
#   NETPLAN_OUT    output path                         (default: /etc/netplan/90-rdma.yaml)
#   SKIP_APPLY     if set, skip `netplan apply`
set -euo pipefail
METADATA_URL=${METADATA_URL:-http://169.254.169.254/v1/gpu-fabric-nics}
RENDERER=${RENDERER:-networkd}
MTU=${MTU:-4200}
TABLE_BASE=${TABLE_BASE:-100}       # first VRF table id; +1 per interface
VRF_PREFIX=${VRF_PREFIX:-vrf-}      # VRF name = <prefix><netdev> (keep <=15 chars)
SUMMARY=${SUMMARY:-fd02::/24}       # on-link fabric summary route
NETPLAN_OUT=${NETPLAN_OUT:-/etc/netplan/90-rdma.yaml}
METADATA_RETRIES=${METADATA_RETRIES:-30}
METADATA_RETRY_SECS=${METADATA_RETRY_SECS:-2}
command -v jq >/dev/null 2>&1 || { echo "error: jq is required" >&2; exit 1; }
command -v curl >/dev/null 2>&1 || { echo "error: curl is required" >&2; exit 1; }
ensure_vrf_module() {
  if lsmod | awk '$1=="vrf" {found=1} END{exit !found}'; then
    echo "vrf module already loaded"
    return 0
  fi
}
# Fetch NIC JSON from metadata (or CONFIG_FILE). Exits 1 on a permanent "not found".
# Prints the config path on stdout when successful.
fetch_config() {
  local config http_code body attempt
  config=$(mktemp)
  if [[ -n ${CONFIG_FILE:-} ]]; then
    [[ -f $CONFIG_FILE ]] || { echo "error: config '$CONFIG_FILE' not found" >&2; exit 1; }
    cp "$CONFIG_FILE" "$config"
    printf '%s' "$config"
    return 0
  fi
  for attempt in $(seq 1 "$METADATA_RETRIES"); do
    body=$(mktemp)
    http_code=$(curl -sS -o "$body" -w '%{http_code}' --connect-timeout 2 --max-time 10 \
      "$METADATA_URL" || true)
    case $http_code in
      200)
        if ! jq -e . >/dev/null 2>&1 <"$body"; then
          echo "error: metadata returned invalid JSON" >&2
          rm -f "$body" "$config"
          exit 1
        fi
        mv "$body" "$config"
        printf '%s' "$config"
        return 0
        ;;
      404)
        # Transient while gpu-fabric.sh is still writing, or permanently absent.
        if grep -qi 'not found' "$body" 2>/dev/null || [[ ! -s $body ]]; then
          if [[ $attempt -lt $METADATA_RETRIES ]]; then
            echo "metadata: not found (attempt $attempt/$METADATA_RETRIES); retrying in ${METADATA_RETRY_SECS}s" >&2
            rm -f "$body"
            sleep "$METADATA_RETRY_SECS"
            continue
          fi
          echo "error: GPU fabric NICs not found at $METADATA_URL" >&2
          rm -f "$body" "$config"
          exit 1
        fi
        echo "error: unexpected 404 body from $METADATA_URL: $(cat "$body")" >&2
        rm -f "$body" "$config"
        exit 1
        ;;
      400)
        echo "error: hypervisor is not GPU fabric enabled (HTTP 400)" >&2
        rm -f "$body" "$config"
        exit 1
        ;;
      *)
        if [[ $attempt -lt $METADATA_RETRIES ]]; then
          echo "metadata: HTTP ${http_code:-000} (attempt $attempt/$METADATA_RETRIES); retrying in ${METADATA_RETRY_SECS}s" >&2
          rm -f "$body"
          sleep "$METADATA_RETRY_SECS"
          continue
        fi
        echo "error: failed to fetch $METADATA_URL (HTTP ${http_code:-000})" >&2
        rm -f "$body" "$config"
        exit 1
        ;;
    esac
  done
}
get_rdma_dev() {
  if [[ -n ${RDMA_DEV_FILE:-} ]]; then cat "$RDMA_DEV_FILE"; else rdma dev; fi
}
# last 6 hex digits, lowercase, no separators (portable: no bash4-isms)
last6() {
  local h=${1//:/}
  h=$(printf '%s' "$h" | tr 'A-F' 'a-f')
  printf '%s' "${h:${#h}-6}"
}
# netdev via sysfs (pipe-free; always returns 0, empty if not found)
netdev_via_sysfs() {
  local dev=$1 p d
  p="/sys/class/infiniband/$dev/device/net"
  [[ -d $p ]] || return 0
  for d in "$p"/*; do
    [[ -e $d ]] || break
    basename "$d"
    return 0
  done
}
# netdev via `rdma link` (fallback; parses "... mlx5_X/port ... netdev <name>")
netdev_via_rdmalink() {
  local dev=$1
  rdma link 2>/dev/null | awk -v d="$dev" '
    { split($2, a, "/"); if (a[1]==d) for (i=1;i<=NF;i++) if ($i=="netdev") { print $(i+1); exit } }'
}
# netdev name for an ib device (never aborts under set -e)
netdev_for_dev() {
  local dev=$1 nd=""
  if [[ -n ${NETDEV_MAP:-} ]]; then
    nd=$(awk -v d="$dev" '$1==d {print $2; exit}' "$NETDEV_MAP") || nd=""
  else
    nd=$(netdev_via_sysfs "$dev") || nd=""
    [[ -n $nd ]] || { nd=$(netdev_via_rdmalink "$dev") || nd=""; }
  fi
  printf '%s' "$nd"
}
emit_netplan() {
  local config=$1 out=$2
  local MAP ROWS matched idx
  MAP=$(mktemp)
  ROWS=$(mktemp)
  matched=0
  while read -r _idx dev rest; do
    [[ -n $dev ]] || continue
    dev=${dev%:}
    guid=$(printf '%s' "$rest" | sed -n 's/.*node_guid \([0-9a-fA-F:]*\).*/\1/p') || guid=""
    [[ -n $guid ]] || continue
    nd=$(netdev_for_dev "$dev") || nd=""
    if [[ -z $nd ]]; then
      echo "warning: could not resolve a netdev for $dev; skipping" >&2
      continue
    fi
    printf '%s %s\n' "$(last6 "$guid")" "$nd" >>"$MAP"
    matched=$((matched + 1))
  done < <(get_rdma_dev)
  if [[ $matched -eq 0 ]]; then
    echo "warning: resolved 0 RDMA netdevs (check 'rdma dev' / sysfs / 'rdma link')" >&2
  fi
  idx=0
  while IFS=$'\t' read -r guid address prefix; do
    table=$((TABLE_BASE + idx))
    idx=$((idx + 1))
    key=$(last6 "$guid")
    nd=$(awk -v k="$key" '$1==k {print $2; exit}' "$MAP")
    if [[ -z $nd ]]; then
      echo "warning: no RDMA device matches guid $guid (last6 $key); skipping" >&2
      continue
    fi
    # next hop = leaf side of the /64 (::2)
    via="${address%::*}::2"
    printf '%s\t%s\t%s\t%s\t%s\n' "$nd" "$address" "$prefix" "$table" "$via" >>"$ROWS"
  done < <(jq -r '.nics[] | [.guid, .address, .prefix] | @tsv' "$config")
  {
    printf 'network:\n  version: 2\n  renderer: %s\n' "$RENDERER"
    printf '  vrfs:\n'
    while IFS=$'\t' read -r nd address prefix table via; do
      printf '    %s%s:\n' "$VRF_PREFIX" "$nd"
      printf '      table: %s\n' "$table"
      printf '      interfaces:\n        - %s\n' "$nd"
    done <"$ROWS"
    printf '  ethernets:\n'
    while IFS=$'\t' read -r nd address prefix table via; do
      printf '    %s:\n' "$nd"
      printf '      ignore-carrier: true\n'
      printf '      addresses:\n        - %s%s\n' "$address" "$prefix"
      printf '      routes:\n        - to: %s\n          via: %s\n          on-link: true\n' "$SUMMARY" "$via"
      printf '      mtu: %s\n' "$MTU"
    done <"$ROWS"
  } >"$out"
  rm -f "$MAP" "$ROWS"
  echo "wrote $out"
}
ensure_vrf_module
CONFIG=$(fetch_config)
if [[ -z $CONFIG ]]; then
  echo "error: no GPU fabric NIC configuration to apply" >&2
  exit 1
fi
trap 'rm -f "$CONFIG"' EXIT
emit_netplan "$CONFIG" "$NETPLAN_OUT"
if [[ -z ${SKIP_APPLY:-} ]]; then
  echo "applying netplan"
  netplan apply
fi

Download the script to your local machine:

curl -fsSLO https://docs.digitalocean.com/products/droplets/how-to/gpu/configure-rail-optimized-fabric/gen-netplan.sh

You can run the script when you create a Droplet, using user data, or on a Droplet that is already running.

Passing the script in the Droplet’s user data configures the fabric NICs on first boot.

First, look up the IDs that the Droplet creation request needs. List the public images and find the NVIDIA AI/ML-ready image ID:

doctl compute image list --public | grep NVIDIA

List your VPC networks and find the ID of the MKC1 VPC network to use:

doctl vpcs list | grep mkc1

List your SSH keys and note the IDs of the keys to add to the Droplet:

doctl compute ssh-key list

Export your personal access token:

export DIGITALOCEAN_ACCESS_TOKEN=<your-api-token>

From the directory that contains gen-netplan.sh, run the following command to create the Droplet. Replace <your-b300-plan-slug> with the multi-node B300 plan slug that support enabled for your account, and replace <image-id>, <vpc-id>, and <ssh-key-id> with the values from the previous commands:

jq -n \
  --arg name "rail-optimized-demo" \
  --arg region "mkc1" \
  --arg size "<your-b300-plan-slug>" \
  --argjson image <image-id> \
  --arg vpc_uuid "<vpc-id>" \
  --argjson ssh_keys '[<ssh-key-id>]' \
  --rawfile script ./gen-netplan.sh \
  '{
    name:$name, region:$region, size:$size, image:$image, vpc_uuid:$vpc_uuid, ssh_keys:$ssh_keys,
    user_data: (
      "#cloud-config\n" +
      "package_update: true\n" +
      "packages:\n  - jq\n  - curl\n" +
      "write_files:\n" +
      "- path: /usr/local/bin/gen-netplan.sh\n" +
      "  encoding: b64\n" +
      "  permissions: \"0755\"\n" +
      "  content: " + ($script|@base64) + "\n" +
      "runcmd:\n" +
      "  - [/usr/local/bin/gen-netplan.sh]\n"
    )
  }' | curl -ifsS -X POST "https://api.digitalocean.com/v2/droplets" \
  -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d @-

The user data installs jq and curl, writes the script to /usr/local/bin/gen-netplan.sh, and runs it once on first boot. The Netplan configuration it writes persists across reboots.

To configure a Droplet that is already running, copy the script to the Droplet. Replace <droplet-ip> with the Droplet’s public IP address:

scp gen-netplan.sh root@<droplet-ip>:/root/

Then install the script’s dependencies and run it:

ssh root@<droplet-ip> 'apt-get update -y && apt-get install -y jq curl && chmod +x /root/gen-netplan.sh && /root/gen-netplan.sh'

Verify the NIC Configuration

After the script runs, connect to the Droplet and list the network interfaces:

ip -br a

Every fabric interface on both planes must be UP and have two IPv6 addresses: the ULA from the metadata service, and an automatically assigned link-local address in fe80::/10. The link-local address is not routable; do not use it for RDMA. The second 16-bit group of the ULA identifies the plane: fd02:0:... for Plane 0 and fd02:1:... for Plane 1. A VRF device for each interface, vrf-eth2 through vrf-eth17, must also exist and be UP. Your addresses differ from the following example:

Output
eth0             UP             203.0.113.85/20 10.52.0.8/16 fe80::a077:4bff:fe76:1835/64
eth1             UP             10.132.0.37/20 fe80::f810:96ff:fe8d:4ad6/64
eth2             UP             fd02:0:400:527::1/64 fe80::ac3a:e2ff:fecf:d75e/64
eth3             UP             fd02:1:400:527::1/64 fe80::ac3a:e2ff:fecf:d75f/64
eth4             UP             fd02:0:500:527::1/64 fe80::ac3a:e2ff:fecf:d77e/64
eth5             UP             fd02:1:500:527::1/64 fe80::ac3a:e2ff:fecf:d77f/64
eth6             UP             fd02:0:600:527::1/64 fe80::ac3a:e2ff:fecf:d78e/64
eth7             UP             fd02:1:600:527::1/64 fe80::ac3a:e2ff:fecf:d78f/64
eth8             UP             fd02:0:700:527::1/64 fe80::ac3a:e2ff:fecf:d76e/64
eth9             UP             fd02:1:700:527::1/64 fe80::ac3a:e2ff:fecf:d76f/64
eth10            UP             fd02:0:300:527::1/64 fe80::ac3a:e2ff:fecf:d7ce/64
eth11            UP             fd02:1:300:527::1/64 fe80::ac3a:e2ff:fecf:d7cf/64
eth12            UP             fd02:0:200:527::1/64 fe80::ac3a:e2ff:fecf:d7ae/64
eth13            UP             fd02:1:200:527::1/64 fe80::ac3a:e2ff:fecf:d7af/64
eth14            UP             fd02:0:100:527::1/64 fe80::ac3a:e2ff:fecf:d79e/64
eth15            UP             fd02:1:100:527::1/64 fe80::ac3a:e2ff:fecf:d79f/64
eth16            UP             fd02:0:0:527::1/64 fe80::ac3a:e2ff:fecf:d7be/64
eth17            UP             fd02:1:0:527::1/64 fe80::ac3a:e2ff:fecf:d7bf/64
...
vrf-eth10        UP
vrf-eth11        UP
vrf-eth12        UP
vrf-eth13        UP
vrf-eth14        UP
vrf-eth15        UP
vrf-eth16        UP
vrf-eth17        UP
vrf-eth2         UP
vrf-eth3         UP
vrf-eth4         UP
vrf-eth5         UP
vrf-eth6         UP
vrf-eth7         UP
vrf-eth8         UP
vrf-eth9         UP

The routing table of each VRF contains the directly connected /64 route for its fabric interface and a summary route to fd02::/24 through the leaf switch, which uses the ::2 address on the same /64. To check the routing table for eth2, run:

ip -6 route show vrf vrf-eth2

The output looks similar to the following:

Output
fd02:0:400:527::/64 dev eth2 proto kernel metric 256 pref medium
fd02::/24 via fd02:0:400:527::2 dev eth2 proto static metric 1024 onlink pref medium
fe80::/64 dev eth2 proto kernel metric 256 pref medium
multicast ff00::/8 dev eth2 proto kernel metric 256 pref medium

Verify Fabric Connectivity

First, verify that every fabric NIC can reach its gateway, the leaf switch at the ::2 address on the NIC’s /64. Run the ping from inside the NIC’s VRF. For example, for eth2:

ip vrf exec vrf-eth2 ping -c3 fd02:0:400:527::2

To check all 16 fabric NICs at once, run the following loop. All 16 pings must succeed:

for i in $(seq 2 17); do
  gw=$(ip -6 route show vrf vrf-eth$i | awk '/via/ {print $3; exit}')
  ip vrf exec vrf-eth$i ping -c3 "$gw"
done

Next, verify connectivity across rails and Droplets within each plane. From each fabric NIC on this Droplet, ping the ULA (the ::1 address) of each of the eight fabric NICs on the same plane of a peer Droplet, for 64 pings per plane. All in-plane pings must succeed. For example, replace <peer-plane-0-ula> and <peer-plane-1-ula> with addresses from the peer Droplet’s ip -br a output:

ip vrf exec vrf-eth2 ping -c3 <peer-plane-0-ula>
ip vrf exec vrf-eth17 ping -c3 <peer-plane-1-ula>

Pings between the two planes, such as from eth2 to a peer’s eth3, fail because the planes are isolated.

Configure RDMA Applications

Because each fabric NIC is bound to its own VRF, the fabric addresses are not reachable through the default routing table. A command that doesn’t specify a source interface or VRF, such as ping fd02:0:400:527::1, fails with Network is unreachable. RDMA applications must either use the out-of-band interface, eth1, for connection establishment, or explicitly specify the source interface or VRF.

For NCCL configuration on B300 Droplets, see NVIDIA B300. When you choose a GID index, use the RoCE v2 entry for the NIC’s ULA, not its link-local address. See GID Index Selection.

We can't find any results for your search.

Try using different keywords or simplifying your search terms.