Skip to content

[mcast] remove multicast feature + keep routing table sizes - #371

Open
zeeshanlakhani wants to merge 8 commits into
mainfrom
zl/mcast-remove-feature
Open

zeeshanlakhani wants to merge 8 commits into
mainfrom
zl/mcast-remove-feature

Conversation

@zeeshanlakhani

@zeeshanlakhani zeeshanlakhani commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

This work is split up into two pieces in turning multicast "on" by default:

[mcast, feature] Feature removal

This removes the multicast feature gating altogether, making it to where multicast is always built into dpd and sidecar.p4.

[p4, sidecar] keep (IPV*_LPM_SIZE - 1) entries for routing tables w/ multicast

Removing the multicast feature gate got us over Tofino's 20-stage pipeline limit. The table resizing work in #268 matched the v6 LPM table size to what we had in v4, putting us at 20 stages without multicast.

In #208, I explored a few different approaches to refactor our pipeline, across ingress and egress, to give us more breathing room (stage-wise). However, we want to be efficient and targeted when making changes to the sidecar.

Reducing mcast table sizes was my first attempt. Route and replication tables fit in SRAM; SSM is TCAM. The result, sadly, was fewer TCAM blocks, but the same stage count.

Instead, I went the direction of fulfilling a Nils'ism:

XXX: this control could be moved to the Egress pipeline if we need more space
in the Ingress pipeline. Currently unicast packets are able to bypass that
pipeline, which is why we've tacked it on here. We could probably also merge
it with the MacRewrite control, as they are both per-port settings, but that
would present some weird semantics to the control plane daemon.

I moved the EgressFilter control to the egress pipeline to save a stage, carrying (or bridging) the NAT egress bit in the bridge header. I did not move the MacRewrite control over to handle both uni+multi(cast) in egress, but that was demonstrated in #208 for a follow-up (post-testing).

With this move, we got to 20 stages (19 after the main merge, below)! However, the v4/v6 route tests still stopped below 8191 entries with a deficit. The exact-match route-target tables were losing capacity under four-way cuckoo placement (default). @ways(8) was the trick I found that gives each key in the table eight placement choices, which now makes table tests fill all 8191 entries.

[merge] main (#398) fallout

@FelixMcFelix's flow hash (#398) merged atop this broke two things that previous incarnations of this PR didn't hit:

  • level2_mcast_hash: with flow_hash[15:3] being a shifted copy, this pushes flow_hash into a 32b container, and the egress decap bitmap no longer fits in PHV.
    • fix: level2 is now a Hash<bit<13>> over {flow_hash, ingress_port}.
    • Note: Neither level has an ECMP/LAG node to incorporate at the moment per the #398
      thread
  • With the changes upstream, p4c reused the match register holding dst_mac[39:32] for the IPv4 protocol byte, causing IPv4 multicast packets to be dropped due to "invalid MACs".
    • fix: the IPv4 mcast 01:00:5e check (IANA's 00:00:5e OUI with the group bit set) has been moved from the parser to the Filter control.
    • The existing IPv6 MAC check already lives in Filter, too.

Includes:

  • Removal of the #ifdef MULTICAST codegen guards.
  • Counter updates to match our move of the NAT egress filter.
  • common.sh follows with one limit TOFINO_STAGES=19 and with MULTICAST gone.

This removes the multicast feature gating altogether, making multicast
inclusive to the work in dendrite/dpd/sidecar.
…multicast

Removing the multicast feature gate got us over Tofino's 20-stage
pipeline limit. The table resizing work in #268 matched the v6 LPM
table size to what we had in v4, putting us at 20 stages without
multicast.

In #208, I explored a few different approaches to refactor our pipeline,
across ingress and egress, to give us more breathing room (stage-wise).
However, we want to be efficient and targeted when making changes to
the sidecar.

Reducing mcast table sizes was my first attempt. Route and replication tables
fit in SRAM; SSM is TCAM. The result, sadly, was fewer TCAM blocks, but
the same stage count.

Instead, I went the direction of fulfilling a Nils'ism:

> XXX: this control could be moved to the Egress pipeline if we need more space
> in the Ingress pipeline.  Currently unicast packets are able to bypass that
> pipeline, which is why we've tacked it on here.  We could probably also merge
> it with the MacRewrite control, as they are both per-port settings, but that
> would present some weird semantics to the control plane daemon.

I moved the `EgressFilter` control to the egress pipeline to save a
stage, carrying (or bridging) the NAT egress bit in the bridge header.
I *did not* move the `MacRewrite` control over to handle both
uni+multi(cast) in egress, but that was demonstrated in #208 for a
follow-up (post-testing).

With the move, 20 stages is achieved! However, the v4/v6 route tests
still stopped below 8191 entries with a deficit. The exact-match
route-target tables were losing capacity under four-way
cuckoo placement (default). [`@ways(8)`][ways] was the trick I found
that gives each key in the table eight placement choices, which now makes
table tests fill all 8191 entries.

Includes:
  - Removal of the `#ifdef MULTICAST` codegen guards.
  - Counter updates to match our move of the NAT egress filter
  - Ordering of the NAT egress filter before the CPU-copy
   (`egress_rid`) check, making sure NAT unicast is not treated as
   a multicast CPU copy.

[ways]: https://github.com/p4lang/p4c/blob/a19f1c3d85a867a6288fd983f7bad505ac47d728/backends/tofino/bf-p4c/common/pragma/pragmas.cpp#L1172-L1183

@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.

  1. From an aesthetic perspective, this PR is magnificent. Tysm for doing this. Very excited to see it land.
  2. What's the plan for validation? All good if E2E testing needs to come at the top of the stack, but do our current CI tests cover these changes well? Both for new functionality and to prevent regressions. If not, I'd appreciate whatever is needed to fill the gap.

Lmk once you feel confident about validation, and I'm happy to stamp.

Comment thread dpd-client/tests/integration_tests/table_tests.rs
Comment thread dpd/p4/constants.p4
Comment thread dpd/src/counters.rs Outdated
cfzimmerman

This comment was marked as resolved.

Comment thread dpd-client/tests/integration_tests/table_tests.rs
@cfzimmerman

This comment was marked as resolved.

@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.

Recent changes lgtm. Although a second reviewer warranted before merge, particularly to review the p4 diffs.

@FelixMcFelix FelixMcFelix 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.

Thanks Zeeshan.

Comment thread dpd/p4/constants.p4
Comment thread asic/src/tofino_asic/mod.rs Outdated
Comment thread dpd/src/link.rs Outdated
Comment thread dpd/src/macaddrs.rs Outdated
Comment thread dpd/p4/sidecar.p4
Comment thread dpd/p4/sidecar.p4
Comment on lines 2132 to 2141
if (ig_tm_md.ucast_egress_port != USER_SPACE_SERVICE_PORT) {
mac_rewrite.apply(hdr, ig_tm_md.ucast_egress_port);
}
meta.bridge_hdr.setInvalid();
ig_tm_md.bypass_egress = 1w1;
if (meta.nat_egress_hit && !meta.service_routed) {
meta.bridge_hdr.nat_egress_hit = true;
} else {
meta.bridge_hdr.setInvalid();
ig_tm_md.bypass_egress = 1w1;
}
}

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.

I think the change in meanings of counters is confusing enough that it should be documented or worked around.

My current read is that to get the true egress count on a port, you need to compute pipe.Ingress.egress_ctr[port_idx] - pipe.Egress.drop_port_ctr[port_idx]. Not unreasonable, but it requires decent knowledge of the dataplane to make sense. Thinking on alternate approaches:

  • Could we move the egress counter hit into the else block? This way the sum of packets allowed to leave on a port is pipe.Ingress.egress_ctr[port_idx] + pipe.Egress.unicast_ctr[port_idx] (plus + pipe.Egress.mcast_ctr[port_idx], if you're so inclined). The counter names are still a little misleading this way, in that Ingress.egress_ctr would be underlay plus inbound nat'd traffic, whereas Egress.unicast_ctr is outbound traffic being decapsulated successfully.
  • Does bypass_egress give us meaningful latency improvement? Does it make enough difference to prevent us pushing all packets through the egress pipeline, and then we only need pipe.Egress.unicast_ctr[port_idx]? This would also mesh well with making MacRewrite an unconditional part of the egress pipeline in future.

@zeeshanlakhani

Copy link
Copy Markdown
Contributor Author

On #371 (comment),

I was being a bit conservative here, but the second alternative approach is what I already explored in #208 (with the MAC rewriting, however). I'll make that update, as it's cleaner anyway and sets us up for that move.

This work follows up on Kyle's review, including adding documentation
on `@ways` defaults (and now refers to a more informational link)
and removing leftover, unnecessary `{}` blocks.

The main change is that every packet copy now runs through to the egress
pipeline, no longer relying on `bypass_egress` in the ingress pipeline.
This follows from #208 and makes the counters more explicit:

- `forwarded_ctr` records all packets leaving a port;
- `unicast_ctr` and `mcast_ctr` now partition what gets forwarded by type;
- drops are recorded separately with port and reason counters.

@FelixMcFelix FelixMcFelix 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.

Okay, I'm happier with these changes now. I think this keeps the counter state somewhat sensible. I don't know what we want the wider testing plan to be yet before we merge, given the risk of London-bridges type scenarios. Are we wanting to validate the whole PR stack with product assurance etc. beforehand?

@zeeshanlakhani

Copy link
Copy Markdown
Contributor Author

Okay, I'm happier with these changes now. I think this keeps the counter state somewhat sensible. I don't know what we want the wider testing plan to be yet before we merge, given the risk of London-bridges type scenarios. Are we wanting to validate the whole PR stack with product assurance etc. beforehand?

Good question on the latter. I'll tag @askfongjojo and @sion42x here as well.

@zeeshanlakhani
zeeshanlakhani removed this pull request from stack #372 September 23, 2026 18:38
@zeeshanlakhani
zeeshanlakhani added this pull request to stack #377 September 24, 2026 17:37
@zeeshanlakhani zeeshanlakhani self-assigned this Oct 6, 2026
@zeeshanlakhani
zeeshanlakhani removed this pull request from stack #377 October 6, 2026 02:42
@zeeshanlakhani
zeeshanlakhani added this pull request to stack #397 October 7, 2026 10:17
@zeeshanlakhani zeeshanlakhani added the multicast Multicast-related feature/impl label Oct 7, 2026
@zeeshanlakhani

Copy link
Copy Markdown
Contributor Author

I ran @rcgoodfellow's ry/tof toolset looking at the Tofino's compiled out for the sidecar here, pitting main vs this branch (which is gaining a stage while not losing table size(s)):

tof PHV

Ingress (tof phv -g ingress pipe/sidecar.bfa):

Ingress PHV main zl/mcast-remove-feature
Normal word 32 / 48 (66.7%) 26 / 48 (54.2%)
Normal half 19 / 72 (26.4%) 65 / 72 (90.3%)
Normal byte 5 / 48 (10.4%) 10 / 48 (20.8%)
Mocha word 16 / 16 (100.0%) 16 / 16 (100.0%)
Mocha half 23 / 24 (95.8%) 23 / 24 (95.8%)
Mocha byte 8 / 16 (50.0%) 7 / 16 (43.8%)
Dark word 4 / 16 (25.0%) 1 / 16 (6.2%)
Dark half 3 / 24 (12.5%) 14 / 24 (58.3%)
Dark byte 0 / 16 (0.0%) 1 / 16 (6.2%)
Total 110 / 280 (39.3%) 163 / 280 (58.2%)

Egress (tof phv -g egress pipe/sidecar.bfa):

Egress PHV main zl/mcast-remove-feature
Normal word 0 / 48 (0.0%) 12 / 48 (25.0%)
Normal half 0 / 72 (0.0%) 6 / 72 (8.3%)
Normal byte 0 / 48 (0.0%) 15 / 48 (31.2%)
Mocha word 0 / 16 (0.0%) 0 / 16 (0.0%)
Mocha half 2 / 24 (8.3%) 1 / 24 (4.2%)
Mocha byte 1 / 16 (6.2%) 10 / 16 (62.5%)
Dark word 0 / 16 (0.0%) 0 / 16 (0.0%)
Dark half 0 / 24 (0.0%) 1 / 24 (4.2%)
Dark byte 0 / 16 (0.0%) 2 / 16 (12.5%)
Total 3 / 280 (1.1%) 47 / 280 (16.8%)

Both Ingress and Egress (tof phv pipe/sidecar.bfa):

Total PHV main zl/mcast-remove-feature
Normal word 32 / 48 (66.7%) 37 / 48 (77.1%)
Normal half 19 / 72 (26.4%) 69 / 72 (95.8%)
Normal byte 5 / 48 (10.4%) 24 / 48 (50.0%)
Mocha word 16 / 16 (100.0%) 16 / 16 (100.0%)
Mocha half 24 / 24 (100.0%) 24 / 24 (100.0%)
Mocha byte 8 / 16 (50.0%) 16 / 16 (100.0%)
Dark word 4 / 16 (25.0%) 1 / 16 (6.2%)
Dark half 3 / 24 (12.5%) 15 / 24 (62.5%)
Dark byte 0 / 16 (0.0%) 3 / 16 (18.8%)
Total 111 / 280 (39.6%) 205 / 280 (73.2%)

I also ran tof with vars + container kinds, focused on fields sharing ingress normal 16-bit containers (e.g. tof vars -g ingress --container-kind normal --container-size half):

Build Containers used Shared by >1 field Most fields in one
main 19 12 4
zl/mcast-remove-feature 65 32 13

@zeeshanlakhani
zeeshanlakhani removed this pull request from stack #397 October 13, 2026 13:58
@zeeshanlakhani
zeeshanlakhani added this pull request to stack #401 October 13, 2026 13:59
Comment thread dpd/p4/parser.p4 Outdated
Comment on lines 596 to 599
transition select(hdr.ipv4.protocol) {
IPPROTO_ICMP: parse_icmp;
IPPROTO_TCP: parse_tcp;
IPPROTO_UDP: parse_udp;
default: accept;
}

@cfzimmerman cfzimmerman Oct 16, 2026 •

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.

In my understanding, we only parse the IPv4 header on egress to check geneve options. And afaik an Oxide geneve header should only ever live behind an outer IPv6 header. So can we just skip parsing an L4 header on egress for IPv4 packets? Because our spec asserts there's never a geneve header within an IPv4 packet?

By the same principle, I'm curious if we can skip parsing a geneve header on IPv4 ingress as well, but that's beyond the scope of this PR.

If sidecar must support IPv4 encapsulated geneve traffic (why?), then this should probably be aware of IPv4 fragments and options. Because in both cases the bytes you're parsing here as an L4 header may be something else entirely. I'm handling this for ingress in another branch right now.

@cfzimmerman cfzimmerman Oct 17, 2026 •

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.

Update: I would like to discuss this further. But maybe it doesn't need to hold up this PR because I'll be proposing it in my own PR soon. I would like to at least have a plan for dealing with irregularly shaped IPv4 packets on egress before we merge this.

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.

So, I ended up taking this on here. It's worth it, I'd say.

Egress parse_ipv4 will now just extract and accept. For IPv6, we only continue to UDP/Geneve parsing when the outer dst is in our underlay ff04::/64 block (and the next_hdr is UDP), as that's the only thing the egress pipeline actually needs Geneve for.

The tof2 parser only has four 8-bit match registers (citation provided in the diff), which meant splitting the /64 check across parser states. I went with 16 bits per state to keep them uniform. I'll leave ingress (fragments/options) to your upcoming work.

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.

Nice! We'll need Ry's review on this one as well, but imo this is a great conceptual win. And I think it's a step closer to rfd 63.


Worth noting, I believe we compute an incorrect checksum if the egressing IPv4 packet has options because option bytes should be part of the checksum.

dendrite/dpd/p4/sidecar.p4

Lines 2387 to 2401 in bc40ea7

// We only need to recalculate the checksum if the packet is
// modified in the case of replication to both external and
// underlay multicast ports, as the TTL and hop limit
// are decremented if packets headed toward external multicast
// subscribers are decapped/stripped.
if (meta.ipv4_checksum_recalc && hdr.inner_ipv4.isValid()) {
hdr.inner_ipv4.hdr_checksum = ipv4_checksum.update({
hdr.inner_ipv4.version, hdr.inner_ipv4.ihl, hdr.inner_ipv4.diffserv,
hdr.inner_ipv4.total_len,
hdr.inner_ipv4.identification,
hdr.inner_ipv4.flags, hdr.inner_ipv4.frag_offset,
hdr.inner_ipv4.ttl, hdr.inner_ipv4.protocol,
hdr.inner_ipv4.src_addr,
hdr.inner_ipv4.dst_addr
});

But that's a niche case, and IPv4 ingress currently has the same checksum gap. I'm inclined to want all checksum fixes in the branch where I fix ingress. Parsing IPv4 options for all cases in a single PR might be easiest, particularly since the data structure for options bytes is a murky design decision.

So in summary, lgtm!

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.

Yeah, makes sense to come back to this in one checksum swoop.

> In my understanding, we only parse the IPv4 header on egress to check
> geneve options. And afaik an Oxide geneve header should only ever live
> behind an outer IPv6 header. So can we just skip parsing an L4 header
> on egress for IPv4 packets? Because our spec asserts there's never a
> geneve header within an IPv4 packet?

This was a good point. The egress pipeline only needs Geneve handling
for underlay mcast replicas (outer hdr with `ff04::/64`, always
IPv6), but we were walking `IPv4` -> `UDP` -> `Geneve` for UDP traffic
to the Geneve port (6081), and similarly for IPv6, where decapped
external replicas or customer traffic to port 6081 could end up in the
Geneve parser. For IPv4, options or a later fragment's payload
could have been read as UDP, with an apparent dst port of 6081
transitioning into the Geneve parser state.

Now, we get:

- IPv4: extract the header and accept
- IPv6: only goes on to UDP for `ff04::/64` + UDP.
@rcgoodfellow

Copy link
Copy Markdown
Collaborator

Has this been run on a racklette where we've seen mcast packets flowing from an off-rack source all the way down to a target instance? I think we want to get through that test before we merge this.

When merging forward the `main` branch's/bookmark's flow hash (#398)
work, two components broke in the combination:

- `level2_mcast_hash`: With `flow_hash[15:3]` being a shifted copy, this
  pushes `flow_hash` into a 32b container, and the egress decap bitmap
  no longer fits in PHV.
  - *fix*: level2 is now a `Hash<bit<13>>` over `{flow_hash, ingress_port}`
  - Note: Neither level has an ECMP/LAG node to incorporate at the moment
    per the #398 thread.
- With the changes upstream, p4c mistakenly reused the match register
  holding `dst_mac[39:32]` for the IPv4 protocol byte, causing IPv4
  multicast packets to be dropped due to "invalid MACs".
  - This existing IPv6 MAC check is also in the `Filter` control.
  - *fix*: the IPv4 mcast OUI (Organizationally Unique Identifier) check
    has been moved from the parser to the `Filter` control.

Includes:

- `TOFINO_STAGES=19` remains the same as `main` and is unconditional.
- Usable IPv4 route capacity doesn't change here.
@zeeshanlakhani
zeeshanlakhani removed this pull request from stack #401 October 21, 2026 01:55
@zeeshanlakhani
zeeshanlakhani added this pull request to stack #410 October 21, 2026 01:59
Comment thread dpd/p4/parser.p4 Outdated
@zeeshanlakhani

Copy link
Copy Markdown
Contributor Author

Note that there's some TTL/parser cleanup that happens in #400 as well, specific to TTL/hop limit checking. cc @taspelund.

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.

4 participants