On 30 Jun 2026, at 1:20, Ilya Maximets wrote:
> On 6/23/26 9:19 PM, Eelco Chaudron via dev wrote:
>> This patch introduces a new API to the offload provider framework that
>> allows hardware offload implementations to control hash value calculation
>> for the OVS_ACTION_ATTR_HASH action.
>>
>> Background and Motivation
>> =========================
>>
>> The OVS hash action (OVS_ACTION_ATTR_HASH) is used to compute a hash value
>> from packet header fields, primarily for load balancing across multiple
>> paths using the select group action. The hash value is stored in the
>> packet's metadata and used by subsequent actions to distribute flows
>> across multiple output ports.
>>
>> However, hardware offload implementations may require different approaches
>> to hash calculation:
>>
>> 1. Hardware NICs may use different hash functions or hash inputs than
>> the software datapath, which can lead to inconsistent load distribution
>> when mixing hardware and software paths.
>>
>> 2. Some hardware may support enhanced hashing mechanisms (e.g., using
>> symmetric hashing for bidirectional flows or hardware-specific hash
>> engines) that provide better load distribution than the default
>> software implementation.
>>
>> Design
>> ======
>>
>> This patch adds a new optional callback to the dpif_offload_class:
>>
>> bool (*netdev_get_dp_hash)(const struct dpif_offload *,
>> const struct netdev *ingress_netdev,
>> struct dp_packet *,
>> const struct ovs_action_hash *, uint32_t *hash);
>>
>> To integrate this into the action execution path, a new optional callback
>> type, odp_hash_cb, is passed to odp_execute_actions() to allow per-packet
>> hash overrides during OVS_ACTION_ATTR_HASH processing. The dpif-netdev
>> datapath provides an implementation that calls the offload provider's
>> netdev_get_dp_hash when hardware offload is enabled and the original
>> ingress port is known. If the provider returns true, the returned hash
>> value is used; otherwise, OVS falls back to the standard hash calculation.
>>
>> Signed-off-by: Eelco Chaudron <[email protected]>
[...]
>> +static bool
>> +dp_hash_override(void *aux_, struct dp_packet *packet,
>> + const struct ovs_action_hash *hash_act, uint32_t *hash)
>> +{
>> + struct dp_netdev_execute_aux *aux = aux_;
>> + struct dp_netdev_pmd_thread *pmd = aux->pmd;
>> +
>> + if (!dpif_offload_enabled()) {
>> + return false;
>> + }
>> +
>> + if (aux->cached_in_odpp != packet->md.orig_in_port
>> + || !aux->cached_in_netdev) {
>> + struct tx_port *in_port = pmd_send_port_cache_lookup(
>> + pmd, packet->md.orig_in_port);
>> +
>> + if (!in_port) {
>> + return false;
>> + }
>> + aux->cached_in_odpp = packet->md.orig_in_port;
>> + aux->cached_in_netdev = in_port->port->netdev;
>> + }
>> +
>> + return dpif_offload_netdev_get_dp_hash(aux->cached_in_netdev, packet,
>> + hash_act, hash);
>> +}
>
> I wonder why this function is specific to userspace datapath?
> The only specifc thing here is the port lookup, but it should
> not actually be specific. We should be able to lookup a netdev
> by port number for any datapath type, e.g. from the offload
> module itself - dpif_offload_get_netdev_by_port_id().
>
> It also seems wrong that the callback is not defined for the
> standard path of the dpif-netlink. Since we decoupled the
> offload layer form the dpif implementation, generic action
> execution should work the same way regardless of the datapath.
>
> And if dp_hash_override() doesn't need to be datapath-specific,
> there is no point for it to be a callback, it could be just a
> function inside the odp-execute module, right? Or am I missing
> something here?
>
> The call into hardware/firmware to obtain the hash will be
> expensive, so I'm also not sure if the netdev caching is stricty
> Necessary here.
I did some experiments to see what the alternative looks like. For
this to work, we need a way to look up the original netdev in the
datapath-independent code. So for the OVS_ACTION_ATTR_HASH case in
odp_execute_actions() we need to know the odp_port or the netdev.
If we have the original_ingress_netdev, we can call
dpif_offload_netdev_get_dp_hash() unmodified. We could read
md.orig_in_port from the packet, but to translate that quickly to a
netdev we need the dpif, which is not part of the generic
odp_execute_actions() API.
So we are left with either passing the original_ingress_netdev or
the dpif to odp_execute_actions().
In v1 I avoided this by moving OVS_ACTION_ATTR_HASH into
requires_datapath_assistance() and changing odp_execute_cb to return
bool, with dpif_execute_helper_cb returning false for HASH to fall
back to software handling. However, that still has your objection of
introducing datapath-specific logic into generic code.
Passing ingress_netdev also does not work cleanly, as packets in a
batch may have different md.orig_in_port values and therefore require
different netdev lookups per packet. So I think the cleanest solution
is to pass the dpif to odp_execute_actions() and resolve the netdev
per packet from md.orig_in_port inside the action loop. Any thoughts,
or do you have another approach in mind?
>> +
>> static void
>> dp_execute_cb(void *aux_, struct dp_packet_batch *packets_,
>> const struct nlattr *a, bool should_steal)
>> @@ -8290,10 +8319,10 @@ dp_netdev_execute_actions(struct
>> dp_netdev_pmd_thread *pmd,
>> bool should_steal, const struct flow *flow,
>> const struct nlattr *actions, size_t actions_len)
>> {
>> - struct dp_netdev_execute_aux aux = { pmd, flow };
>> + struct dp_netdev_execute_aux aux = { pmd, flow, NULL, ODPP_NONE };
>>
>> odp_execute_actions(&aux, packets, should_steal, actions,
>> - actions_len, dp_execute_cb);
>> + actions_len, dp_execute_cb, dp_hash_override);
>> }
>>
>> struct dp_netdev_ct_dump {
>> diff --git a/lib/dpif-offload-dummy.c b/lib/dpif-offload-dummy.c
>> index cc9fc5792..01b20d3b6 100644
>> --- a/lib/dpif-offload-dummy.c
>> +++ b/lib/dpif-offload-dummy.c
>> @@ -640,6 +640,103 @@ dummy_offload_udp_tnl_get_src_port(
>> return true;
>> }
>>
>> +static uint32_t
>> +fnv1a_add(uint32_t hash, uint32_t word)
>> +{
>> + const uint32_t prime = 16777619U;
>> +
>> + for (size_t i = 0; i < sizeof(uint32_t); i++) {
>> + hash ^= (word >> (i * 8)) & 0xff;
>> + hash *= prime;
>> + }
>> + return hash;
>> +}
>> +
>> +static uint32_t
>> +fnv1a_add64(uint32_t hash, uint64_t word)
>> +{
>> + return fnv1a_add(fnv1a_add(hash, word), word >> 32);
>> +}
>> +
>> +static uint32_t
>> +fnv1a_init(uint32_t seed)
>> +{
>> + const uint32_t fnv_offset_basis = 2166136261U;
>> +
>> + return seed ? fnv1a_add(fnv_offset_basis, seed) : fnv_offset_basis;
>> +}
>> +
>> +static bool
>> +get_l4_sym_hash(struct flow *flow, uint32_t seed, uint32_t *hash_)
>> +{
>> + struct ds ds = DS_EMPTY_INITIALIZER;
>> + uint32_t hash = fnv1a_init(seed);
>> +
>> + if (flow->dl_type == htons(ETH_TYPE_IP)) {
>> + hash = fnv1a_add(hash,
>> + (OVS_FORCE uint32_t)(flow->nw_src ^ flow->nw_dst));
>> + } else if (flow->dl_type == htons(ETH_TYPE_IPV6)) {
>> + /* IPv6 addresses are 64-bit aligned inside struct flow. */
>> + const ovs_be64 *a = ALIGNED_CAST(const ovs_be64 *,
>> + flow->ipv6_src.s6_addr);
>> + const ovs_be64 *b = ALIGNED_CAST(const ovs_be64 *,
>> + flow->ipv6_dst.s6_addr);
>> +
>> + for (int i = 0; i < sizeof flow->ipv6_src / sizeof *a; i++) {
>> + hash = fnv1a_add64(hash, ntohll(a[i]) ^ ntohll(b[i]));
>> + }
>> + } else {
>> + return false;
>> + }
>> +
>> + hash = fnv1a_add(hash, flow->nw_proto);
>> + if (!(flow->nw_frag & FLOW_NW_FRAG_MASK)
>> + && (flow->nw_proto == IPPROTO_TCP
>> + || flow->nw_proto == IPPROTO_SCTP
>> + || flow->nw_proto == IPPROTO_UDP)) {
>> + hash = fnv1a_add(hash,
>> + (OVS_FORCE uint16_t)(flow->tp_src ^ flow->tp_dst));
>> + }
>> +
>> + ds_put_format(&ds, "l4_sym_hash: %8.8x for packet: ", hash);
>> + flow_format(&ds, flow, NULL);
>> + VLOG_DBG("%s", ds_cstr(&ds));
>> + ds_destroy(&ds);
>> +
>> + *hash_ = hash;
>> + return true;
>> +}
>
> I wonder if we need all this machinery for the hash computation? Can't we
> just return a fixed number? Or a simple sum of port numbers? This will be
> enough for most test scenarios, as we can ensure that we're setting the ports
> in those scenarios. It's a dummy implementation, it doesn't have to be good.
Fair point. It was fun to write, but I will simplify it in v2.
[...]
>> +AT_CHECK([ovs-appctl netdev-dummy/receive p1 $packet])
>> +AT_CHECK([ovs-appctl netdev-dummy/receive p1 $packet])
>> +
>> +AT_CHECK([ovs-appctl dpctl/dump-flows -m | strip_hw_offload], [0], [dnl
>
> Is -m needed here?
Yes, the -m flag is needed to include the offloaded:yes and dp:dummy
fields in the output.
[...]
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev