Skip to content

[softnpu, mcast] wire up the softnpu backend for multicast table ops - #405

Open
zeeshanlakhani wants to merge 16 commits into
zl/mcast-swadmfrom
zl/mcast-softnpu
Open

zeeshanlakhani wants to merge 16 commits into
zl/mcast-swadmfrom
zl/mcast-softnpu

Conversation

@zeeshanlakhani

@zeeshanlakhani zeeshanlakhani commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Previously, SoftNPU didn't interact with multicast. mc_group_create and mc_port_add returned a stubbed OperationUnsupported, and port remove/destroy were no-ops.

With the merge of bitmap-based replication in oxidecomputer/p4rs#240, SoftNPU can now replicate off of port bitmaps carried in the replication table entry itself. On Tofino, membership subscriptions live in the PRE and change independently of match-action state. On SoftNPU, membership is captured at write time, and a membership mutation involves rewriting these affected replication entries.

Includes:

Previously, SoftNPU didn't interact with multicast. `mc_group_create`
and `mc_port_add` returned a stubbed `OperationUnsupported`, and port
remove/destroy were no-ops.

With bitmap-based replication in p4rs ([#240](oxidecomputer/p4#240)),
SoftNPU can replicate off of port bitmaps carried in the replication
table entry itself. On the Tofino, membership subscriptions live in the
PRE and change independently of match-action state. In the SoftNPU work,
membership is captured at write time, and a membership mutation means
rewriting the affected replication entries (when triggered).

Includes:

- *asic/SoftNPU*:
  - Live replication entries are tracked per group and rewritten when
    members are added/removed. The replication tracking lock covers
    both the snapshot and table updates.
  - Source filter, mcast NAT (tagged and untagged), egress decap, and
    mcast src MAC tables now map onto their sidecar-lite tables
    respectively.
    - `egress.asic_id_to_port` maps ASIC port IDs to logical port
      numbers for indexing the decap bitmap (same as on Tofino).
  - `TableRemove` is sent before `TableAdd` because p4rs drops an
    add whose key already exists and keeps the older entry
    (action and params).
- *dpd/mcast*:
  - `mcast::reset` deletes each known group's entries before
    the table clears. SoftNPU's `Table::clear` is still a stub.
  - `del_entries_where` skips the stale-entry table sweep when the backend
    cannot list entries (e.g., in SoftNPU).
- softnpu-lib is pinned to `zl/multicast` @ softnpu#183, which pulls in
  p4#240. SoftNPU loads sidecar-lite#152 at runtime.
@zeeshanlakhani
zeeshanlakhani added this pull request to stack #401 October 15, 2026 13:38
@zeeshanlakhani zeeshanlakhani self-assigned this Oct 15, 2026
@zeeshanlakhani zeeshanlakhani added the multicast Multicast-related feature/impl label Oct 15, 2026
zeeshanlakhani added a commit to oxidecomputer/sidecar-lite that referenced this pull request Oct 26, 2026
This adds multicast replication to sidecar-lite, emulating DPD's sidecar.p4 on dendrite up through oxidecomputer/dendrite#405 following RFD 488.

We manage 2 bitmaps per underlay group (external and underlay), for which replication is driven by p4rs `replicate` (oxidecomputer/p4#240; we track p4 on main now). Ingress zeroes out at most one of these bitmaps determined by the geneve multicast option tag (`tag=0` zeroes out underlay; `tag=1` zeroes out external), and merges both.

Tag 0 (External) also decaps upon ingress. With no tag given, as on external NAT ingress packets, both bitmaps remain in place.

Only geneve-encapsulated multicast is replicated. Multicast packets that miss ingress NAT drop on the empty bitmap, matching DPD's `drop_mcastv{4,6}_no_group` semantics. Packet copies that remain encapsulated lose a hop on the outer IPv6 header, as modelled in `MulticastRouter6`.

External multicast is NAT-encapsulated into the underlay via `nat_v{4,6}_mcast(_untagged)`, reusing `forward_to_sled`. Tagged and untagged variants exist as separate tables since x4c cannot key directly on header validity. The encap piece now zeroes out the outer src, traffic class, flow label, and inner eth src, like DPD's `add_encap_headers`.

Egress decap checks `ff04::/64` and `tag=2`, matching DPD's egress `mcast_tag_check`, with per-port decap from a RID-keyed bitmap (`tbl_decap_ports`) and optional VLAN. `tag=2` decapsulation also rejects inner unicast. `tag=0` copies (already decapped in ingress) use the table for the VLAN. Dst MAC derivation (RFC 1112, RFC 2464) and per-port src MAC rewriting run on every packet copy.

Includes:

- The parser rejects `ff01` addresses outright (`ff02` *does* still go to the scrimlet).
- Multicast packets with a TTL/hop_limit <= 1 get dropped in ingress after the `fwd_from_scrimlet` return, letting scrimlet transmissions pass through.
- Both decapsulation paths drop inner TTL <= 1. IPv4 checksums use RFC 1624 (eqn. 3): decrementing the TTL adds 0xfeff in one's complement, which yields 0x0000 rather than 0xffff at the zero boundary, matching a full recompute (XOR is used here due to `~` not being parsable by x4c).
- scadm gets add/remove/dump for replication (v6), source filters, mcast NAT, decap ports, src MAC, and asic-id-to-port (for logical mapping).
- Unicast modifications:
  - The router drops TTL/hop_limit <= 1 and decrements remaining packets on routes to nonzero ports, updating the IPv4 checksum.
    - Port 0 (the scrimlet) skips the `<= 1` check and the decrement, while a TTL/hop_limit `= 0` still drops before route lookup, except on the boundary services decapsulation path.
    - DPD's Router4/6 still decrement service-bound packets.
  - The NAT encap zeroing described above is shared with unicast NAT.
  - Packets with a parsed Geneve header skip ingress NAT, including unicast.

@cfzimmerman cfzimmerman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Voxel exercises softnpu, right? Can you run a voxel test and post some validation results? I'll stamp once we know it works.

Comment thread asic/src/softnpu/mcast.rs
Comment on lines +30 to +32
/// On the production sidecar, the replication table holds a group identifier,
/// while the replication engine tracks live membership subscriptions, which means
/// that any port additions do not trigger table rewrites.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, how did you learn this? Are there docs somewhere that explain? Or is this from reading tofino-backend compiler source?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started from the TNA doc: §5.7.3 "Packet Replication Engine" (pp. 26-27), Ingress only hands the TM (traffic manager) a group id (mcast_grp_a/b, rid, exclusion ids; see §5.4 "Destinations"), and the control plane configures each group's L1 nodes / L2 ports separately (§5.7.3.1 "Multicast group configuration").

The fields we use are here: https://github.com/barefootnetworks/Open-Tofino/blob/5b6ef19873698fc7d9c49cb33fd54c5fca2ecadd/share/p4c/p4include/tofino1_base.p4#L154-L181. And, we interact with them here:

bf_mc_mgrp_create(mcast_hdl, dev_id, domain, &mut mgrp_hdl)
.check_error("creating multicast group")?;
}
Ok(mgrp_hdl)
}
fn mgrp_destroy(
mcast_hdl: bf_mc_session_hdl_t,
dev_id: bf_dev_id_t,
mgrp_hdl: bf_mc_mgrp_hdl_t,
) -> AsicResult<()> {
unsafe {
bf_mc_mgrp_destroy(mcast_hdl, dev_id, mgrp_hdl)
.check_error("destroying multicast group")?;
}
Ok(())
}
fn mgrp_get_count(
mcast_hdl: &Handle,
dev_id: bf_dev_id_t,
mut count: u32,
) -> AsicResult<usize> {
unsafe {
bf_mc_mgrp_get_count(mcast_hdl.bf_get().mcast_hdl, dev_id, &mut count)
.check_error("getting total count of multicast groups")?;
}
Ok(count as usize)
}
fn associate_node(
mcast_hdl: bf_mc_session_hdl_t,
dev_id: bf_dev_id_t,
mgrp_hdl: bf_mc_mgrp_hdl_t,
node_hdl: bf_mc_node_hdl_t,
exclusion_id: u16,
) -> AsicResult<()> {
unsafe {
bf_mc_associate_node(
mcast_hdl,
dev_id,
mgrp_hdl,
node_hdl,
exclusion_id != 0,
exclusion_id,
)
.check_error("associating multicast node")?;
}
Ok(())
}
fn dissociate_node(
mcast_hdl: bf_mc_session_hdl_t,
dev_id: bf_dev_id_t,
mgrp_hdl: bf_mc_mgrp_hdl_t,
node_hdl: bf_mc_node_hdl_t,
) -> AsicResult<()> {
unsafe {
bf_mc_dissociate_node(mcast_hdl, dev_id, mgrp_hdl, node_hdl)
.check_error("dissociating multicast node from group")?;
}
Ok(())
}
fn node_create(
mcast_hdl: bf_mc_session_hdl_t,
dev_id: bf_dev_id_t,
repl_id: u16,
portmap: &mut [u8],
lagmap: &mut [u8],
) -> AsicResult<u32> {
let mut node_hdl = 0u32;
unsafe {
bf_mc_node_create(

So, yeah, on the sidecar, a port join never rewrites the table entry. SoftNPU, on the other hand, has no PRE, which is why we insert the bitmap into the entry and rewrite it on membership updates.

Comment thread asic/src/softnpu/mcast.rs Resolved
Comment thread asic/src/softnpu/table.rs Resolved
Comment thread asic/src/softnpu/table.rs Resolved
Comment thread asic/src/softnpu/table.rs Resolved
Comment thread asic/src/softnpu/mcast.rs Resolved
Comment thread asic/src/softnpu/mcast.rs Resolved
Comment thread asic/src/softnpu/table.rs Resolved
@zeeshanlakhani

Copy link
Copy Markdown
Contributor Author

Voxel exercises softnpu, right? Can you run a voxel test and post some validation results? I'll stamp once we know it works.

Incoming with this line of work. Via oxidecomputer/voxel#75 and past iterations, I demo'ed the prototype set of branches working.

@cfzimmerman cfzimmerman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a reply on the table.rs diff. Otherwise looks good to me once we're confident in e2e voxel testing.

Comment thread asic/src/softnpu/table.rs Resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

multicast Multicast-related feature/impl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants