Repository navigation
[multicast] Bitmap-based replication and bit-slice assignment for softnpu/a4x2 - #240
Conversation
…protocol This extends the management message protocol with operations for multicast group lifecycle: create, destroy, add port, remove port, and list. These dispatch to the corresponding [p4rs](https://github.com/oxidecomputer/p4) Pipeline trait methods (add_mcast_group, remove_mcast_group, add_mcast_port, etc.) that were added to support bifurcated multicast replication in the p4rs codegen. Mutating operations (create, destroy, add, remove) are fire-and-forget, matching the existing TableAdd/TableRemove pattern. `MulticastGroupList` is the only round-trip operation, returning a sorted group-to-ports mapping for use by dendrite's `AsicMulticastOps` queries. The `MulticastPortAdd` struct carries rid and level1_excl_id for API parity with dendrite's AsicMulticastOps trait, though softnpu does not use them. Tofino's PRE handles per-replica identification and exclusion via these fields. softnpu handles the equivalent via `McastReplicationTag` in the codegenning. This branch depends on the related oxidecomputer/p4#240.
fc3bde8 to
e07a034
Compare
…protocol This extends the management message protocol with operations for multicast group lifecycle: create, destroy, add port, remove port, and list. These dispatch to the corresponding [p4rs](https://github.com/oxidecomputer/p4) Pipeline trait methods (add_mcast_group, remove_mcast_group, add_mcast_port, etc.) that were added to support bifurcated multicast replication in the p4rs codegen. Mutating operations (create, destroy, add, remove) are fire-and-forget, matching the existing TableAdd/TableRemove pattern. `MulticastGroupList` is the only round-trip operation, returning a sorted group-to-ports mapping for use by dendrite's `AsicMulticastOps` queries. The `MulticastPortAdd` struct carries rid and level1_excl_id for API parity with dendrite's AsicMulticastOps trait, though softnpu does not use them. Tofino's PRE handles per-replica identification and exclusion via these fields. softnpu handles the equivalent via `McastReplicationTag` in the codegenning. This branch depends on the related oxidecomputer/p4#240.
a0ee3d2 to
0e20707
Compare
…protocol This extends the management message protocol with operations for multicast group lifecycle: create, destroy, add port, remove port, and list. These dispatch to the corresponding [p4rs](https://github.com/oxidecomputer/p4) Pipeline trait methods (add_mcast_group, remove_mcast_group, add_mcast_port, etc.) that were added to support bifurcated multicast replication in the p4rs codegen. Mutating operations (create, destroy, add, remove) are fire-and-forget, matching the existing TableAdd/TableRemove pattern. `MulticastGroupList` is the only round-trip operation, returning a sorted group-to-ports mapping for use by dendrite's `AsicMulticastOps` queries. The `MulticastPortAdd` struct carries rid and level1_excl_id for API parity with dendrite's AsicMulticastOps trait, though softnpu does not use them. Tofino's PRE handles per-replica identification and exclusion via these fields. softnpu handles the equivalent via `McastReplicationTag` in the codegenning. This branch depends on the related oxidecomputer/p4#240.
c6c1858 to
3df53f6
Compare
…protocol This extends the management message protocol with operations for multicast group lifecycle: create, destroy, add port, remove port, and list. These dispatch to the corresponding [p4rs](https://github.com/oxidecomputer/p4) Pipeline trait methods (add_mcast_group, remove_mcast_group, add_mcast_port, etc.) that were added to support bifurcated multicast replication in the p4rs codegen. Mutating operations (create, destroy, add, remove) are fire-and-forget, matching the existing TableAdd/TableRemove pattern. `MulticastGroupList` is the only round-trip operation, returning a sorted group-to-ports mapping for use by dendrite's `AsicMulticastOps` queries. The `MulticastPortAdd` struct carries rid and level1_excl_id for API parity with dendrite's AsicMulticastOps trait, though softnpu does not use them. Tofino's PRE handles per-replica identification and exclusion via these fields. softnpu handles the equivalent via `McastReplicationTag` in the codegenning. This branch depends on the related oxidecomputer/p4#240.
3df53f6 to
3e15edd
Compare
This adds multicast replication to sidecar-lite, emulating DPD's sidecar.p4 We manage 2 bitmaps per multicast destination (external, underlay), in which replication is driven via the `Replicate` extern (from p4rs #240). The ingress control block suppresses (or doesn't) these bitmaps based on the geneve multicast option tag, and then merges both into a single replication bitmap. This tag is read-only, matching DPD's sidecar.p4. OPTE (would) stamp it before the packet reaches the switch. Egress decap is gated on the reserved underlay subnet (ff04::/64, per RFC 7346 admin-local scope + Omicron) and tag=2 (Both), matching DPD's egress mcast_tag_check. Ports in the decap table get geneve stripped, others keep encapsulation for OPTE to handle on the receiving sled. Multicast dst MAC derivation (RFC 1112 for IPv4, RFC 2464 for IPv6) and per-port source MAC rewrite run on every replicated copy. Egress validates the outer IP multicast range before any mcast processing. ### References This work depends on the P4(rs) work in [#240](oxidecomputer/p4#240).
3e15edd to
06dd1de
Compare
…tnpu/a4x2
## Replication
This introduces bitmap-based packet replication for softnpu. Replication is
opt-in via a Replicate extern that P4 programs declare and call explicitly:
```p4
extern Replicate {
void replicate(in bit<128> bitmap);
}
Replicate() rep;
rep.replicate(egress.bitmap_a | egress.bitmap_b);
```
The codegen scans the AST for the extern call, extracts the bitmap expression,
and generates the replication loop at the pipeline level between ingress and
egress. The call is elided from generated code after compile-time validation
of the argument.
`p4rs::replicate()` collects set bits from the bitmap expression,
filtering out the ingress port to prevent self-replication. It interprets
bitmaps in little-endian integer order: bit N (value 2^N) corresponds to port N.
This matches the encoding produced by P4 shifting (`128w1 << port`) via `shl_le`,
so replication bitmaps and shift-based bitmap checks use the same convention.
The pipeline codegen no longer hardcodes metadata struct names
(`ingress_metadata_t`, `egress_metadata_t`), deriving variable names and
types from P4 control parameter declarations.
Generated P4 structs now initialize `bit<N>` fields to properly-sized
zero bitvecs (`bitvec![u8, Msb0; 0; N]`) instead of empty `BitVec`
(length 0). This fixes bitmap comparisons and arithmetic on
uninitialized metadata fields.
## Shift operators
Adds left shift ("<<") and right shift (">>") support across the full
compiler pipeline: lexer, parser, AST, HLIR, type checker, and codegen.
The lexer previously tokenized "<<" but was not wired through the parser
or AST (yet).
## Bit-slice assignment
Adds `Statement::SliceAssignment` for P4-16 spec 8.6 `lval[hi:lo] = expr`
syntax, including parser updates, HLIR bounds validation, type checking
(RHS width must equal hi - lo + 1), and codegen with byte-reversal-aware bitvec
range mapping. Non-contiguous slices after byte reversal fall back to
arithmetic extraction.
Slice reads assigned to local variables (e.g., `bit<16> lo = x[15:0]`)
produce owned BitVec values via `.to_bitvec()`.
## Slice codegen handling
- Fixes a latent issue where the slice-to-bitvec mapping ignored header byte
reversal, producing incorrect ranges for sub-byte slices on multi-byte fields
(e.g., field[31:28] on bit<32>). Fixes Varbit/Int slice reads, which were
rejected due to swapped destructure naming.
- Fixes single-bit slices (x[n:n]) rejected in the read context.
## Bitwise operators
Clones operands for BitOr, BitAnd, Xor, and Mask in the Expression
codegen. The previous generated code moved out of mutable references for
BitVec operands, which does not implement Copy.
## Tests
- Bitmap replication (port selection, self-replication filtering, empty
bitmap, broadcast precedence), emulating multicast concepts.
- Slice assignment with RFC 1112 MAC derivation and same-field aliasing.
- Shift operators and width resizing.
- Sub-byte slice reads verify byte-reversal correctness.
06dd1de to
0e8a28a
Compare
This wires up the softnpu ASIC backend to support multicast end-to-end by translating DPD's sidecar.p4 table operations into sidecar-lite.p4's simplified P4 pipeline. ## AsicMulticastOps We replace the stubbed AsicMulticastOps implementation (which returned "OperationUnsupported" for group creation and port addition with in-memory group tracking via McGroupData, following the tofino_stub pattern. Group membership is used by the table translation layer to build port bitmaps for sidecar-lite's Replicate extern. Ports >= 128 are rejected at add time to match sidecar-lite's 128-bit bitmap width. ## Table translation (asic/src/softnpu/table.rs) We map sidecar.p4 table names to sidecar-lite equivalents and translate action parameters where the designs differ for emulation. All multicast action arms are gated with #[cfg(feature = "multicast")]. ## References - [softnpu #183](oxidecomputer/softnpu#183) - [propolis #1093](oxidecomputer/propolis#1093) - [p4rs #240](oxidecomputer/p4#240) - [sidecar-lite #152](oxidecomputer/sidecar-lite#152) - tokio: 1.50 (due to softnpu) - oxide-tokio-rt: 0.1.3 (following-up from tokio's move to 1.50)
This wires up the softnpu ASIC backend to support multicast end-to-end by translating DPD's sidecar.p4 table operations into sidecar-lite.p4's simplified P4 pipeline. ## AsicMulticastOps We replace the stubbed AsicMulticastOps implementation (which returned "OperationUnsupported" for group creation and port addition with in-memory group tracking via McGroupData, following the tofino_stub pattern. Group membership is used by the table translation layer to build port bitmaps for sidecar-lite's Replicate extern. Ports >= 128 are rejected at add time to match sidecar-lite's 128-bit bitmap width. ## Table translation (asic/src/softnpu/table.rs) We map sidecar.p4 table names to sidecar-lite equivalents and translate action parameters where the designs differ for emulation. All multicast action arms are gated with #[cfg(feature = "multicast")]. ## References - [softnpu #183](oxidecomputer/softnpu#183) - [propolis #1093](oxidecomputer/propolis#1093) - [p4rs #240](oxidecomputer/p4#240) - [sidecar-lite #152](oxidecomputer/sidecar-lite#152) - tokio: 1.50 (due to softnpu) - oxide-tokio-rt: 0.1.3 (following-up from tokio's move to 1.50)
This wires up the softnpu ASIC backend to support multicast end-to-end by translating DPD's sidecar.p4 table operations into sidecar-lite.p4's simplified P4 pipeline. ## AsicMulticastOps We replace the stubbed AsicMulticastOps implementation (which returned "OperationUnsupported" for group creation and port addition with in-memory group tracking via McGroupData, following the tofino_stub pattern. Group membership is used by the table translation layer to build port bitmaps for sidecar-lite's Replicate extern. Ports >= 128 are rejected at add time to match sidecar-lite's 128-bit bitmap width. ## Table translation (asic/src/softnpu/table.rs) We map sidecar.p4 table names to sidecar-lite equivalents and translate action parameters where the designs differ for emulation. ## Fix update semantics We fix an issue with stale in-memory sources after update with absent source list(s). `modify_group_external` wrote back `new.sources.or(existing)` to the in-memory record while `update_external_tables` in the same call canonicalized `new.sources` directly to rewrite the P4 source-filter entries. A `None` update therefore cleared the P4 table but left a stale `Some([...])` in memory. All multicast action arms are gated with #[cfg(feature = "multicast")]. ## References and Deps - [softnpu #183](oxidecomputer/softnpu#183) - [propolis #1093](oxidecomputer/propolis#1093) - [p4rs #240](oxidecomputer/p4#240) - [sidecar-lite #152](oxidecomputer/sidecar-lite#152) - tokio: 1.50 (due to softnpu) - oxide-tokio-rt: 0.1.3 (following-up from tokio's move to 1.50) - bump oxide-tokio-rt to 0.1.4, tokio to 1.52.1 (matching Omicron)
This adds multicast replication to sidecar-lite, emulating DPD's sidecar.p4 We manage 2 bitmaps per multicast destination (external, underlay), in which replication is driven via the `Replicate` extern (from p4rs #240). The ingress control block suppresses (or doesn't) these bitmaps based on the geneve multicast option tag, and then merges both into a single replication bitmap. This tag is read-only, matching DPD's sidecar.p4. OPTE (would) stamp it before the packet reaches the switch. Egress decap is gated on the reserved underlay subnet (ff04::/64, per RFC 7346 admin-local scope + Omicron) and tag=2 (Both), matching DPD's egress mcast_tag_check. Ports in the decap table get geneve stripped, others keep encapsulation for OPTE to handle on the receiving sled. Multicast dst MAC derivation (RFC 1112 for IPv4, RFC 2464 for IPv6) and per-port source MAC rewrite run on every replicated copy. Egress validates the outer IP multicast range before any mcast processing. Decap port selection uses a RID-keyed bitmap table (tbl_decap_ports), matching Dendrite's tbl_decap_ports + port_bitmap_check. The egress shifts 1 << egress.port and ANDs against the bitmap to determine per-port decap, with optional VLAN insertion (following the current sidecar in DPD). We augmented the IPv4 header checksum after TTL decrement to use ones-complement addition with carry fold via 16-to-32-bit widening. ### References This work depends on the P4(rs) work in [#240](oxidecomputer/p4#240).
The mainline PR points softnpu at main to merge in pieces. a4x2 needs bitmap-based replication from softnpu zl/multicast, so this branch carries the pin until oxidecomputer/p4#240 and oxidecomputer/softnpu#183 land.
|
What does the Having replication state in metadata as a field also makes things a bit more debuggable, as then it's a concrete value in the state of the pipeline that things like dtrace scripts can observe. |
Yep. I'm actually taking this approach in my cleanup work that will go in soon. The original thinking was to match and model the PRE being a fixed-fucntion block on hardware with an extern, while also avoiding some extra magic field naming. The other thing pushing me in this direction is that I'll continue to move to the simpler mechanism. |
Includes:
- Per Ry's review, this drops the `Replicate` extern and the AST scanning that
went with it for a `port_bitmap` field in egress metadata, read after
ingress, ahead of broadcast/unicast. It'll now show up in dtrace
metadata dumps for debugging, like other fields. Each selected port
runs egress with its own copy of headers and metadata.
- The example/test sidecar-lite is modified to remove its `Replicate`
extern.
- The mcast fixture now carries dendrite-style MAC splitting, i.e.
unicast in the ingress pipeline and replicas in the egress pipeline,
while the shift, slice, and metadata-names tests check and assert on
packet bytes and `process_packet_headers` together.
- Fixes and tightening around expression precedence, shift typing (>=
width returns zero), unsigned comparisons, slice reads/writes, and
operator carets in diagnostics.
- bit<W> -> bit<V> widening keeps the existing value (for sub-byte
widths too).
- This is an x4c extension at the moment since there's no casting yet,
and narrowing is now rejected.
- Added HLIR checks on initializers, assignments, and slice bounds: runtime
(unsized) int is rejected, and x4c/SoftNPU limitations get bubbled up
instead of looking just like type errors.
|
@rcgoodfellow back up for review. |
This cleanup does two things: - The checker now errors appropriately when a table's default action isn't listed in its actions list (P4-16 14.2.1.3). I've updated the fixtures/tests. - `dynamic_router_noaddr.p4` failed to compile (on `main` even) because its `local` action shadowed the `local` table, but nothing built it, and it went unnoticed. This is now fixed.
When adding new tests to sidecar-lite (based on this dep), I started seeing things like VLAN 300 coming out as 44 (and 256 as 0) on a decap action. VLAN 100 in the tests hid this issue because it fit into 1 byte. This work fixes the issue in `extract_bit_action_parameter`, which used `resize(n)` and dropped the low nibble of the high byte. We now shift the partial last byte into the high bits, the same layout as `store_le` and the header fields, which works for any width. Plus, we also include the latent codegen offset (`n >> 3` -> `div_ceil(8)`), short `parameter_data` now being ignored vs panicking, and updated harness checks (vid, timeout, extra frames).
This adds multicast replication to sidecar-lite, emulating DPD's
sidecar.p4 on dendrite @ zl/mcast-swadm, following RFD 488.
We manage 2 bitmaps per underlay group (external and underlay), for
which replication is driven by p4rs `replicate` (dep @ p4#240). 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 commits to replication. 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, as well as `ff02` destined
packets with a hop limit of 0 (`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): a checksum of 0xfeff turns into 0x0000 after
decrementing the TTL (XOR 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 both the TTL check and decrement.
- 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.
### References
This work depends on the P4(rs) work in
[#240](oxidecomputer/p4#240) and softnpu
zl/multicast.
rcgoodfellow
left a comment
There was a problem hiding this comment.
Thanks @zeeshanlakhani. Things are looking much better after moving from an extern to metadata for multicast egress replication. Also some great compiler additions here! Comments follow.
| }; | ||
|
|
||
| let (hi, lo) = Self::slice_bounds(begin, end); | ||
| if Self::slice_is_contiguous(hi, lo, field_width) { |
There was a problem hiding this comment.
slice_is_contiguous calls reversed_slice_range and returns is_some on the result (effectively throwing away the calculated indices) and then generate_slice calls reversed_slice_range again with an unwrap to get the actual start and end indices. It's seems like the flow could be improved here to only call reversed_slice_range once and then match on the resulting Option for generate_slice obviating the need for the extra calls and the unwrap.
But a higher-level question for the entire implementation: is generate_slice_read_arith sufficiently general for both cases? It's a LOT simpler than going through all the index calculations and it may actually be faster code at the end of the day to perform the slice operation through a simple shift on an integer rather than futzing with index extraction over the bitvec.
There was a problem hiding this comment.
Yeah, you're right. It should be general enough already and had handled every non-contiguous slice plus all the non-byte-multiple widths > 8.
The next push will use arithmetic for all slice reads and writes. I've dropped reversed_slice_range, slice_is_contiguous, and generate_slice methods.
Note: one thing did come up during this edit: the mask was (1u128 << w) - 1, which overflows for a full [127:0]. It never hit previously because only the range path took that case; it's u128::MAX >> (128 - w) now.
| #(#members),* | ||
| } | ||
|
|
||
| impl Default for #name { |
There was a problem hiding this comment.
What required us to move away from a derived default impl?
There was a problem hiding this comment.
I needed the bitvecs to maintain their declared widths, but the derived Default implementation left them empty, and slice reads panic in load_le on empty bitvecs.
We've been getting away with this through is_empty() guards in p4rs and with resize in slice assignment.
| }); | ||
| return; | ||
| } | ||
| let Some(name_info) = names.get(name) else { |
There was a problem hiding this comment.
Why are we dropping the error diagnostic for undefined call names?
There was a problem hiding this comment.
Actually, I'll restore it, similar reasoning initially to the one above.
| self.parse_binary(0) | ||
| } | ||
|
|
||
| fn parse_binary(&mut self, min_prec: u8) -> Result<Box<Expression>, Error> { |
There was a problem hiding this comment.
It's a bit odd that a function called parse_binary can return expressions that are not binary expressions.
There was a problem hiding this comment.
Good point. It's precedence related really, so I've renamed it to parse_with_precedence.
| egress.broadcast = true; | ||
| } | ||
|
|
||
| action broadcast_drop() { |
There was a problem hiding this comment.
is a broadcast drop somehow different than a unicast drop? Seems like it should be the case that if egress.drop is set, it should not really matter what egress.broadcast or egress.port are.
There was a problem hiding this comment.
Nope, it's the same drop semantics. One action covers drop with broadcast set and the other with the unicast port set (drop_precedes_broadcast drop_precedes_unicast in hub.rs).
On main, broadcast packet copies ran through egress before checking drop (note: egress can clear it). Now, drop is checked before selecting any ports. Packets with drop set after ingress never actually reach egress.
rcgoodfellow
left a comment
There was a problem hiding this comment.
Thanks @zeeshanlakhani. LGTM
oxidecomputer/p4#240 (c8b190f) makes x4c reject a table whose default_action is not in its actions list (P4-16 14.2.1.3). Add NoAction to the six tables that default to it, and drop to both rtr tables, and move the p4 lock to c8b190f so the build runs the check. Stop at c8b190f rather than p4 main: #278 (aa0d3c8) inserts Pipeline::clear_table_entries mid-trait, so a libsidecar_lite.so built against it would not match the vtable of softnpu hosts built against earlier p4rs. Signed-off-by: Trey Aspelund <trey@oxidecomputer.com>
This work includes and/or incorporates:
Bitmap-based packet replication for softnpu: a P4 program can make use of a
port_bitmapfield in its egress metadata and set the field during ingress (via table action, metadata assignment, etc.):check_port_bitmaprequiring abit<N>.p4rs::bitmap_to_port_list()function skips the ingress port and bits at or above the givenradix. If a non-zero bitmap filters down to no ports, the packet gets dropped. A zeroed bitmap falls back to broadcast/unicast handling. Bit N (value 2^N) is port N, which is the same encoding128w1 << portproduces viashl_le.dropis now checked before any port selection takes place, enforcing that the egress pipeline never runs on a dropped packet.The pipeline codegen no longer hardcodes the
ingress_metadata_t/egress_metadata_ttypes, which are now derived from the control parameters. The generated locals use fixed names (i.e.,__ingress_metadata,__egress_metadata) in order to prevent P4 parameter names from shadowing pipeline locals.Fixtures have been updated, including the in-repo sidecar-lite example, which now makes use of a
mcast_replication_v6table whoseset_port_bitmapaction drives theport_bitmapfield.The addition of shift operators (
<<,>>). Binary expressions follow operator precedence, associating operators at equal precedence from left to right.The addition of bit-slice assignment (
lval[hi:lo] = expr, P4-16 8.6), and slice reads are fixed now too.bit<W>slices are lowered, whereasvarbitslicing results in a language error (8.10), andint<W>/compile-timeintslicing are reported as SoftNPU/x4c restrictions.Fixes and tightening around unsigned comparisons and diagnostic carets, which are now pulled from the token spans themselves, so that operators and full dotted names get marked properly.
bit<W>->bit<V>widening keeps the existing value (for sub-byte widths too).New HLIR checks on initializers, assignments, and slice bounds: runtime (unsized) int is rejected, and x4c/SoftNPU limitations get bubbled up instead of looking just like type errors.
use_p4!macro now parses every file before AST checking, returning semantic errors as asyn::Error.Unaligned
bit<N>action parameters have been fixed. For example, VLAN 300 returned as 44 (and 256 as 0!).New check: a table's
default_actionmust appear in itsactionslist (P4-16 14.2.1.3).Bug fix:
<=never lexed (s/">="/"<="/ forLessThanEquals).