* [PATCH net-next v7 1/5] net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices [not found] <20260612083530.1650245-1-hawk@kernel.org> @ 2026-06-12 8:35 ` hawk 2026-06-12 8:35 ` [PATCH net-next v7 2/5] veth: implement Byte Queue Limits (BQL) for latency reduction hawk ` (3 subsequent siblings) 4 siblings, 0 replies; 9+ messages in thread From: hawk @ 2026-06-12 8:35 UTC (permalink / raw) To: netdev Cc: kernel-team, simon.schippers, Jesper Dangaard Brouer, Jonas Köppeler, Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan, Kuniyuki Iwashima, Stanislav Fomichev, Christian Brauner, Frederic Weisbecker, Yajun Deng, linux-doc, linux-kernel From: Jesper Dangaard Brouer <hawk@kernel.org> Virtual devices with IFF_NO_QUEUE or lltx are excluded from BQL sysfs by netdev_uses_bql(), since they traditionally lack real hardware queues. However, some virtual devices like veth implement a real ptr_ring FIFO with NAPI processing and benefit from BQL to limit in-flight bytes and reduce latency. Add a per-device 'bql' bitfield boolean in the priv_flags_slow section of struct net_device. When set, it overrides the IFF_NO_QUEUE/lltx exclusion and exposes BQL sysfs entries (/sys/class/net/<dev>/queues/ tx-<n>/byte_queue_limits/). The flag is still gated on CONFIG_BQL. This allows drivers that use BQL despite being IFF_NO_QUEUE to opt in to sysfs visibility for monitoring and debugging. Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org> Tested-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> --- Documentation/networking/net_cachelines/net_device.rst | 1 + include/linux/netdevice.h | 2 ++ net/core/net-sysfs.c | 8 +++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Documentation/networking/net_cachelines/net_device.rst b/Documentation/networking/net_cachelines/net_device.rst index eb2e6851c6f6..a65d48b6ecc1 100644 --- a/Documentation/networking/net_cachelines/net_device.rst +++ b/Documentation/networking/net_cachelines/net_device.rst @@ -169,6 +169,7 @@ unsigned_long:1 see_all_hwtstamp_requests unsigned_long:1 change_proto_down unsigned_long:1 netns_immutable unsigned_long:1 fcoe_mtu +unsigned_long:1 bql netdev_uses_bql(net-sysfs.c) struct list_head net_notifier_list struct macsec_ops* macsec_ops struct udp_tunnel_nic_info* udp_tunnel_nic_info diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h index 7f4f0837c09f..f699fded20b4 100644 --- a/include/linux/netdevice.h +++ b/include/linux/netdevice.h @@ -2079,6 +2079,7 @@ enum netdev_reg_state { * @change_proto_down: device supports setting carrier via IFLA_PROTO_DOWN * @netns_immutable: interface can't change network namespaces * @fcoe_mtu: device supports maximum FCoE MTU, 2158 bytes + * @bql: device uses BQL (DQL sysfs) despite having IFF_NO_QUEUE * * @net_notifier_list: List of per-net netdev notifier block * that follow this device when it is moved @@ -2495,6 +2496,7 @@ struct net_device { unsigned long change_proto_down:1; unsigned long netns_immutable:1; unsigned long fcoe_mtu:1; + unsigned long bql:1; struct list_head net_notifier_list; diff --git a/net/core/net-sysfs.c b/net/core/net-sysfs.c index 0e71c9ed41e8..3cb470b0f17d 100644 --- a/net/core/net-sysfs.c +++ b/net/core/net-sysfs.c @@ -1939,10 +1939,16 @@ static const struct kobj_type netdev_queue_ktype = { static bool netdev_uses_bql(const struct net_device *dev) { + if (!IS_ENABLED(CONFIG_BQL)) + return false; + + if (dev->bql) + return true; + if (dev->lltx || (dev->priv_flags & IFF_NO_QUEUE)) return false; - return IS_ENABLED(CONFIG_BQL); + return true; } static int netdev_queue_add_kobject(struct net_device *dev, int index) -- 2.43.0 ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net-next v7 2/5] veth: implement Byte Queue Limits (BQL) for latency reduction [not found] <20260612083530.1650245-1-hawk@kernel.org> 2026-06-12 8:35 ` [PATCH net-next v7 1/5] net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices hawk @ 2026-06-12 8:35 ` hawk 2026-06-12 8:35 ` [PATCH net-next v7 3/5] veth: add tx_timeout watchdog as BQL safety net hawk ` (2 subsequent siblings) 4 siblings, 0 replies; 9+ messages in thread From: hawk @ 2026-06-12 8:35 UTC (permalink / raw) To: netdev Cc: kernel-team, simon.schippers, Jesper Dangaard Brouer, Jonas Köppeler, Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Alexei Starovoitov, Daniel Borkmann, John Fastabend, Stanislav Fomichev, linux-kernel, bpf From: Jesper Dangaard Brouer <hawk@kernel.org> Commit dc82a33297fc ("veth: apply qdisc backpressure on full ptr_ring to reduce TX drops") gave qdiscs control over veth by returning NETDEV_TX_BUSY when the ptr_ring is full (DRV_XOFF). That commit noted a known limitation: the 256-entry ptr_ring sits in front of the qdisc as a dark buffer, adding base latency because the qdisc has no visibility into how many bytes are already queued there. Add BQL support so the qdisc gets feedback and can begin shaping traffic before the ring fills. In testing with fq_codel, BQL reduces ping RTT under UDP load from ~6.61ms to ~0.36ms (18x). Charge a fixed VETH_BQL_UNIT (1) per packet rather than skb->len, so the DQL limit tracks packets-in-flight. Unlike a physical NIC, veth has no link speed -- the ptr_ring drains at CPU speed and is packet-indexed, not byte-indexed, so bytes are not the natural unit. With byte-based charging, small packets sneak many more entries into the ring before STACK_XOFF fires, deepening the dark buffer under mixed-size workloads. Testing with a concurrent min-size packet flood shows 3.7x ping RTT degradation with skb->len charging versus no change with fixed-unit charging. Charge BQL inside veth_xdp_rx() under the ptr_ring producer_lock, after confirming the ring is not full. The charge must precede the produce because the NAPI consumer can run on another CPU and complete the SKB the instant it becomes visible in the ring. Doing both under the same lock avoids a pre-charge/undo pattern -- BQL is only charged when produce is guaranteed to succeed. BQL is enabled only when a real qdisc is attached (guarded by !qdisc_txq_has_no_queue), as HARD_TX_LOCK provides serialization for TXQ modification like dql_queued(). For lltx devices, like veth, this HARD_TX_LOCK serialization isn't provided. The ptr_ring producer_lock provides additional serialization that would allow BQL to work correctly even with noqueue, though that combination is not currently enabled, as the netstack will drop and warn. Track per-SKB BQL state via a VETH_BQL_FLAG pointer tag in the ptr_ring entry. This is necessary because the qdisc can be replaced live while SKBs are in-flight -- each SKB must carry the charge decision made at enqueue time rather than re-checking the peer's qdisc at completion. Complete per-SKB in veth_xdp_rcv() rather than in bulk, so STACK_XOFF clears promptly when producer and consumer run on different CPUs. BQL introduces a second independent queue-stop mechanism (STACK_XOFF) alongside the existing DRV_XOFF (ring full). Both must be clear for the queue to transmit. At teardown, veth_napi_del_range() drains the leftover ring entries after synchronize_net() -- once NAPI is gone and the producer has stopped charging BQL (it observes rq->napi == NULL). Rather than netdev_tx_reset_queue(), which calls dql_reset() and races with a concurrent producer, balance the DQL accounting by completing the outstanding charges via netdev_tx_completed_queue(). The peer txq is still woken to clear any DRV_XOFF a late veth_xmit() may have set. Clamp the loop to the peer's num_tx_queues, since the peer may have fewer TX queues than the local device has RX queues (e.g. veth enslaved to a bond with XDP attached). Co-developed-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> Signed-off-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org> --- drivers/net/veth.c | 131 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 120 insertions(+), 11 deletions(-) diff --git a/drivers/net/veth.c b/drivers/net/veth.c index 0cfb19b760dd..a3505627f49e 100644 --- a/drivers/net/veth.c +++ b/drivers/net/veth.c @@ -34,9 +34,13 @@ #define DRV_VERSION "1.0" #define VETH_XDP_FLAG BIT(0) +#define VETH_BQL_FLAG BIT(1) #define VETH_RING_SIZE 256 #define VETH_XDP_HEADROOM (XDP_PACKET_HEADROOM + NET_IP_ALIGN) +/* Fixed BQL charge: DQL limit tracks packets-in-flight, not bytes */ +#define VETH_BQL_UNIT 1 + #define VETH_XDP_TX_BULK_SIZE 16 #define VETH_XDP_BATCH 16 @@ -280,6 +284,21 @@ static bool veth_is_xdp_frame(void *ptr) return (unsigned long)ptr & VETH_XDP_FLAG; } +static bool veth_ptr_is_bql(void *ptr) +{ + return (unsigned long)ptr & VETH_BQL_FLAG; +} + +static struct sk_buff *veth_ptr_to_skb(void *ptr) +{ + return (void *)((unsigned long)ptr & ~VETH_BQL_FLAG); +} + +static void *veth_skb_to_ptr(struct sk_buff *skb, bool bql) +{ + return bql ? (void *)((unsigned long)skb | VETH_BQL_FLAG) : skb; +} + static struct xdp_frame *veth_ptr_to_xdp(void *ptr) { return (void *)((unsigned long)ptr & ~VETH_XDP_FLAG); @@ -295,7 +314,26 @@ static void veth_ptr_free(void *ptr) if (veth_is_xdp_frame(ptr)) xdp_return_frame(veth_ptr_to_xdp(ptr)); else - kfree_skb(ptr); + kfree_skb(veth_ptr_to_skb(ptr)); +} + +/* Drain frames left in the ptr_ring at teardown, freeing each one and + * returning the number of BQL-charged SKBs. The caller completes these + * via netdev_tx_completed_queue() to balance the DQL accounting, avoiding + * the racy netdev_tx_reset_queue()/dql_reset(). + */ +static unsigned int veth_ptr_ring_drain(struct ptr_ring *ring) +{ + unsigned int n_bql = 0; + void *ptr; + + while ((ptr = ptr_ring_consume(ring))) { + if (veth_ptr_is_bql(ptr)) + n_bql++; + veth_ptr_free(ptr); + } + + return n_bql; } static void __veth_xdp_flush(struct veth_rq *rq) @@ -309,19 +347,39 @@ static void __veth_xdp_flush(struct veth_rq *rq) } } -static int veth_xdp_rx(struct veth_rq *rq, struct sk_buff *skb) +static int veth_xdp_rx(struct veth_rq *rq, struct sk_buff *skb, bool do_bql, + struct netdev_queue *txq) { - if (unlikely(ptr_ring_produce(&rq->xdp_ring, skb))) + struct ptr_ring *ring = &rq->xdp_ring; + + spin_lock(&ring->producer_lock); + if (unlikely(__ptr_ring_check_produce(ring))) { + spin_unlock(&ring->producer_lock); return NETDEV_TX_BUSY; /* signal qdisc layer */ + } + + /* Charge BQL before produce; the consumer cannot see the entry yet. + * veth is lltx, so the stack skips HARD_TX_LOCK and txq->_xmit_lock + * does not serialise txq->dql here. This producer_lock is the single + * producer lock for dql_queued() (1:1 with this rq's peer txq), and + * the peer NAPI in veth_xdp_rcv() is the single completer -- the + * two-context model that dql_queued()/dql_completed() require. + */ + if (do_bql) + netdev_tx_sent_queue(txq, VETH_BQL_UNIT); + + __ptr_ring_produce(ring, veth_skb_to_ptr(skb, do_bql)); + spin_unlock(&ring->producer_lock); return NET_RX_SUCCESS; /* same as NETDEV_TX_OK */ } static int veth_forward_skb(struct net_device *dev, struct sk_buff *skb, - struct veth_rq *rq, bool xdp) + struct veth_rq *rq, bool xdp, bool do_bql, + struct netdev_queue *txq) { return __dev_forward_skb(dev, skb) ?: xdp ? - veth_xdp_rx(rq, skb) : + veth_xdp_rx(rq, skb, do_bql, txq) : __netif_rx(skb); } @@ -347,11 +405,12 @@ static bool veth_skb_is_eligible_for_gro(const struct net_device *dev, static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev) { struct veth_priv *rcv_priv, *priv = netdev_priv(dev); + struct netdev_queue *txq = NULL; struct veth_rq *rq = NULL; - struct netdev_queue *txq; struct net_device *rcv; int length = skb->len; bool use_napi = false; + bool do_bql = false; int ret, rxq; rcu_read_lock(); @@ -375,8 +434,12 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev) } skb_tx_timestamp(skb); - - ret = veth_forward_skb(rcv, skb, rq, use_napi); + if (rxq < dev->real_num_tx_queues) { + txq = netdev_get_tx_queue(dev, rxq); + /* BQL charge happens inside veth_xdp_rx() under producer_lock */ + do_bql = use_napi && !qdisc_txq_has_no_queue(txq); + } + ret = veth_forward_skb(rcv, skb, rq, use_napi, do_bql, txq); switch (ret) { case NET_RX_SUCCESS: /* same as NETDEV_TX_OK */ if (!use_napi) @@ -412,6 +475,7 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev) net_crit_ratelimited("%s(%s): Invalid return code(%d)", __func__, dev->name, ret); } + rcu_read_unlock(); return ret; @@ -900,7 +964,8 @@ static struct sk_buff *veth_xdp_rcv_skb(struct veth_rq *rq, static int veth_xdp_rcv(struct veth_rq *rq, int budget, struct veth_xdp_tx_bq *bq, - struct veth_stats *stats) + struct veth_stats *stats, + struct netdev_queue *peer_txq) { int i, done = 0, n_xdpf = 0; void *xdpf[VETH_XDP_BATCH]; @@ -928,9 +993,13 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, } } else { /* ndo_start_xmit */ - struct sk_buff *skb = ptr; + bool bql_charged = veth_ptr_is_bql(ptr); + struct sk_buff *skb = veth_ptr_to_skb(ptr); stats->xdp_bytes += skb->len; + if (peer_txq && bql_charged) + netdev_tx_completed_queue(peer_txq, 1, VETH_BQL_UNIT); + skb = veth_xdp_rcv_skb(rq, skb, bq, stats); if (skb) { if (skb_shared(skb) || skb_unclone(skb, GFP_ATOMIC)) @@ -976,7 +1045,7 @@ static int veth_poll(struct napi_struct *napi, int budget) netdev_get_tx_queue(peer_dev, queue_idx) : NULL; xdp_set_return_frame_no_direct(); - done = veth_xdp_rcv(rq, budget, &bq, &stats); + done = veth_xdp_rcv(rq, budget, &bq, &stats, peer_txq); if (stats.xdp_redirect > 0) xdp_do_flush(); @@ -1074,6 +1143,7 @@ static int __veth_napi_enable(struct net_device *dev) static void veth_napi_del_range(struct net_device *dev, int start, int end) { struct veth_priv *priv = netdev_priv(dev); + struct net_device *peer; int i; for (i = start; i < end; i++) { @@ -1085,11 +1155,49 @@ static void veth_napi_del_range(struct net_device *dev, int start, int end) } synchronize_net(); + /* This rq's frames were BQL-charged on the peer's txq[i]. */ + peer = rtnl_dereference(priv->peer); + for (i = start; i < end; i++) { struct veth_rq *rq = &priv->rq[i]; + struct netdev_queue *txq; + unsigned int n_bql; rq->rx_notify_masked = false; + + /* Drain leftover ring frames, counting BQL-charged SKBs that + * were charged via netdev_tx_sent_queue() but never consumed. + */ + n_bql = veth_ptr_ring_drain(&rq->xdp_ring); ptr_ring_cleanup(&rq->xdp_ring, veth_ptr_free); + + if (!peer || i >= peer->num_tx_queues) + continue; + + txq = netdev_get_tx_queue(peer, i); + + /* Balance the peer txq's DQL accounting by completing the + * outstanding charges instead of netdev_tx_reset_queue(): + * dql_reset() races with a concurrent producer, while + * netdev_tx_completed_queue() is the normal single-completer + * path and is safe here -- NAPI is gone (synchronize_net() + * above) and the producer stopped charging BQL once it + * observed rq->napi == NULL. Completing every charge drives + * DQL inflight to 0 and clears STACK_XOFF. + */ + if (n_bql) + netdev_tx_completed_queue(txq, n_bql, + n_bql * VETH_BQL_UNIT); + + /* DRV_XOFF is independent of BQL/STACK_XOFF: a concurrent + * veth_xmit() may have set it between rcu_assign_pointer(napi, + * NULL) and synchronize_net(); with NAPI gone nothing else + * clears it. The completion above only clears STACK_XOFF, so + * still wake the txq to clear DRV_XOFF -- but only when the + * device is still up. + */ + if (netif_running(dev)) + netif_tx_wake_queue(txq); } for (i = start; i < end; i++) { @@ -1741,6 +1849,7 @@ static void veth_setup(struct net_device *dev) dev->priv_flags |= IFF_PHONY_HEADROOM; dev->priv_flags |= IFF_DISABLE_NETPOLL; dev->lltx = true; + dev->bql = true; dev->netdev_ops = &veth_netdev_ops; dev->xdp_metadata_ops = &veth_xdp_metadata_ops; -- 2.43.0 ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net-next v7 3/5] veth: add tx_timeout watchdog as BQL safety net [not found] <20260612083530.1650245-1-hawk@kernel.org> 2026-06-12 8:35 ` [PATCH net-next v7 1/5] net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices hawk 2026-06-12 8:35 ` [PATCH net-next v7 2/5] veth: implement Byte Queue Limits (BQL) for latency reduction hawk @ 2026-06-12 8:35 ` hawk 2026-06-12 8:35 ` [PATCH net-next v7 4/5] net: sched: add timeout count to NETDEV WATCHDOG message hawk 2026-06-12 8:35 ` [PATCH net-next v7 5/5] veth: time-based BQL completion coalescing via ethtool tx-usecs hawk 4 siblings, 0 replies; 9+ messages in thread From: hawk @ 2026-06-12 8:35 UTC (permalink / raw) To: netdev Cc: kernel-team, simon.schippers, Jesper Dangaard Brouer, Jonas Köppeler, Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-kernel From: Jesper Dangaard Brouer <hawk@kernel.org> With the introduction of BQL (Byte Queue Limits) for veth, there are now two independent mechanisms that can stop a transmit queue: - DRV_XOFF: set by netif_tx_stop_queue() when the ptr_ring is full - STACK_XOFF: set by BQL when the byte-in-flight limit is reached If either mechanism stalls without a corresponding wake/completion, the queue stops permanently. Enable the net device watchdog timer and implement ndo_tx_timeout as a failsafe recovery. The timeout handler resets BQL state (clearing STACK_XOFF) and wakes the queue (clearing DRV_XOFF), covering both stop mechanisms. The watchdog fires after 16 seconds, which accommodates worst-case NAPI processing (budget=64 packets x 250ms per-packet consumer delay) without false positives under normal backpressure. Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org> Tested-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> --- drivers/net/veth.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/drivers/net/veth.c b/drivers/net/veth.c index a3505627f49e..2473f730734b 100644 --- a/drivers/net/veth.c +++ b/drivers/net/veth.c @@ -44,6 +44,13 @@ #define VETH_XDP_TX_BULK_SIZE 16 #define VETH_XDP_BATCH 16 +/* tx_timeout watchdog timeout. DRV_XOFF is only cleared at the end of a NAPI + * veth_poll() (netif_tx_wake_queue()), so the timeout must outlast a full + * worst-case poll: a 64-packet budget with a pessimistic 250 ms/pkt consumer + * delay => 64 * 250 ms = 16 s. + */ +#define VETH_WATCHDOG_TIMEOUT_MS (64 * 250) + struct veth_stats { u64 rx_drops; /* xdp */ @@ -1487,6 +1494,22 @@ static int veth_set_channels(struct net_device *dev, goto out; } +static void veth_tx_timeout(struct net_device *dev, unsigned int txqueue) +{ + struct netdev_queue *txq = netdev_get_tx_queue(dev, txqueue); + + netdev_err(dev, + "veth backpressure(0x%lX) stalled(n:%ld) TXQ(%u) re-enable\n", + txq->state, atomic_long_read(&txq->trans_timeout), txqueue); + + /* Cannot call netdev_tx_reset_queue(): dql_reset() races with + * peer NAPI calling dql_completed() concurrently. + * Just clear the stop bits; the qdisc will re-stop if still stuck. + */ + clear_bit(__QUEUE_STATE_STACK_XOFF, &txq->state); + netif_tx_wake_queue(txq); +} + static int veth_open(struct net_device *dev) { struct veth_priv *priv = netdev_priv(dev); @@ -1825,6 +1848,7 @@ static const struct net_device_ops veth_netdev_ops = { .ndo_bpf = veth_xdp, .ndo_xdp_xmit = veth_ndo_xdp_xmit, .ndo_get_peer_dev = veth_peer_dev, + .ndo_tx_timeout = veth_tx_timeout, }; static const struct xdp_metadata_ops veth_xdp_metadata_ops = { @@ -1864,6 +1888,7 @@ static void veth_setup(struct net_device *dev) dev->priv_destructor = veth_dev_free; dev->pcpu_stat_type = NETDEV_PCPU_STAT_TSTATS; dev->max_mtu = ETH_MAX_MTU; + dev->watchdog_timeo = msecs_to_jiffies(VETH_WATCHDOG_TIMEOUT_MS); dev->hw_features = VETH_FEATURES; dev->hw_enc_features = VETH_FEATURES; -- 2.43.0 ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net-next v7 4/5] net: sched: add timeout count to NETDEV WATCHDOG message [not found] <20260612083530.1650245-1-hawk@kernel.org> ` (2 preceding siblings ...) 2026-06-12 8:35 ` [PATCH net-next v7 3/5] veth: add tx_timeout watchdog as BQL safety net hawk @ 2026-06-12 8:35 ` hawk 2026-06-12 8:35 ` [PATCH net-next v7 5/5] veth: time-based BQL completion coalescing via ethtool tx-usecs hawk 4 siblings, 0 replies; 9+ messages in thread From: hawk @ 2026-06-12 8:35 UTC (permalink / raw) To: netdev Cc: kernel-team, simon.schippers, Jesper Dangaard Brouer, Jakub Kicinski, Jonas Köppeler, Jamal Hadi Salim, Jiri Pirko, David S. Miller, Eric Dumazet, Paolo Abeni, Simon Horman, linux-kernel From: Jesper Dangaard Brouer <hawk@kernel.org> Add the per-queue timeout counter (trans_timeout) to the core NETDEV WATCHDOG log message. This makes it easy to determine how frequently a particular queue is stalling from a single log line, without having to search through and correlate spaced-out log entries. Useful for production monitoring where timeouts are spaced by the watchdog interval, making frequency hard to judge. Suggested-by: Jakub Kicinski <kuba@kernel.org> Link: https://lore.kernel.org/all/20251107175445.58eba452@kernel.org/ Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org> Tested-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> --- net/sched/sch_generic.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/net/sched/sch_generic.c b/net/sched/sch_generic.c index 237ee1cd0136..eb6066d1ed90 100644 --- a/net/sched/sch_generic.c +++ b/net/sched/sch_generic.c @@ -533,6 +533,7 @@ static void dev_watchdog(struct timer_list *t) netif_running(dev) && netif_carrier_ok(dev)) { unsigned int timedout_ms = 0; + unsigned long trans_timeout = 0; unsigned int i; unsigned long trans_start; unsigned long oldest_start = jiffies; @@ -553,6 +554,7 @@ static void dev_watchdog(struct timer_list *t) if (time_after(jiffies, trans_start + dev->watchdog_timeo)) { timedout_ms = jiffies_to_msecs(jiffies - trans_start); atomic_long_inc(&txq->trans_timeout); + trans_timeout = atomic_long_read(&txq->trans_timeout); break; } if (time_after(oldest_start, trans_start)) @@ -561,9 +563,9 @@ static void dev_watchdog(struct timer_list *t) if (unlikely(timedout_ms)) { trace_net_dev_xmit_timeout(dev, i); - netdev_crit(dev, "NETDEV WATCHDOG: CPU: %d: transmit queue %u timed out %u ms\n", + netdev_crit(dev, "NETDEV WATCHDOG: CPU: %d: transmit queue %u timed out %u ms (n:%ld)\n", raw_smp_processor_id(), - i, timedout_ms); + i, timedout_ms, trans_timeout); netif_freeze_queues(dev); dev->netdev_ops->ndo_tx_timeout(dev, i); netif_unfreeze_queues(dev); -- 2.43.0 ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net-next v7 5/5] veth: time-based BQL completion coalescing via ethtool tx-usecs [not found] <20260612083530.1650245-1-hawk@kernel.org> ` (3 preceding siblings ...) 2026-06-12 8:35 ` [PATCH net-next v7 4/5] net: sched: add timeout count to NETDEV WATCHDOG message hawk @ 2026-06-12 8:35 ` hawk 2026-06-13 14:14 ` Simon Schippers 4 siblings, 1 reply; 9+ messages in thread From: hawk @ 2026-06-12 8:35 UTC (permalink / raw) To: netdev Cc: kernel-team, simon.schippers, Jesper Dangaard Brouer, Jonas Köppeler, Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Alexei Starovoitov, Daniel Borkmann, John Fastabend, Stanislav Fomichev, linux-kernel, bpf From: Simon Schippers <simon.schippers@tu-dortmund.de> Per-packet BQL completion forces DQL to converge on limit=2, causing excessive NAPI scheduling overhead and qdisc requeues. Accumulate BQL completions and flush them when a configurable time threshold (tx-usecs) is exceeded, letting DQL discover a limit that bounds actual queuing delay to the configured interval. Coalescing state persists across NAPI polls in struct veth_rq so completions can accumulate beyond a single budget=64 cycle. The flush condition is: state->time + bql_flush_ns <= current_time || state->n_bql > dql.limit Flushing when n_bql exceeds dql.limit handles BQL starvation. The comparison is strictly greater-than because netdev_tx_sent_queue() always lets the producer exceed the limit by one before it stops, so n_bql == dql.limit is a normal in-flight state. dql.limit lives in the same cacheline as the completion path, so the check is cheap. Add ethtool tx-usecs support for runtime tuning. Default is 100 us; setting tx-usecs to 0 disables coalescing and falls back to per-packet completion. ethtool -C <veth-dev> tx-usecs 500 # 500us coalescing ethtool -C <veth-dev> tx-usecs 0 # per-packet (no coalescing) Co-developed-by: Jesper Dangaard Brouer <hawk@kernel.org> Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org> Co-developed-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> Signed-off-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de> --- drivers/net/veth.c | 123 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 6 deletions(-) diff --git a/drivers/net/veth.c b/drivers/net/veth.c index 2473f730734b..c62d87a8402c 100644 --- a/drivers/net/veth.c +++ b/drivers/net/veth.c @@ -28,6 +28,7 @@ #include <linux/bpf_trace.h> #include <linux/net_tstamp.h> #include <linux/skbuff_ref.h> +#include <linux/sched/clock.h> #include <net/page_pool/helpers.h> #define DRV_NAME "veth" @@ -50,6 +51,7 @@ * delay => 64 * 250 ms = 16 s. */ #define VETH_WATCHDOG_TIMEOUT_MS (64 * 250) +#define VETH_BQL_COAL_TX_USECS 100 /* default tx-usecs for BQL batching */ struct veth_stats { u64 rx_drops; @@ -69,6 +71,11 @@ struct veth_rq_stats { struct u64_stats_sync syncp; }; +struct veth_bql_state { + u64 time; /* sched_clock() when current coalescing window started */ + uint n_bql; /* BQL completions batched in the current window */ +}; + struct veth_rq { struct napi_struct xdp_napi; struct napi_struct __rcu *napi; /* points to xdp_napi when the latter is initialized */ @@ -76,6 +83,7 @@ struct veth_rq { struct bpf_prog __rcu *xdp_prog; struct xdp_mem_info xdp_mem; struct veth_rq_stats stats; + struct veth_bql_state bql_state; bool rx_notify_masked; struct ptr_ring xdp_ring; struct xdp_rxq_info xdp_rxq; @@ -88,6 +96,7 @@ struct veth_priv { struct bpf_prog *_xdp_prog; struct veth_rq *rq; unsigned int requested_headroom; + unsigned int tx_coal_usecs; /* BQL completion coalescing */ }; struct veth_xdp_tx_bq { @@ -272,7 +281,56 @@ static void veth_get_channels(struct net_device *dev, static int veth_set_channels(struct net_device *dev, struct ethtool_channels *ch); +static int veth_get_coalesce(struct net_device *dev, + struct ethtool_coalesce *ec, + struct kernel_ethtool_coalesce *kernel_coal, + struct netlink_ext_ack *extack) +{ + struct veth_priv *priv = netdev_priv(dev); + + ec->tx_coalesce_usecs = priv->tx_coal_usecs; + return 0; +} + +static int veth_set_coalesce(struct net_device *dev, + struct ethtool_coalesce *ec, + struct kernel_ethtool_coalesce *kernel_coal, + struct netlink_ext_ack *extack) +{ + struct veth_priv *priv = netdev_priv(dev); + struct net_device *peer; + + /* The coalescing window delays BQL completions, so keep tx-usecs well + * below the tx_timeout watchdog; otherwise a large value could stall a + * stopped queue long enough to trip a false watchdog timeout. Cap at + * half the watchdog to leave a generous safety margin. tx-usecs is + * microseconds, the watchdog is milliseconds. + */ + if (ec->tx_coalesce_usecs > VETH_WATCHDOG_TIMEOUT_MS / 2 * USEC_PER_MSEC) { + NL_SET_ERR_MSG_MOD(extack, + "tx-usecs must stay below half the tx_timeout watchdog"); + return -ERANGE; + } + + /* Paired with READ_ONCE in veth_xdp_rcv(). */ + WRITE_ONCE(priv->tx_coal_usecs, ec->tx_coalesce_usecs); + + /* veth_xdp_rcv() reads each device's own value, so mirror it onto + * the peer to keep the pair symmetric: both directions coalesce + * with the same tx-usecs. Called under RTNL, rtnl_dereference() is safe. + */ + peer = rtnl_dereference(priv->peer); + if (peer) { + struct veth_priv *peer_priv = netdev_priv(peer); + + WRITE_ONCE(peer_priv->tx_coal_usecs, ec->tx_coalesce_usecs); + } + + return 0; +} + static const struct ethtool_ops veth_ethtool_ops = { + .supported_coalesce_params = ETHTOOL_COALESCE_TX_USECS, .get_drvinfo = veth_get_drvinfo, .get_link = ethtool_op_get_link, .get_strings = veth_get_strings, @@ -282,6 +340,8 @@ static const struct ethtool_ops veth_ethtool_ops = { .get_ts_info = ethtool_op_get_ts_info, .get_channels = veth_get_channels, .set_channels = veth_set_channels, + .get_coalesce = veth_get_coalesce, + .set_coalesce = veth_set_coalesce, }; /* general routines */ @@ -969,13 +1029,54 @@ static struct sk_buff *veth_xdp_rcv_skb(struct veth_rq *rq, return NULL; } +static void veth_bql_maybe_complete(struct veth_bql_state *state, + struct netdev_queue *peer_txq, + u64 bql_flush_ns) +{ + u64 current_time; + + /* There is no reason to complete with 0 and + * peer_txq could go away. + */ + if (!state->n_bql || !peer_txq) + return; + + current_time = sched_clock(); + + /* We complete if: + * 1. We reach bql_flush_ns. + * 2. We potentially have BQL starvation. + */ + if (state->time + bql_flush_ns <= current_time || + state->n_bql > peer_txq->dql.limit) { + netdev_tx_completed_queue(peer_txq, state->n_bql, + state->n_bql * VETH_BQL_UNIT); + state->time = current_time; + state->n_bql = 0; + } +} + static int veth_xdp_rcv(struct veth_rq *rq, int budget, struct veth_xdp_tx_bq *bq, struct veth_stats *stats, struct netdev_queue *peer_txq) { + struct veth_priv *priv = netdev_priv(rq->dev); + struct veth_bql_state *state = &rq->bql_state; int i, done = 0, n_xdpf = 0; void *xdpf[VETH_XDP_BATCH]; + u64 bql_flush_ns; + + /* Mirrored to both peers; paired with WRITE_ONCE() in veth_set_coalesce */ + bql_flush_ns = (u64)READ_ONCE(priv->tx_coal_usecs) * 1000; + + /* Clamp stored timestamp in case we migrated to a CPU with a behind + * sched_clock(); tries to reduce late BQL flushes. + */ + state->time = min(state->time, sched_clock()); + + /* Flush completions that timed out since the previous NAPI poll. */ + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); for (i = 0; i < budget; i++) { void *ptr = __ptr_ring_consume(&rq->xdp_ring); @@ -1000,12 +1101,11 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, } } else { /* ndo_start_xmit */ - bool bql_charged = veth_ptr_is_bql(ptr); struct sk_buff *skb = veth_ptr_to_skb(ptr); + if (veth_ptr_is_bql(ptr)) + state->n_bql++; stats->xdp_bytes += skb->len; - if (peer_txq && bql_charged) - netdev_tx_completed_queue(peer_txq, 1, VETH_BQL_UNIT); skb = veth_xdp_rcv_skb(rq, skb, bq, stats); if (skb) { @@ -1015,6 +1115,7 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, napi_gro_receive(&rq->xdp_napi, skb); } } + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); done++; } @@ -1123,6 +1224,9 @@ static int __veth_napi_enable_range(struct net_device *dev, int start, int end) for (i = start; i < end; i++) { struct veth_rq *rq = &priv->rq[i]; + rq->bql_state.time = sched_clock(); + rq->bql_state.n_bql = 0; + napi_enable(&rq->xdp_napi); rcu_assign_pointer(priv->rq[i].napi, &priv->rq[i].xdp_napi); } @@ -1172,11 +1276,15 @@ static void veth_napi_del_range(struct net_device *dev, int start, int end) rq->rx_notify_masked = false; - /* Drain leftover ring frames, counting BQL-charged SKBs that - * were charged via netdev_tx_sent_queue() but never consumed. + /* Drain leftover ring frames, counting BQL-charged SKBs, and + * add the completions still pending in the coalescing window + * (consumed by NAPI but not yet flushed). Both were charged + * via netdev_tx_sent_queue() and are still outstanding. */ - n_bql = veth_ptr_ring_drain(&rq->xdp_ring); + n_bql = veth_ptr_ring_drain(&rq->xdp_ring) + rq->bql_state.n_bql; ptr_ring_cleanup(&rq->xdp_ring, veth_ptr_free); + rq->bql_state.n_bql = 0; + rq->bql_state.time = 0; if (!peer || i >= peer->num_tx_queues) continue; @@ -1865,6 +1973,8 @@ static const struct xdp_metadata_ops veth_xdp_metadata_ops = { static void veth_setup(struct net_device *dev) { + struct veth_priv *priv = netdev_priv(dev); + ether_setup(dev); dev->priv_flags &= ~IFF_TX_SKB_SHARING; @@ -1889,6 +1999,7 @@ static void veth_setup(struct net_device *dev) dev->pcpu_stat_type = NETDEV_PCPU_STAT_TSTATS; dev->max_mtu = ETH_MAX_MTU; dev->watchdog_timeo = msecs_to_jiffies(VETH_WATCHDOG_TIMEOUT_MS); + priv->tx_coal_usecs = VETH_BQL_COAL_TX_USECS; dev->hw_features = VETH_FEATURES; dev->hw_enc_features = VETH_FEATURES; -- 2.43.0 ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v7 5/5] veth: time-based BQL completion coalescing via ethtool tx-usecs 2026-06-12 8:35 ` [PATCH net-next v7 5/5] veth: time-based BQL completion coalescing via ethtool tx-usecs hawk @ 2026-06-13 14:14 ` Simon Schippers 2026-06-30 14:00 ` Jonas Köppeler 0 siblings, 1 reply; 9+ messages in thread From: Simon Schippers @ 2026-06-13 14:14 UTC (permalink / raw) To: hawk, netdev Cc: kernel-team, Jonas Köppeler, Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Alexei Starovoitov, Daniel Borkmann, John Fastabend, Stanislav Fomichev, linux-kernel, bpf On 6/12/26 10:35, hawk@kernel.org wrote: > From: Simon Schippers <simon.schippers@tu-dortmund.de> > > Per-packet BQL completion forces DQL to converge on limit=2, causing > excessive NAPI scheduling overhead and qdisc requeues. > > Accumulate BQL completions and flush them when a configurable time > threshold (tx-usecs) is exceeded, letting DQL discover a limit that > bounds actual queuing delay to the configured interval. Coalescing > state persists across NAPI polls in struct veth_rq so completions can > accumulate beyond a single budget=64 cycle. > > The flush condition is: > > state->time + bql_flush_ns <= current_time || state->n_bql > dql.limit > > Flushing when n_bql exceeds dql.limit handles BQL starvation. > > The comparison is strictly greater-than because netdev_tx_sent_queue() > always lets the producer exceed the limit by one before it stops, so > n_bql == dql.limit is a normal in-flight state. dql.limit lives in > the same cacheline as the completion path, so the check is cheap. > > Add ethtool tx-usecs support for runtime tuning. Default is 100 us; > setting tx-usecs to 0 disables coalescing and falls back to per-packet > completion. > > ethtool -C <veth-dev> tx-usecs 500 # 500us coalescing > ethtool -C <veth-dev> tx-usecs 0 # per-packet (no coalescing) > > Co-developed-by: Jesper Dangaard Brouer <hawk@kernel.org> > Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org> > Co-developed-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> > Signed-off-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> > Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de> > --- > drivers/net/veth.c | 123 ++++++++++++++++++++++++++++++++++++++++++--- > 1 file changed, 117 insertions(+), 6 deletions(-) > > diff --git a/drivers/net/veth.c b/drivers/net/veth.c > index 2473f730734b..c62d87a8402c 100644 > --- a/drivers/net/veth.c > +++ b/drivers/net/veth.c > @@ -28,6 +28,7 @@ > #include <linux/bpf_trace.h> > #include <linux/net_tstamp.h> > #include <linux/skbuff_ref.h> > +#include <linux/sched/clock.h> > #include <net/page_pool/helpers.h> > > #define DRV_NAME "veth" > @@ -50,6 +51,7 @@ > * delay => 64 * 250 ms = 16 s. > */ > #define VETH_WATCHDOG_TIMEOUT_MS (64 * 250) > +#define VETH_BQL_COAL_TX_USECS 100 /* default tx-usecs for BQL batching */ > > struct veth_stats { > u64 rx_drops; > @@ -69,6 +71,11 @@ struct veth_rq_stats { > struct u64_stats_sync syncp; > }; > > +struct veth_bql_state { > + u64 time; /* sched_clock() when current coalescing window started */ > + uint n_bql; /* BQL completions batched in the current window */ > +}; > + > struct veth_rq { > struct napi_struct xdp_napi; > struct napi_struct __rcu *napi; /* points to xdp_napi when the latter is initialized */ > @@ -76,6 +83,7 @@ struct veth_rq { > struct bpf_prog __rcu *xdp_prog; > struct xdp_mem_info xdp_mem; > struct veth_rq_stats stats; > + struct veth_bql_state bql_state; > bool rx_notify_masked; > struct ptr_ring xdp_ring; > struct xdp_rxq_info xdp_rxq; > @@ -88,6 +96,7 @@ struct veth_priv { > struct bpf_prog *_xdp_prog; > struct veth_rq *rq; > unsigned int requested_headroom; > + unsigned int tx_coal_usecs; /* BQL completion coalescing */ > }; > > struct veth_xdp_tx_bq { > @@ -272,7 +281,56 @@ static void veth_get_channels(struct net_device *dev, > static int veth_set_channels(struct net_device *dev, > struct ethtool_channels *ch); > > +static int veth_get_coalesce(struct net_device *dev, > + struct ethtool_coalesce *ec, > + struct kernel_ethtool_coalesce *kernel_coal, > + struct netlink_ext_ack *extack) > +{ > + struct veth_priv *priv = netdev_priv(dev); > + > + ec->tx_coalesce_usecs = priv->tx_coal_usecs; > + return 0; > +} > + > +static int veth_set_coalesce(struct net_device *dev, > + struct ethtool_coalesce *ec, > + struct kernel_ethtool_coalesce *kernel_coal, > + struct netlink_ext_ack *extack) > +{ > + struct veth_priv *priv = netdev_priv(dev); > + struct net_device *peer; > + > + /* The coalescing window delays BQL completions, so keep tx-usecs well > + * below the tx_timeout watchdog; otherwise a large value could stall a > + * stopped queue long enough to trip a false watchdog timeout. Cap at > + * half the watchdog to leave a generous safety margin. tx-usecs is > + * microseconds, the watchdog is milliseconds. > + */ > + if (ec->tx_coalesce_usecs > VETH_WATCHDOG_TIMEOUT_MS / 2 * USEC_PER_MSEC) { > + NL_SET_ERR_MSG_MOD(extack, > + "tx-usecs must stay below half the tx_timeout watchdog"); > + return -ERANGE; > + } > + > + /* Paired with READ_ONCE in veth_xdp_rcv(). */ > + WRITE_ONCE(priv->tx_coal_usecs, ec->tx_coalesce_usecs); > + > + /* veth_xdp_rcv() reads each device's own value, so mirror it onto > + * the peer to keep the pair symmetric: both directions coalesce > + * with the same tx-usecs. Called under RTNL, rtnl_dereference() is safe. > + */ > + peer = rtnl_dereference(priv->peer); > + if (peer) { > + struct veth_priv *peer_priv = netdev_priv(peer); > + > + WRITE_ONCE(peer_priv->tx_coal_usecs, ec->tx_coalesce_usecs); > + } > + > + return 0; > +} > + > static const struct ethtool_ops veth_ethtool_ops = { > + .supported_coalesce_params = ETHTOOL_COALESCE_TX_USECS, > .get_drvinfo = veth_get_drvinfo, > .get_link = ethtool_op_get_link, > .get_strings = veth_get_strings, > @@ -282,6 +340,8 @@ static const struct ethtool_ops veth_ethtool_ops = { > .get_ts_info = ethtool_op_get_ts_info, > .get_channels = veth_get_channels, > .set_channels = veth_set_channels, > + .get_coalesce = veth_get_coalesce, > + .set_coalesce = veth_set_coalesce, > }; > > /* general routines */ > @@ -969,13 +1029,54 @@ static struct sk_buff *veth_xdp_rcv_skb(struct veth_rq *rq, > return NULL; > } > > +static void veth_bql_maybe_complete(struct veth_bql_state *state, > + struct netdev_queue *peer_txq, > + u64 bql_flush_ns) > +{ > + u64 current_time; > + > + /* There is no reason to complete with 0 and > + * peer_txq could go away. > + */ > + if (!state->n_bql || !peer_txq) > + return; > + > + current_time = sched_clock(); > + > + /* We complete if: > + * 1. We reach bql_flush_ns. > + * 2. We potentially have BQL starvation. > + */ > + if (state->time + bql_flush_ns <= current_time || > + state->n_bql > peer_txq->dql.limit) { Both Sashiko-Nipa and Sashiko-Gemini are right, this is missing a #ifdef CONFIG_BQL. Not sure what is the best way to add them. And for the struct we could maybe do: #ifdef CONFIG_BQL struct veth_bql_state { u64 time; /* sched_clock() when current coalescing window started */ uint n_bql; /* BQL completions batched in the current window */ }; #else struct veth_bql_state {}; #endif > + netdev_tx_completed_queue(peer_txq, state->n_bql, > + state->n_bql * VETH_BQL_UNIT); > + state->time = current_time; > + state->n_bql = 0; > + } > +} > + > static int veth_xdp_rcv(struct veth_rq *rq, int budget, > struct veth_xdp_tx_bq *bq, > struct veth_stats *stats, > struct netdev_queue *peer_txq) > { > + struct veth_priv *priv = netdev_priv(rq->dev); > + struct veth_bql_state *state = &rq->bql_state; > int i, done = 0, n_xdpf = 0; > void *xdpf[VETH_XDP_BATCH]; > + u64 bql_flush_ns; > + > + /* Mirrored to both peers; paired with WRITE_ONCE() in veth_set_coalesce */ > + bql_flush_ns = (u64)READ_ONCE(priv->tx_coal_usecs) * 1000; > + > + /* Clamp stored timestamp in case we migrated to a CPU with a behind > + * sched_clock(); tries to reduce late BQL flushes. > + */ > + state->time = min(state->time, sched_clock()); > + > + /* Flush completions that timed out since the previous NAPI poll. */ > + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); > > for (i = 0; i < budget; i++) { > void *ptr = __ptr_ring_consume(&rq->xdp_ring); > @@ -1000,12 +1101,11 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, > } > } else { > /* ndo_start_xmit */ > - bool bql_charged = veth_ptr_is_bql(ptr); > struct sk_buff *skb = veth_ptr_to_skb(ptr); > > + if (veth_ptr_is_bql(ptr)) > + state->n_bql++; > stats->xdp_bytes += skb->len; > - if (peer_txq && bql_charged) > - netdev_tx_completed_queue(peer_txq, 1, VETH_BQL_UNIT); > > skb = veth_xdp_rcv_skb(rq, skb, bq, stats); > if (skb) { > @@ -1015,6 +1115,7 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, > napi_gro_receive(&rq->xdp_napi, skb); > } > } > + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); > done++; Sashiko-Nipa reports: "If veth_xdp_rcv() finishes and returns a done count less than the budget, NAPI will go to sleep in veth_poll(). Do we need to unconditionally flush any stranded BQL completions in veth_poll() before sleeping? If completions are left in rq->bql_state indefinitely across NAPI idle periods, it might present an artificially massive delay to DQL. This could cause DQL to mistakenly conclude the hardware is extremely slow and aggressively shrink dql.limit to its minimum, crippling throughput on subsequent bursts." Again the issue that I found to be non-problematic in [1] and can be seen by an BQL inflight > 0 when for example pktgen suddenly stops. If we would "unconditionally flush any stranded BQL completions in veth_poll() before sleeping" we would *not* accumulate BQL completions across NAPI polls but we want to do that. Do you agree? [1] https://lore.kernel.org/netdev/c8650d3a-e488-4279-b28f-549d766c23a1@tu-dortmund.de/ ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v7 5/5] veth: time-based BQL completion coalescing via ethtool tx-usecs 2026-06-13 14:14 ` Simon Schippers @ 2026-06-30 14:00 ` Jonas Köppeler 2026-06-30 19:07 ` Simon Schippers 0 siblings, 1 reply; 9+ messages in thread From: Jonas Köppeler @ 2026-06-30 14:00 UTC (permalink / raw) To: Simon Schippers, hawk, netdev Cc: kernel-team, Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Alexei Starovoitov, Daniel Borkmann, John Fastabend, Stanislav Fomichev, linux-kernel, bpf On 6/13/26 4:14 PM, Simon Schippers wrote: > On 6/12/26 10:35, hawk@kernel.org wrote: >> From: Simon Schippers <simon.schippers@tu-dortmund.de> >> >> Per-packet BQL completion forces DQL to converge on limit=2, causing >> excessive NAPI scheduling overhead and qdisc requeues. >> >> Accumulate BQL completions and flush them when a configurable time >> threshold (tx-usecs) is exceeded, letting DQL discover a limit that >> bounds actual queuing delay to the configured interval. Coalescing >> state persists across NAPI polls in struct veth_rq so completions can >> accumulate beyond a single budget=64 cycle. >> >> The flush condition is: >> >> state->time + bql_flush_ns <= current_time || state->n_bql > dql.limit >> >> Flushing when n_bql exceeds dql.limit handles BQL starvation. >> >> The comparison is strictly greater-than because netdev_tx_sent_queue() >> always lets the producer exceed the limit by one before it stops, so >> n_bql == dql.limit is a normal in-flight state. dql.limit lives in >> the same cacheline as the completion path, so the check is cheap. >> >> Add ethtool tx-usecs support for runtime tuning. Default is 100 us; >> setting tx-usecs to 0 disables coalescing and falls back to per-packet >> completion. >> >> ethtool -C <veth-dev> tx-usecs 500 # 500us coalescing >> ethtool -C <veth-dev> tx-usecs 0 # per-packet (no coalescing) >> >> Co-developed-by: Jesper Dangaard Brouer <hawk@kernel.org> >> Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org> >> Co-developed-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> >> Signed-off-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> >> Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de> >> --- >> drivers/net/veth.c | 123 ++++++++++++++++++++++++++++++++++++++++++--- >> 1 file changed, 117 insertions(+), 6 deletions(-) >> >> diff --git a/drivers/net/veth.c b/drivers/net/veth.c >> index 2473f730734b..c62d87a8402c 100644 >> --- a/drivers/net/veth.c >> +++ b/drivers/net/veth.c >> @@ -28,6 +28,7 @@ >> #include <linux/bpf_trace.h> >> #include <linux/net_tstamp.h> >> #include <linux/skbuff_ref.h> >> +#include <linux/sched/clock.h> >> #include <net/page_pool/helpers.h> >> >> #define DRV_NAME "veth" >> @@ -50,6 +51,7 @@ >> * delay => 64 * 250 ms = 16 s. >> */ >> #define VETH_WATCHDOG_TIMEOUT_MS (64 * 250) >> +#define VETH_BQL_COAL_TX_USECS 100 /* default tx-usecs for BQL batching*/ >> >> struct veth_stats { >> u64 rx_drops; >> @@ -69,6 +71,11 @@ struct veth_rq_stats { >> struct u64_stats_sync syncp; >> }; >> >> +struct veth_bql_state { >> + u64 time; /* sched_clock() when current coalescing window started */ >> + uint n_bql; /* BQL completions batched in the current window */ >> +}; >> + >> struct veth_rq { >> struct napi_struct xdp_napi; >> struct napi_struct __rcu *napi; /* points to xdp_napi when the latteris initialized */ >> @@ -76,6 +83,7 @@ struct veth_rq { >> struct bpf_prog __rcu *xdp_prog; >> struct xdp_mem_info xdp_mem; >> struct veth_rq_stats stats; >> + struct veth_bql_state bql_state; >> bool rx_notify_masked; >> struct ptr_ring xdp_ring; >> struct xdp_rxq_info xdp_rxq; >> @@ -88,6 +96,7 @@ struct veth_priv { >> struct bpf_prog *_xdp_prog; >> struct veth_rq *rq; >> unsigned int requested_headroom; >> + unsigned int tx_coal_usecs; /* BQL completion coalescing */ >> }; >> >> struct veth_xdp_tx_bq { >> @@ -272,7 +281,56 @@ static void veth_get_channels(struct net_device *dev, >> static int veth_set_channels(struct net_device *dev, >> struct ethtool_channels *ch); >> >> +static int veth_get_coalesce(struct net_device *dev, >> + struct ethtool_coalesce *ec, >> + struct kernel_ethtool_coalesce *kernel_coal, >> + struct netlink_ext_ack *extack) >> +{ >> + struct veth_priv *priv = netdev_priv(dev); >> + >> + ec->tx_coalesce_usecs = priv->tx_coal_usecs; >> + return 0; >> +} >> + >> +static int veth_set_coalesce(struct net_device *dev, >> + struct ethtool_coalesce *ec, >> + struct kernel_ethtool_coalesce *kernel_coal, >> + struct netlink_ext_ack *extack) >> +{ >> + struct veth_priv *priv = netdev_priv(dev); >> + struct net_device *peer; >> + >> + /* The coalescing window delays BQL completions, so keep tx-usecs well >> + * below the tx_timeout watchdog; otherwise a large value could stall a >> + * stopped queue long enough to trip a false watchdog timeout. Cap at >> + * half the watchdog to leave a generous safety margin. tx-usecs is >> + * microseconds, the watchdog is milliseconds. >> + */ >> + if (ec->tx_coalesce_usecs > VETH_WATCHDOG_TIMEOUT_MS / 2 * USEC_PER_MSEC) { >> + NL_SET_ERR_MSG_MOD(extack, >> + "tx-usecs must stay below half the tx_timeout watchdog"); >> + return -ERANGE; >> + } >> + >> + /* Paired with READ_ONCE in veth_xdp_rcv(). */ >> + WRITE_ONCE(priv->tx_coal_usecs, ec->tx_coalesce_usecs); >> + >> + /* veth_xdp_rcv() reads each device's own value, so mirror it onto >> + * the peer to keep the pair symmetric: both directions coalesce >> + * with the same tx-usecs. Called under RTNL, rtnl_dereference() is safe. >> + */ >> + peer = rtnl_dereference(priv->peer); >> + if (peer) { >> + struct veth_priv *peer_priv = netdev_priv(peer); >> + >> + WRITE_ONCE(peer_priv->tx_coal_usecs, ec->tx_coalesce_usecs); >> + } >> + >> + return 0; >> +} >> + >> static const struct ethtool_ops veth_ethtool_ops = { >> + .supported_coalesce_params = ETHTOOL_COALESCE_TX_USECS, >> .get_drvinfo = veth_get_drvinfo, >> .get_link = ethtool_op_get_link, >> .get_strings = veth_get_strings, >> @@ -282,6 +340,8 @@ static const struct ethtool_ops veth_ethtool_ops ={ >> .get_ts_info = ethtool_op_get_ts_info, >> .get_channels = veth_get_channels, >> .set_channels = veth_set_channels, >> + .get_coalesce = veth_get_coalesce, >> + .set_coalesce = veth_set_coalesce, >> }; >> >> /* general routines */ >> @@ -969,13 +1029,54 @@ static struct sk_buff *veth_xdp_rcv_skb(struct veth_rq *rq, >> return NULL; >> } >> >> +static void veth_bql_maybe_complete(struct veth_bql_state *state, >> + struct netdev_queue *peer_txq, >> + u64 bql_flush_ns) >> +{ >> + u64 current_time; >> + >> + /* There is no reason to complete with 0 and >> + * peer_txq could go away. >> + */ >> + if (!state->n_bql || !peer_txq) >> + return; >> + >> + current_time = sched_clock(); >> + >> + /* We complete if: >> + * 1. We reach bql_flush_ns. >> + * 2. We potentially have BQL starvation. >> + */ >> + if (state->time + bql_flush_ns <= current_time || >> + state->n_bql > peer_txq->dql.limit) { > Indeed, this does not compile when CONFIG_BQL is not set. I think we should just bring back the 'queue is empty + queue is stopped' check from v6 back at the end of the poll and remove the n_bql > dql.limit check. It also feels not obvious why this is handling the starvation case. This only works, because the producer has went overlimit previously and was stopped. So more than 'limit' packets have been enqueued to the ring, and they are eventually drained when this check is true. By removing this we can also avoid accessing dql internal members, but if you don't think that's a problem we can leave as is. Further, this is only works if VETH_BQL_UNIT stays 1, otherwise it will never fire. Anyway, still its necessary to check for CONFIG_BQL. But we could solve this by adding VETH_BQL_UNIT to n_bql instead of 1. This is also safe from any overflows, since limit is bound to limit_max, inflight is always less than limit + 1*VETH_BQL_UNIT and n_bql <= inflight. In a version of bringing back the 'queue-empty' check and keeping most of the current logic (so a mixture of v6 and v7) resulted in the same performance on an x86_64 architecture. > Both Sashiko-Nipa and Sashiko-Gemini are right, this is missing a > #ifdef CONFIG_BQL. Not sure what is the best way to add them. > And for the struct we could maybe do: > > #ifdef CONFIG_BQL > struct veth_bql_state { > u64 time; /* sched_clock() when current coalescing window started */ > uint n_bql; /* BQL completions batched in the current window */ > }; > #else > struct veth_bql_state {}; > #endif Regarding the configs: we can just do something along those lines. struct veth_rq { ... #ifdef CONFIG_BQL struct veth_bql_state dql; #endif ... } and we put the rest of the code that accesses or performs an action regarding bql in some functions and do it like in netdev_* functions with Function-Signature() { #ifdef CONFIG_BQL // Code #endif } Wdyt? - Jonas > >> + netdev_tx_completed_queue(peer_txq, state->n_bql, >> + state->n_bql * VETH_BQL_UNIT); >> + state->time = current_time; >> + state->n_bql = 0; >> + } >> +} >> + >> static int veth_xdp_rcv(struct veth_rq *rq, int budget, >> struct veth_xdp_tx_bq *bq, >> struct veth_stats *stats, >> struct netdev_queue *peer_txq) >> { >> + struct veth_priv *priv = netdev_priv(rq->dev); >> + struct veth_bql_state *state = &rq->bql_state; >> int i, done = 0, n_xdpf = 0; >> void *xdpf[VETH_XDP_BATCH]; >> + u64 bql_flush_ns; >> + >> + /* Mirrored to both peers; paired with WRITE_ONCE() in veth_set_coalesce */ >> + bql_flush_ns = (u64)READ_ONCE(priv->tx_coal_usecs) * 1000; >> + >> + /* Clamp stored timestamp in case we migrated to a CPU with a behind >> + * sched_clock(); tries to reduce late BQL flushes. >> + */ >> + state->time = min(state->time, sched_clock()); >> + >> + /* Flush completions that timed out since the previous NAPI poll. */ >> + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns);>> >> for (i = 0; i < budget; i++) { >> void *ptr = __ptr_ring_consume(&rq->xdp_ring); >> @@ -1000,12 +1101,11 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, >> } >> } else { >> /* ndo_start_xmit */ >> - bool bql_charged = veth_ptr_is_bql(ptr); >> struct sk_buff *skb = veth_ptr_to_skb(ptr); >> >> + if (veth_ptr_is_bql(ptr)) >> + state->n_bql++; >> stats->xdp_bytes += skb->len; >> - if (peer_txq && bql_charged) >> - netdev_tx_completed_queue(peer_txq, 1, VETH_BQL_UNIT); >> >> skb = veth_xdp_rcv_skb(rq, skb, bq, stats); >> if (skb) { >> @@ -1015,6 +1115,7 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, >> napi_gro_receive(&rq->xdp_napi, skb); >> } >> } >> + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); >> done++; > > Sashiko-Nipa reports: > > "If veth_xdp_rcv() finishes and returns a done count less than the budget, > NAPI will go to sleep in veth_poll(). Do we need to unconditionally flush > any stranded BQL completions in veth_poll() before sleeping? > If completions are left in rq->bql_state indefinitely across NAPI idle > periods, it might present an artificially massive delay to DQL. This could > cause DQL to mistakenly conclude the hardware is extremely slow and > aggressively shrink dql.limit to its minimum, crippling throughput on > subsequent bursts." > > Again the issue that I found to be non-problematic in [1] and can be > seen by an BQL inflight > 0 when for example pktgen suddenly stops. > > If we would "unconditionally flush any stranded BQL completions in > veth_poll() before sleeping" we would *not* accumulate BQL completions > across NAPI polls but we want to do that. > > Do you agree? > > [1] https://lore.kernel.org/netdev/c8650d3a-e488-4279-b28f-549d766c23a1@tu-dortmund.de/ ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v7 5/5] veth: time-based BQL completion coalescing via ethtool tx-usecs 2026-06-30 14:00 ` Jonas Köppeler @ 2026-06-30 19:07 ` Simon Schippers 2026-07-09 10:03 ` Jonas Köppeler 0 siblings, 1 reply; 9+ messages in thread From: Simon Schippers @ 2026-06-30 19:07 UTC (permalink / raw) To: Jonas Köppeler, hawk, netdev Cc: kernel-team, Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Alexei Starovoitov, Daniel Borkmann, John Fastabend, Stanislav Fomichev, linux-kernel, bpf On 6/30/26 16:00, Jonas Köppeler wrote: > On 6/13/26 4:14 PM, Simon Schippers wrote: >> On 6/12/26 10:35, hawk@kernel.org wrote: >>> From: Simon Schippers <simon.schippers@tu-dortmund.de> >>> >>> Per-packet BQL completion forces DQL to converge on limit=2, causing >>> excessive NAPI scheduling overhead and qdisc requeues. >>> >>> Accumulate BQL completions and flush them when a configurable time >>> threshold (tx-usecs) is exceeded, letting DQL discover a limit that >>> bounds actual queuing delay to the configured interval. Coalescing >>> state persists across NAPI polls in struct veth_rq so completions can >>> accumulate beyond a single budget=64 cycle. >>> >>> The flush condition is: >>> >>> state->time + bql_flush_ns <= current_time || state->n_bql > dql.limit >>> >>> Flushing when n_bql exceeds dql.limit handles BQL starvation. >>> >>> The comparison is strictly greater-than because netdev_tx_sent_queue() >>> always lets the producer exceed the limit by one before it stops, so >>> n_bql == dql.limit is a normal in-flight state. dql.limit lives in >>> the same cacheline as the completion path, so the check is cheap. >>> >>> Add ethtool tx-usecs support for runtime tuning. Default is 100 us; >>> setting tx-usecs to 0 disables coalescing and falls back to per-packet >>> completion. >>> >>> ethtool -C <veth-dev> tx-usecs 500 # 500us coalescing >>> ethtool -C <veth-dev> tx-usecs 0 # per-packet (no coalescing) >>> >>> Co-developed-by: Jesper Dangaard Brouer <hawk@kernel.org> >>> Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org> >>> Co-developed-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> >>> Signed-off-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> >>> Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de> >>> --- >>> drivers/net/veth.c | 123 ++++++++++++++++++++++++++++++++++++++++++--- >>> 1 file changed, 117 insertions(+), 6 deletions(-) >>> >>> diff --git a/drivers/net/veth.c b/drivers/net/veth.c >>> index 2473f730734b..c62d87a8402c 100644 >>> --- a/drivers/net/veth.c >>> +++ b/drivers/net/veth.c >>> @@ -28,6 +28,7 @@ >>> #include <linux/bpf_trace.h> >>> #include <linux/net_tstamp.h> >>> #include <linux/skbuff_ref.h> >>> +#include <linux/sched/clock.h> >>> #include <net/page_pool/helpers.h> >>> #define DRV_NAME "veth" >>> @@ -50,6 +51,7 @@ >>> * delay => 64 * 250 ms = 16 s. >>> */ >>> #define VETH_WATCHDOG_TIMEOUT_MS (64 * 250) >>> +#define VETH_BQL_COAL_TX_USECS 100 /* default tx-usecs for BQL batching*/ >>> struct veth_stats { >>> u64 rx_drops; >>> @@ -69,6 +71,11 @@ struct veth_rq_stats { >>> struct u64_stats_sync syncp; >>> }; >>> +struct veth_bql_state { >>> + u64 time; /* sched_clock() when current coalescing window started */ >>> + uint n_bql; /* BQL completions batched in the current window */ >>> +}; >>> + >>> struct veth_rq { >>> struct napi_struct xdp_napi; >>> struct napi_struct __rcu *napi; /* points to xdp_napi when the latteris initialized */ >>> @@ -76,6 +83,7 @@ struct veth_rq { >>> struct bpf_prog __rcu *xdp_prog; >>> struct xdp_mem_info xdp_mem; >>> struct veth_rq_stats stats; >>> + struct veth_bql_state bql_state; >>> bool rx_notify_masked; >>> struct ptr_ring xdp_ring; >>> struct xdp_rxq_info xdp_rxq; >>> @@ -88,6 +96,7 @@ struct veth_priv { >>> struct bpf_prog *_xdp_prog; >>> struct veth_rq *rq; >>> unsigned int requested_headroom; >>> + unsigned int tx_coal_usecs; /* BQL completion coalescing */ >>> }; >>> struct veth_xdp_tx_bq { >>> @@ -272,7 +281,56 @@ static void veth_get_channels(struct net_device *dev, >>> static int veth_set_channels(struct net_device *dev, >>> struct ethtool_channels *ch); >>> +static int veth_get_coalesce(struct net_device *dev, >>> + struct ethtool_coalesce *ec, >>> + struct kernel_ethtool_coalesce *kernel_coal, >>> + struct netlink_ext_ack *extack) >>> +{ >>> + struct veth_priv *priv = netdev_priv(dev); >>> + >>> + ec->tx_coalesce_usecs = priv->tx_coal_usecs; >>> + return 0; >>> +} >>> + >>> +static int veth_set_coalesce(struct net_device *dev, >>> + struct ethtool_coalesce *ec, >>> + struct kernel_ethtool_coalesce *kernel_coal, >>> + struct netlink_ext_ack *extack) >>> +{ >>> + struct veth_priv *priv = netdev_priv(dev); >>> + struct net_device *peer; >>> + >>> + /* The coalescing window delays BQL completions, so keep tx-usecs well >>> + * below the tx_timeout watchdog; otherwise a large value could stall a >>> + * stopped queue long enough to trip a false watchdog timeout. Cap at >>> + * half the watchdog to leave a generous safety margin. tx-usecs is >>> + * microseconds, the watchdog is milliseconds. >>> + */ >>> + if (ec->tx_coalesce_usecs > VETH_WATCHDOG_TIMEOUT_MS / 2 * USEC_PER_MSEC) { >>> + NL_SET_ERR_MSG_MOD(extack, >>> + "tx-usecs must stay below half the tx_timeout watchdog"); >>> + return -ERANGE; >>> + } >>> + >>> + /* Paired with READ_ONCE in veth_xdp_rcv(). */ >>> + WRITE_ONCE(priv->tx_coal_usecs, ec->tx_coalesce_usecs); >>> + >>> + /* veth_xdp_rcv() reads each device's own value, so mirror it onto >>> + * the peer to keep the pair symmetric: both directions coalesce >>> + * with the same tx-usecs. Called under RTNL, rtnl_dereference() is safe. >>> + */ >>> + peer = rtnl_dereference(priv->peer); >>> + if (peer) { >>> + struct veth_priv *peer_priv = netdev_priv(peer); >>> + >>> + WRITE_ONCE(peer_priv->tx_coal_usecs, ec->tx_coalesce_usecs); >>> + } >>> + >>> + return 0; >>> +} >>> + >>> static const struct ethtool_ops veth_ethtool_ops = { >>> + .supported_coalesce_params = ETHTOOL_COALESCE_TX_USECS, >>> .get_drvinfo = veth_get_drvinfo, >>> .get_link = ethtool_op_get_link, >>> .get_strings = veth_get_strings, >>> @@ -282,6 +340,8 @@ static const struct ethtool_ops veth_ethtool_ops ={ >>> .get_ts_info = ethtool_op_get_ts_info, >>> .get_channels = veth_get_channels, >>> .set_channels = veth_set_channels, >>> + .get_coalesce = veth_get_coalesce, >>> + .set_coalesce = veth_set_coalesce, >>> }; >>> /* general routines */ >>> @@ -969,13 +1029,54 @@ static struct sk_buff *veth_xdp_rcv_skb(struct veth_rq *rq, >>> return NULL; >>> } >>> +static void veth_bql_maybe_complete(struct veth_bql_state *state, >>> + struct netdev_queue *peer_txq, >>> + u64 bql_flush_ns) >>> +{ >>> + u64 current_time; >>> + >>> + /* There is no reason to complete with 0 and >>> + * peer_txq could go away. >>> + */ >>> + if (!state->n_bql || !peer_txq) >>> + return; >>> + >>> + current_time = sched_clock(); >>> + >>> + /* We complete if: >>> + * 1. We reach bql_flush_ns. >>> + * 2. We potentially have BQL starvation. >>> + */ >>> + if (state->time + bql_flush_ns <= current_time || >>> + state->n_bql > peer_txq->dql.limit) { >> > Indeed, this does not compile when CONFIG_BQL is not set. I think we should just bring back the 'queue is empty + queue is stopped' check from v6 back at the end of the poll and remove the n_bql > dql.limit check. We would put #ifdef CONFIG_BQL around that logic aswell. > It also feels not obvious why this is handling the starvation case. This only works, because the producer has went overlimit previously and was stopped. So more than 'limit' packets have been enqueued to the ring, and they are eventually drained when this check is true. I think it just needs some comment tweaking: /* We complete if: * 1. We reach bql_flush_ns. * 2. We have BQL starvation. This means that the queue was over-limit * in the last interval, and there is no more data in the queue, * which is equivalent to we consumed more than limit items. */ > By removing this we can also avoid accessing dql internal members, but if you don't think that's a problem we can leave as is. I agree accessing dql internal variables is not perfect. That is why I have locally implemented DQL for software interfaces in a generic way inside dynamic_queue_limits.{h,c}. I was able to squeeze the time and n_bql variables into the completion cacheline of the dql struct by moving around variables. The logic applies inside dql_completed() if enabled. With this we just have to call netdev_completed_queue(). Also it allows for per-queue tweaking of tx_usecs via sysfs. Works well for me, can share it if we want to use it. > > Further, this is only works if VETH_BQL_UNIT stays 1, otherwise it will never fire. Anyway, still its necessary to check for CONFIG_BQL. But we could solve this by adding VETH_BQL_UNIT to n_bql instead of 1. This is also safe from any overflows, since limit is bound to limit_max, inflight is always less than limit + 1*VETH_BQL_UNIT and n_bql <= inflight. You are right. But I think there is no reason for VETH_BQL_UNIT anyway. There should be no difference in the BQL algorithm, I personally would replace VETH_BQL_UNIT with a hard-coded 1. > > In a version of bringing back the 'queue-empty' check and keeping most of the current logic (so a mixture of v6 and v7) resulted in the same performance on an x86_64 architecture. > >> Both Sashiko-Nipa and Sashiko-Gemini are right, this is missing a >> #ifdef CONFIG_BQL. Not sure what is the best way to add them. >> And for the struct we could maybe do: >> >> #ifdef CONFIG_BQL >> struct veth_bql_state { >> u64 time; /* sched_clock() when current coalescing window started */ >> uint n_bql; /* BQL completions batched in the current window */ >> }; >> #else >> struct veth_bql_state {}; >> #endif > Regarding the configs: we can just do something along those lines. > struct veth_rq { > ... > #ifdef CONFIG_BQL > struct veth_bql_state dql; > #endif > ... > } > > and we put the rest of the code that accesses or performs an action regarding bql in some functions and do it like in netdev_* functions with > > Function-Signature() > { > #ifdef CONFIG_BQL > // Code > #endif > } > > Wdyt? > - Jonas Yes, we have to. Unless we put it into dynamic_queue_limits.{h,c} of course :^) Thanks, Simon >> >>> + netdev_tx_completed_queue(peer_txq, state->n_bql, >>> + state->n_bql * VETH_BQL_UNIT); >>> + state->time = current_time; >>> + state->n_bql = 0; >>> + } >>> +} >>> + >>> static int veth_xdp_rcv(struct veth_rq *rq, int budget, >>> struct veth_xdp_tx_bq *bq, >>> struct veth_stats *stats, >>> struct netdev_queue *peer_txq) >>> { >>> + struct veth_priv *priv = netdev_priv(rq->dev); >>> + struct veth_bql_state *state = &rq->bql_state; >>> int i, done = 0, n_xdpf = 0; >>> void *xdpf[VETH_XDP_BATCH]; >>> + u64 bql_flush_ns; >>> + >>> + /* Mirrored to both peers; paired with WRITE_ONCE() in veth_set_coalesce */ >>> + bql_flush_ns = (u64)READ_ONCE(priv->tx_coal_usecs) * 1000; >>> + >>> + /* Clamp stored timestamp in case we migrated to a CPU with a behind >>> + * sched_clock(); tries to reduce late BQL flushes. >>> + */ >>> + state->time = min(state->time, sched_clock()); >>> + >>> + /* Flush completions that timed out since the previous NAPI poll. */ >>> + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns);>> >>> for (i = 0; i < budget; i++) { >>> void *ptr = __ptr_ring_consume(&rq->xdp_ring); >>> @@ -1000,12 +1101,11 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, >>> } >>> } else { >>> /* ndo_start_xmit */ >>> - bool bql_charged = veth_ptr_is_bql(ptr); >>> struct sk_buff *skb = veth_ptr_to_skb(ptr); >>> + if (veth_ptr_is_bql(ptr)) >>> + state->n_bql++; >>> stats->xdp_bytes += skb->len; >>> - if (peer_txq && bql_charged) >>> - netdev_tx_completed_queue(peer_txq, 1, VETH_BQL_UNIT); >>> skb = veth_xdp_rcv_skb(rq, skb, bq, stats); >>> if (skb) { >>> @@ -1015,6 +1115,7 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, >>> napi_gro_receive(&rq->xdp_napi, skb); >>> } >>> } >>> + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); >>> done++; >> >> Sashiko-Nipa reports: >> >> "If veth_xdp_rcv() finishes and returns a done count less than the budget, >> NAPI will go to sleep in veth_poll(). Do we need to unconditionally flush >> any stranded BQL completions in veth_poll() before sleeping? >> If completions are left in rq->bql_state indefinitely across NAPI idle >> periods, it might present an artificially massive delay to DQL. This could >> cause DQL to mistakenly conclude the hardware is extremely slow and >> aggressively shrink dql.limit to its minimum, crippling throughput on >> subsequent bursts." >> >> Again the issue that I found to be non-problematic in [1] and can be >> seen by an BQL inflight > 0 when for example pktgen suddenly stops. >> >> If we would "unconditionally flush any stranded BQL completions in >> veth_poll() before sleeping" we would *not* accumulate BQL completions >> across NAPI polls but we want to do that. >> >> Do you agree? >> >> [1] https://lore.kernel.org/netdev/c8650d3a-e488-4279-b28f-549d766c23a1@tu-dortmund.de/ > ^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v7 5/5] veth: time-based BQL completion coalescing via ethtool tx-usecs 2026-06-30 19:07 ` Simon Schippers @ 2026-07-09 10:03 ` Jonas Köppeler 0 siblings, 0 replies; 9+ messages in thread From: Jonas Köppeler @ 2026-07-09 10:03 UTC (permalink / raw) To: Simon Schippers, hawk, netdev Cc: kernel-team, Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni, Alexei Starovoitov, Daniel Borkmann, John Fastabend, Stanislav Fomichev, linux-kernel, bpf On 6/30/26 21:07, Simon Schippers wrote: > On 6/30/26 16:00, Jonas Köppeler wrote: >> On 6/13/26 4:14 PM, Simon Schippers wrote: >>> On 6/12/26 10:35, hawk@kernel.org wrote: >>>> From: Simon Schippers <simon.schippers@tu-dortmund.de> >>>> >>>> Per-packet BQL completion forces DQL to converge on limit=2, causing >>>> excessive NAPI scheduling overhead and qdisc requeues. >>>> >>>> Accumulate BQL completions and flush them when a configurable time >>>> threshold (tx-usecs) is exceeded, letting DQL discover a limit that >>>> bounds actual queuing delay to the configured interval. Coalescing >>>> state persists across NAPI polls in struct veth_rq so completions can >>>> accumulate beyond a single budget=64 cycle. >>>> >>>> The flush condition is: >>>> >>>> state->time + bql_flush_ns <= current_time || state->n_bql > dql.limit >>>> >>>> Flushing when n_bql exceeds dql.limit handles BQL starvation. >>>> >>>> The comparison is strictly greater-than because netdev_tx_sent_queue() >>>> always lets the producer exceed the limit by one before it stops, so >>>> n_bql == dql.limit is a normal in-flight state. dql.limit lives in >>>> the same cacheline as the completion path, so the check is cheap. >>>> >>>> Add ethtool tx-usecs support for runtime tuning. Default is 100 us; >>>> setting tx-usecs to 0 disables coalescing and falls back to per-packet >>>> completion. >>>> >>>> ethtool -C <veth-dev> tx-usecs 500 # 500us coalescing >>>> ethtool -C <veth-dev> tx-usecs 0 # per-packet (no coalescing) >>>> >>>> Co-developed-by: Jesper Dangaard Brouer <hawk@kernel.org> >>>> Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org> >>>> Co-developed-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> >>>> Signed-off-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> >>>> Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de> >>>> --- >>>> drivers/net/veth.c | 123 ++++++++++++++++++++++++++++++++++++++++++--- >>>> 1 file changed, 117 insertions(+), 6 deletions(-) >>>> >>>> diff --git a/drivers/net/veth.c b/drivers/net/veth.c >>>> index 2473f730734b..c62d87a8402c 100644 >>>> --- a/drivers/net/veth.c >>>> +++ b/drivers/net/veth.c >>>> @@ -28,6 +28,7 @@ >>>> #include <linux/bpf_trace.h> >>>> #include <linux/net_tstamp.h> >>>> #include <linux/skbuff_ref.h> >>>> +#include <linux/sched/clock.h> >>>> #include <net/page_pool/helpers.h> >>>> #define DRV_NAME "veth" >>>> @@ -50,6 +51,7 @@ >>>> * delay => 64 * 250 ms = 16 s. >>>> */ >>>> #define VETH_WATCHDOG_TIMEOUT_MS (64 * 250) >>>> +#define VETH_BQL_COAL_TX_USECS 100 /* default tx-usecs for BQL batching*/ >>>> struct veth_stats { >>>> u64 rx_drops; >>>> @@ -69,6 +71,11 @@ struct veth_rq_stats { >>>> struct u64_stats_sync syncp; >>>> }; >>>> +struct veth_bql_state { >>>> + u64 time; /* sched_clock() when current coalescing window started */ >>>> + uint n_bql; /* BQL completions batched in the current window */ >>>> +}; >>>> + >>>> struct veth_rq { >>>> struct napi_struct xdp_napi; >>>> struct napi_struct __rcu *napi; /* points to xdp_napi when the latteris initialized */ >>>> @@ -76,6 +83,7 @@ struct veth_rq { >>>> struct bpf_prog __rcu *xdp_prog; >>>> struct xdp_mem_info xdp_mem; >>>> struct veth_rq_stats stats; >>>> + struct veth_bql_state bql_state; >>>> bool rx_notify_masked; >>>> struct ptr_ring xdp_ring; >>>> struct xdp_rxq_info xdp_rxq; >>>> @@ -88,6 +96,7 @@ struct veth_priv { >>>> struct bpf_prog *_xdp_prog; >>>> struct veth_rq *rq; >>>> unsigned int requested_headroom; >>>> + unsigned int tx_coal_usecs; /* BQL completion coalescing */ >>>> }; >>>> struct veth_xdp_tx_bq { >>>> @@ -272,7 +281,56 @@ static void veth_get_channels(struct net_device *dev, >>>> static int veth_set_channels(struct net_device *dev, >>>> struct ethtool_channels *ch); >>>> +static int veth_get_coalesce(struct net_device *dev, >>>> + struct ethtool_coalesce *ec, >>>> + struct kernel_ethtool_coalesce *kernel_coal, >>>> + struct netlink_ext_ack *extack) >>>> +{ >>>> + struct veth_priv *priv = netdev_priv(dev); >>>> + >>>> + ec->tx_coalesce_usecs = priv->tx_coal_usecs; >>>> + return 0; >>>> +} >>>> + >>>> +static int veth_set_coalesce(struct net_device *dev, >>>> + struct ethtool_coalesce *ec, >>>> + struct kernel_ethtool_coalesce *kernel_coal, >>>> + struct netlink_ext_ack *extack) >>>> +{ >>>> + struct veth_priv *priv = netdev_priv(dev); >>>> + struct net_device *peer; >>>> + >>>> + /* The coalescing window delays BQL completions, so keep tx-usecs well >>>> + * below the tx_timeout watchdog; otherwise a large value could stall a >>>> + * stopped queue long enough to trip a false watchdog timeout. Cap at >>>> + * half the watchdog to leave a generous safety margin. tx-usecs is >>>> + * microseconds, the watchdog is milliseconds. >>>> + */ >>>> + if (ec->tx_coalesce_usecs > VETH_WATCHDOG_TIMEOUT_MS / 2 * USEC_PER_MSEC) { >>>> + NL_SET_ERR_MSG_MOD(extack, >>>> + "tx-usecs must stay below half the tx_timeout watchdog"); >>>> + return -ERANGE; >>>> + } >>>> + >>>> + /* Paired with READ_ONCE in veth_xdp_rcv(). */ >>>> + WRITE_ONCE(priv->tx_coal_usecs, ec->tx_coalesce_usecs); >>>> + >>>> + /* veth_xdp_rcv() reads each device's own value, so mirror it onto >>>> + * the peer to keep the pair symmetric: both directions coalesce >>>> + * with the same tx-usecs. Called under RTNL, rtnl_dereference() is safe. >>>> + */ >>>> + peer = rtnl_dereference(priv->peer); >>>> + if (peer) { >>>> + struct veth_priv *peer_priv = netdev_priv(peer); >>>> + >>>> + WRITE_ONCE(peer_priv->tx_coal_usecs, ec->tx_coalesce_usecs); >>>> + } >>>> + >>>> + return 0; >>>> +} >>>> + >>>> static const struct ethtool_ops veth_ethtool_ops = { >>>> + .supported_coalesce_params = ETHTOOL_COALESCE_TX_USECS, >>>> .get_drvinfo = veth_get_drvinfo, >>>> .get_link = ethtool_op_get_link, >>>> .get_strings = veth_get_strings, >>>> @@ -282,6 +340,8 @@ static const struct ethtool_ops veth_ethtool_ops ={ >>>> .get_ts_info = ethtool_op_get_ts_info, >>>> .get_channels = veth_get_channels, >>>> .set_channels = veth_set_channels, >>>> + .get_coalesce = veth_get_coalesce, >>>> + .set_coalesce = veth_set_coalesce, >>>> }; >>>> /* general routines */ >>>> @@ -969,13 +1029,54 @@ static struct sk_buff *veth_xdp_rcv_skb(struct veth_rq *rq, >>>> return NULL; >>>> } >>>> +static void veth_bql_maybe_complete(struct veth_bql_state *state, >>>> + struct netdev_queue *peer_txq, >>>> + u64 bql_flush_ns) >>>> +{ >>>> + u64 current_time; >>>> + >>>> + /* There is no reason to complete with 0 and >>>> + * peer_txq could go away. >>>> + */ >>>> + if (!state->n_bql || !peer_txq) >>>> + return; >>>> + >>>> + current_time = sched_clock(); >>>> + >>>> + /* We complete if: >>>> + * 1. We reach bql_flush_ns. >>>> + * 2. We potentially have BQL starvation. >>>> + */ >>>> + if (state->time + bql_flush_ns <= current_time || >>>> + state->n_bql > peer_txq->dql.limit) { >>> >> Indeed, this does not compile when CONFIG_BQL is not set. I think we should just bring back the 'queue is empty + queue is stopped' check from v6 back at the end of the poll and remove the n_bql > dql.limit check. > > We would put #ifdef CONFIG_BQL around that logic aswell. > >> It also feels not obvious why this is handling the starvation case. This only works, because the producer has went overlimit previously and was stopped. So more than 'limit' packets have been enqueued to the ring, and they are eventually drained when this check is true. > > I think it just needs some comment tweaking: > > /* We complete if: > * 1. We reach bql_flush_ns. > * 2. We have BQL starvation. This means that the queue was over-limit > * in the last interval, and there is no more data in the queue, > * which is equivalent to we consumed more than limit items. > */ > >> By removing this we can also avoid accessing dql internal members, but if you don't think that's a problem we can leave as is. > > I agree accessing dql internal variables is not perfect. > > That is why I have locally implemented DQL for software interfaces in > a generic way inside dynamic_queue_limits.{h,c}. > I was able to squeeze the time and n_bql variables into the completion > cacheline of the dql struct by moving around variables. > The logic applies inside dql_completed() if enabled. > With this we just have to call netdev_completed_queue(). > Also it allows for per-queue tweaking of tx_usecs via sysfs. > Works well for me, can share it if we want to use it. > >> >> Further, this is only works if VETH_BQL_UNIT stays 1, otherwise it will never fire. Anyway, still its necessary to check for CONFIG_BQL. But we could solve this by adding VETH_BQL_UNIT to n_bql instead of 1. This is also safe from any overflows, since limit is bound to limit_max, inflight is always less than limit + 1*VETH_BQL_UNIT and n_bql <= inflight. > > You are right. > > But I think there is no reason for VETH_BQL_UNIT anyway. > There should be no difference in the BQL algorithm, I personally > would replace VETH_BQL_UNIT with a hard-coded 1. > >> >> In a version of bringing back the 'queue-empty' check and keeping most of the current logic (so a mixture of v6 and v7) resulted in the same performance on an x86_64 architecture. >> >>> Both Sashiko-Nipa and Sashiko-Gemini are right, this is missing a >>> #ifdef CONFIG_BQL. Not sure what is the best way to add them. >>> And for the struct we could maybe do: >>> >>> #ifdef CONFIG_BQL >>> struct veth_bql_state { >>> u64 time; /* sched_clock() when current coalescing window started */ >>> uint n_bql; /* BQL completions batched in the current window */ >>> }; >>> #else >>> struct veth_bql_state {}; >>> #endif >> Regarding the configs: we can just do something along those lines. >> struct veth_rq { >> ... >> #ifdef CONFIG_BQL >> struct veth_bql_state dql; >> #endif >> ... >> } >> >> and we put the rest of the code that accesses or performs an action regarding bql in some functions and do it like in netdev_* functions with >> >> Function-Signature() >> { >> #ifdef CONFIG_BQL >> // Code >> #endif >> } >> >> Wdyt? >> - Jonas > > Yes, we have to. Unless we put it into dynamic_queue_limits.{h,c} > of course :^) > > Thanks, > Simon > I did implement the CONFIG_BQL guard, and reordered the completion call, dropping the pre-loop completion call and moved the in-loop completion call in front of the packet processing. I think we can drop one of the completion calls, since the there is only a difference of one packet more or less that is completed. Two patches on top of 5/5, inline below as RFC (not for application): 1) veth: simplify BQL completion condition 2) veth: Add CONFIG_BQL guards Patch 1 is the "bring back the queue-empty check" idea: it drops the state->n_bql > dql.limit test and splits the flush into (a) the time-based completion, kept per packet, and (b) an explicit post-loop "ring drained + peer stopped" wake. So veth no longer touches dql.limit. Basically the same we had in v6. However, if we like we can just replace it in the eth_bql_flush_starved with the barrier-free alternative: if (peer_txq && (u64)state->n_bql * VETH_BQL_UNIT > peer_txq->dql.limit) veth_bql_complete(...); Once the producer is stopped, num_queued/num_completed/limit are frozen, so as the ring drains n_bql rises to inflight and this becomes true exactly when the ring empties under backpressure — same event, no barrier. So there are three options how to handle this case: a) barrier + STACK_XOFF (patch 1 as posted). b) n_bql * VETH_BQL_UNIT > dql.limit. c) Simon's generic DQL-for-software-interfaces in dynamic_queue_limits.{h,c} I do not have a strong opinion. For a and b I could not measure any performance difference. Patch 2 does the CONFIG_BQL wrapping the way I sketched (BQL-only helpers with no-op stubs so veth_xdp_rcv()/teardown stay ifdef-free) and rejects `ethtool -C tx-usecs` with -EOPNOTSUPP when BQL is compiled out. Performance of !CONFIG_BQL+v7+patch-2 and net-next/main is the same. Full diffs below. - Jonas ---8<--- patch 1 ---8<--- From: =?UTF-8?q?Jonas=20K=C3=B6ppeler?= <j.koeppeler@tu-berlin.de> Date: Sun, 28 Jun 2026 10:26:15 +0200 Subject: [PATCH] veth: simplify BQL completion condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous patch flushed batched BQL completions when either the coalescing window elapsed or the batch grew past the DQL limit (state->n_bql > peer_txq->dql.limit). The latter test reaches into DQL internals and is unit-fragile: it compares the raw count n_bql against dql.limit, dropping the VETH_BQL_UNIT factor of the charge (n_bql * VETH_BQL_UNIT), so it only holds because that unit is 1. Replace that test. The time-based completion stays per packet in veth_xdp_rcv(), issued before veth_xdp_rcv_skb() so the producer wake overlaps with the first skb processing. The wake-a-stalled-producer case becomes an explicit post-loop block: once the ring has drained, if the peer TX queue is stopped by BQL backpressure (STACK_XOFF), release the batched completions to unblock it. DRV_XOFF is left to the existing wake in veth_poll(). Reading STACK_XOFF after the drain needs an smp_rmb(): the producer sets STACK_XOFF before publishing into the ring, so a consumer on another CPU that observed the packet must order its ring read ahead of the state read, or it may read a stale, un-stopped state and drop the wakeup. Pairs with the set_bit()/smp_wmb() on the producer side. Signed-off-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> --- drivers/net/veth.c | 53 ++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/drivers/net/veth.c b/drivers/net/veth.c index c62d87a8402c..2963f190988f 100644 --- a/drivers/net/veth.c +++ b/drivers/net/veth.c @@ -1029,11 +1029,21 @@ static struct sk_buff *veth_xdp_rcv_skb(struct veth_rq *rq, return NULL; } +static void veth_bql_complete(struct veth_bql_state *state, + struct netdev_queue *peer_txq, + u64 now) +{ + netdev_tx_completed_queue(peer_txq, state->n_bql, + state->n_bql * VETH_BQL_UNIT); + state->time = now; + state->n_bql = 0; +} + static void veth_bql_maybe_complete(struct veth_bql_state *state, struct netdev_queue *peer_txq, u64 bql_flush_ns) { - u64 current_time; + u64 now; /* There is no reason to complete with 0 and * peer_txq could go away. @@ -1041,19 +1051,12 @@ static void veth_bql_maybe_complete(struct veth_bql_state *state, if (!state->n_bql || !peer_txq) return; - current_time = sched_clock(); - - /* We complete if: - * 1. We reach bql_flush_ns. - * 2. We potentially have BQL starvation. + /* Release the batched completions once the coalescing window has + * elapsed. */ - if (state->time + bql_flush_ns <= current_time || - state->n_bql > peer_txq->dql.limit) { - netdev_tx_completed_queue(peer_txq, state->n_bql, - state->n_bql * VETH_BQL_UNIT); - state->time = current_time; - state->n_bql = 0; - } + now = sched_clock(); + if (state->time + bql_flush_ns <= now) + veth_bql_complete(state, peer_txq, now); } static int veth_xdp_rcv(struct veth_rq *rq, int budget, @@ -1075,9 +1078,6 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, */ state->time = min(state->time, sched_clock()); - /* Flush completions that timed out since the previous NAPI poll. */ - veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); - for (i = 0; i < budget; i++) { void *ptr = __ptr_ring_consume(&rq->xdp_ring); @@ -1105,8 +1105,12 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, if (veth_ptr_is_bql(ptr)) state->n_bql++; - stats->xdp_bytes += skb->len; + /* Complete before processing so the producer wakes + * sooner; ring-empty case handled after the loop. + */ + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); + stats->xdp_bytes += skb->len; skb = veth_xdp_rcv_skb(rq, skb, bq, stats); if (skb) { if (skb_shared(skb) || skb_unclone(skb, GFP_ATOMIC)) @@ -1115,13 +1119,26 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, napi_gro_receive(&rq->xdp_napi, skb); } } - veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); done++; } if (n_xdpf) veth_xdp_rcv_bulk_skb(rq, xdpf, n_xdpf, bq, stats); + /* If the ring drained and the peer TX queue is stalled by BQL + * backpressure (STACK_XOFF), release the batched completions now to + * unblock the producer. DRV_XOFF is handled by the wake in veth_poll(). + */ + if (peer_txq && state->n_bql && __ptr_ring_empty(&rq->xdp_ring)) { + /* The consume above observed the producer's publish; order it + * before reading STACK_XOFF. Pairs with the smp_wmb() and XOFF + * set_bit() on the producer side. + */ + smp_rmb(); + if (test_bit(__QUEUE_STATE_STACK_XOFF, &peer_txq->state)) + veth_bql_complete(state, peer_txq, sched_clock()); + } + u64_stats_update_begin(&rq->stats.syncp); rq->stats.vs.xdp_redirect += stats->xdp_redirect; rq->stats.vs.xdp_bytes += stats->xdp_bytes; -- 2.53.0 ---8<--- patch 2 ---8<--- From: =?UTF-8?q?Jonas=20K=C3=B6ppeler?= <j.koeppeler@tu-berlin.de> Date: Thu, 2 Jul 2026 10:13:02 +0000 Subject: [PATCH] veth: Add CONFIG_BQL guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the BQL-only code under CONFIG_BQL and expose it to the driver through a small set of helpers with no-op stubs for the !CONFIG_BQL case, so veth_xdp_rcv() and the NAPI teardown path stay free of ifdefs: veth_bql_poll_prepare() per-poll setup: clamps the coalescing window timestamp and returns the interval length, so !CONFIG_BQL builds never read tx_coal_usecs veth_bql_account() counts a consumed BQL-tagged skb and releases the batch once the interval window has elapsed veth_bql_flush_starved() releases the batch early when the ring has drained and the peer txq is stopped by BQL backpressure (STACK_XOFF) veth_bql_state_init() veth_bql_drain_and_reset() per-queue state setup and teardown Since tx-usecs only batches BQL completions, reject ethtool -C tx-usecs with -EOPNOTSUPP when BQL is compiled out instead of silently storing an inert value. Signed-off-by: Jonas Köppeler <j.koeppeler@tu-berlin.de> --- drivers/net/veth.c | 173 +++++++++++++++++++++++++++++---------------- 1 file changed, 111 insertions(+), 62 deletions(-) diff --git a/drivers/net/veth.c b/drivers/net/veth.c index 2963f190988f..5c3b7820c55c 100644 --- a/drivers/net/veth.c +++ b/drivers/net/veth.c @@ -73,7 +73,7 @@ struct veth_rq_stats { struct veth_bql_state { u64 time; /* sched_clock() when current coalescing window started */ - uint n_bql; /* BQL completions batched in the current window */ + unsigned int n_bql; /* BQL completions batched in the current window */ }; struct veth_rq { @@ -83,7 +83,9 @@ struct veth_rq { struct bpf_prog __rcu *xdp_prog; struct xdp_mem_info xdp_mem; struct veth_rq_stats stats; +#ifdef CONFIG_BQL struct veth_bql_state bql_state; +#endif bool rx_notify_masked; struct ptr_ring xdp_ring; struct xdp_rxq_info xdp_rxq; @@ -300,6 +302,12 @@ static int veth_set_coalesce(struct net_device *dev, struct veth_priv *priv = netdev_priv(dev); struct net_device *peer; + /* tx-usecs only batches BQL completions; without BQL it is inert. */ + if (!IS_ENABLED(CONFIG_BQL)) { + NL_SET_ERR_MSG_MOD(extack, "tx-usecs requires CONFIG_BQL"); + return -EOPNOTSUPP; + } + /* The coalescing window delays BQL completions, so keep tx-usecs well * below the tx_timeout watchdog; otherwise a large value could stall a * stopped queue long enough to trip a false watchdog timeout. Cap at @@ -351,11 +359,6 @@ static bool veth_is_xdp_frame(void *ptr) return (unsigned long)ptr & VETH_XDP_FLAG; } -static bool veth_ptr_is_bql(void *ptr) -{ - return (unsigned long)ptr & VETH_BQL_FLAG; -} - static struct sk_buff *veth_ptr_to_skb(void *ptr) { return (void *)((unsigned long)ptr & ~VETH_BQL_FLAG); @@ -384,25 +387,6 @@ static void veth_ptr_free(void *ptr) kfree_skb(veth_ptr_to_skb(ptr)); } -/* Drain frames left in the ptr_ring at teardown, freeing each one and - * returning the number of BQL-charged SKBs. The caller completes these - * via netdev_tx_completed_queue() to balance the DQL accounting, avoiding - * the racy netdev_tx_reset_queue()/dql_reset(). - */ -static unsigned int veth_ptr_ring_drain(struct ptr_ring *ring) -{ - unsigned int n_bql = 0; - void *ptr; - - while ((ptr = ptr_ring_consume(ring))) { - if (veth_ptr_is_bql(ptr)) - n_bql++; - veth_ptr_free(ptr); - } - - return n_bql; -} - static void __veth_xdp_flush(struct veth_rq *rq) { /* Write ptr_ring before reading rx_notify_masked */ @@ -1029,6 +1013,31 @@ static struct sk_buff *veth_xdp_rcv_skb(struct veth_rq *rq, return NULL; } +#ifdef CONFIG_BQL +static bool veth_ptr_is_bql(void *ptr) +{ + return (unsigned long)ptr & VETH_BQL_FLAG; +} + +/* Drain frames left in the ptr_ring at teardown, freeing each one and + * returning the number of BQL-charged SKBs. The caller completes these + * via netdev_tx_completed_queue() to balance the DQL accounting, avoiding + * the racy netdev_tx_reset_queue()/dql_reset(). + */ +static unsigned int veth_bql_ring_drain(struct ptr_ring *ring) +{ + unsigned int n_bql = 0; + void *ptr; + + while ((ptr = ptr_ring_consume(ring))) { + if (veth_ptr_is_bql(ptr)) + n_bql++; + veth_ptr_free(ptr); + } + + return n_bql; +} + static void veth_bql_complete(struct veth_bql_state *state, struct netdev_queue *peer_txq, u64 now) @@ -1039,18 +1048,18 @@ static void veth_bql_complete(struct veth_bql_state *state, state->n_bql = 0; } -static void veth_bql_maybe_complete(struct veth_bql_state *state, - struct netdev_queue *peer_txq, - u64 bql_flush_ns) +static void veth_bql_account(struct veth_rq *rq, + struct netdev_queue *peer_txq, + void *ptr, u64 bql_flush_ns) { + struct veth_bql_state *state = &rq->bql_state; u64 now; - /* There is no reason to complete with 0 and - * peer_txq could go away. - */ - if (!state->n_bql || !peer_txq) + if (!peer_txq || !veth_ptr_is_bql(ptr)) return; + state->n_bql++; + /* Release the batched completions once the coalescing window has * elapsed. */ @@ -1059,24 +1068,81 @@ static void veth_bql_maybe_complete(struct veth_bql_state *state, veth_bql_complete(state, peer_txq, now); } +/* Per-poll setup: clamp the window timestamp and return the length of the + * coalescing window in ns. + */ +static u64 veth_bql_poll_prepare(struct veth_rq *rq) +{ + struct veth_priv *priv = netdev_priv(rq->dev); + struct veth_bql_state *state = &rq->bql_state; + + /* Clamp stored timestamp in case we migrated to a CPU with a behind + * sched_clock(); tries to reduce late BQL flushes. + */ + state->time = min(state->time, sched_clock()); + + /* Mirrored to both peers; paired with WRITE_ONCE() in veth_set_coalesce */ + return (u64)READ_ONCE(priv->tx_coal_usecs) * NSEC_PER_USEC; +} + +static void veth_bql_flush_starved(struct veth_rq *rq, + struct netdev_queue *peer_txq) +{ + struct veth_bql_state *state = &rq->bql_state; + + if (!peer_txq) + return; + + /* If the ring drained and the peer TX queue is stalled by BQL + * backpressure (STACK_XOFF), release the batched completions now to + * unblock the producer. DRV_XOFF is handled by the wake in veth_poll(). + */ + if (state->n_bql && __ptr_ring_empty(&rq->xdp_ring)) { + /* The consume above observed the producer's publish; order it + * before reading STACK_XOFF. Pairs with the smp_wmb() and XOFF + * set_bit() on the producer side. + */ + smp_rmb(); + if (test_bit(__QUEUE_STATE_STACK_XOFF, &peer_txq->state)) + veth_bql_complete(state, peer_txq, sched_clock()); + } +} + +static void veth_bql_state_init(struct veth_rq *rq) +{ + rq->bql_state.time = sched_clock(); + rq->bql_state.n_bql = 0; +} + +static unsigned int veth_bql_drain_and_reset(struct veth_rq *rq) +{ + unsigned int n_bql = veth_bql_ring_drain(&rq->xdp_ring) + rq->bql_state.n_bql; + + rq->bql_state.n_bql = 0; + rq->bql_state.time = 0; + return n_bql; +} +#else +static inline void veth_bql_account(struct veth_rq *rq, + struct netdev_queue *peer_txq, + void *ptr, u64 bql_flush_ns) {} +static inline u64 veth_bql_poll_prepare(struct veth_rq *rq) { return 0; } +static inline void veth_bql_flush_starved(struct veth_rq *rq, + struct netdev_queue *peer_txq) {} +static inline void veth_bql_state_init(struct veth_rq *rq) {} +static inline unsigned int veth_bql_drain_and_reset(struct veth_rq *rq) { return 0; } +#endif + static int veth_xdp_rcv(struct veth_rq *rq, int budget, struct veth_xdp_tx_bq *bq, struct veth_stats *stats, struct netdev_queue *peer_txq) { - struct veth_priv *priv = netdev_priv(rq->dev); - struct veth_bql_state *state = &rq->bql_state; int i, done = 0, n_xdpf = 0; void *xdpf[VETH_XDP_BATCH]; u64 bql_flush_ns; - /* Mirrored to both peers; paired with WRITE_ONCE() in veth_set_coalesce */ - bql_flush_ns = (u64)READ_ONCE(priv->tx_coal_usecs) * 1000; - - /* Clamp stored timestamp in case we migrated to a CPU with a behind - * sched_clock(); tries to reduce late BQL flushes. - */ - state->time = min(state->time, sched_clock()); + bql_flush_ns = veth_bql_poll_prepare(rq); for (i = 0; i < budget; i++) { void *ptr = __ptr_ring_consume(&rq->xdp_ring); @@ -1103,12 +1169,10 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, /* ndo_start_xmit */ struct sk_buff *skb = veth_ptr_to_skb(ptr); - if (veth_ptr_is_bql(ptr)) - state->n_bql++; /* Complete before processing so the producer wakes * sooner; ring-empty case handled after the loop. */ - veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); + veth_bql_account(rq, peer_txq, ptr, bql_flush_ns); stats->xdp_bytes += skb->len; skb = veth_xdp_rcv_skb(rq, skb, bq, stats); @@ -1125,19 +1189,7 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, if (n_xdpf) veth_xdp_rcv_bulk_skb(rq, xdpf, n_xdpf, bq, stats); - /* If the ring drained and the peer TX queue is stalled by BQL - * backpressure (STACK_XOFF), release the batched completions now to - * unblock the producer. DRV_XOFF is handled by the wake in veth_poll(). - */ - if (peer_txq && state->n_bql && __ptr_ring_empty(&rq->xdp_ring)) { - /* The consume above observed the producer's publish; order it - * before reading STACK_XOFF. Pairs with the smp_wmb() and XOFF - * set_bit() on the producer side. - */ - smp_rmb(); - if (test_bit(__QUEUE_STATE_STACK_XOFF, &peer_txq->state)) - veth_bql_complete(state, peer_txq, sched_clock()); - } + veth_bql_flush_starved(rq, peer_txq); u64_stats_update_begin(&rq->stats.syncp); rq->stats.vs.xdp_redirect += stats->xdp_redirect; @@ -1241,8 +1293,7 @@ static int __veth_napi_enable_range(struct net_device *dev, int start, int end) for (i = start; i < end; i++) { struct veth_rq *rq = &priv->rq[i]; - rq->bql_state.time = sched_clock(); - rq->bql_state.n_bql = 0; + veth_bql_state_init(rq); napi_enable(&rq->xdp_napi); rcu_assign_pointer(priv->rq[i].napi, &priv->rq[i].xdp_napi); @@ -1298,10 +1349,8 @@ static void veth_napi_del_range(struct net_device *dev, int start, int end) * (consumed by NAPI but not yet flushed). Both were charged * via netdev_tx_sent_queue() and are still outstanding. */ - n_bql = veth_ptr_ring_drain(&rq->xdp_ring) + rq->bql_state.n_bql; + n_bql = veth_bql_drain_and_reset(rq); ptr_ring_cleanup(&rq->xdp_ring, veth_ptr_free); - rq->bql_state.n_bql = 0; - rq->bql_state.time = 0; if (!peer || i >= peer->num_tx_queues) continue; -- 2.53.0 >>> >>>> + netdev_tx_completed_queue(peer_txq, state->n_bql, >>>> + state->n_bql * VETH_BQL_UNIT); >>>> + state->time = current_time; >>>> + state->n_bql = 0; >>>> + } >>>> +} >>>> + >>>> static int veth_xdp_rcv(struct veth_rq *rq, int budget, >>>> struct veth_xdp_tx_bq *bq, >>>> struct veth_stats *stats, >>>> struct netdev_queue *peer_txq) >>>> { >>>> + struct veth_priv *priv = netdev_priv(rq->dev); >>>> + struct veth_bql_state *state = &rq->bql_state; >>>> int i, done = 0, n_xdpf = 0; >>>> void *xdpf[VETH_XDP_BATCH]; >>>> + u64 bql_flush_ns; >>>> + >>>> + /* Mirrored to both peers; paired with WRITE_ONCE() in veth_set_coalesce */ >>>> + bql_flush_ns = (u64)READ_ONCE(priv->tx_coal_usecs) * 1000; >>>> + >>>> + /* Clamp stored timestamp in case we migrated to a CPU with a behind >>>> + * sched_clock(); tries to reduce late BQL flushes. >>>> + */ >>>> + state->time = min(state->time, sched_clock()); >>>> + >>>> + /* Flush completions that timed out since the previous NAPI poll. */ >>>> + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns);>> >>>> for (i = 0; i < budget; i++) { >>>> void *ptr = __ptr_ring_consume(&rq->xdp_ring); >>>> @@ -1000,12 +1101,11 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, >>>> } >>>> } else { >>>> /* ndo_start_xmit */ >>>> - bool bql_charged = veth_ptr_is_bql(ptr); >>>> struct sk_buff *skb = veth_ptr_to_skb(ptr); >>>> + if (veth_ptr_is_bql(ptr)) >>>> + state->n_bql++; >>>> stats->xdp_bytes += skb->len; >>>> - if (peer_txq && bql_charged) >>>> - netdev_tx_completed_queue(peer_txq, 1, VETH_BQL_UNIT); >>>> skb = veth_xdp_rcv_skb(rq, skb, bq, stats); >>>> if (skb) { >>>> @@ -1015,6 +1115,7 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget, >>>> napi_gro_receive(&rq->xdp_napi, skb); >>>> } >>>> } >>>> + veth_bql_maybe_complete(state, peer_txq, bql_flush_ns); >>>> done++; >>> >>> Sashiko-Nipa reports: >>> >>> "If veth_xdp_rcv() finishes and returns a done count less than the budget, >>> NAPI will go to sleep in veth_poll(). Do we need to unconditionally flush >>> any stranded BQL completions in veth_poll() before sleeping? >>> If completions are left in rq->bql_state indefinitely across NAPI idle >>> periods, it might present an artificially massive delay to DQL. This could >>> cause DQL to mistakenly conclude the hardware is extremely slow and >>> aggressively shrink dql.limit to its minimum, crippling throughput on >>> subsequent bursts." >>> >>> Again the issue that I found to be non-problematic in [1] and can be >>> seen by an BQL inflight > 0 when for example pktgen suddenly stops. >>> >>> If we would "unconditionally flush any stranded BQL completions in >>> veth_poll() before sleeping" we would *not* accumulate BQL completions >>> across NAPI polls but we want to do that. >>> >>> Do you agree? >>> >>> [1] https://lore.kernel.org/netdev/c8650d3a-e488-4279-b28f-549d766c23a1@tu-dortmund.de/ >> ^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-07-09 10:04 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
[not found] <20260612083530.1650245-1-hawk@kernel.org>
2026-06-12 8:35 ` [PATCH net-next v7 1/5] net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices hawk
2026-06-12 8:35 ` [PATCH net-next v7 2/5] veth: implement Byte Queue Limits (BQL) for latency reduction hawk
2026-06-12 8:35 ` [PATCH net-next v7 3/5] veth: add tx_timeout watchdog as BQL safety net hawk
2026-06-12 8:35 ` [PATCH net-next v7 4/5] net: sched: add timeout count to NETDEV WATCHDOG message hawk
2026-06-12 8:35 ` [PATCH net-next v7 5/5] veth: time-based BQL completion coalescing via ethtool tx-usecs hawk
2026-06-13 14:14 ` Simon Schippers
2026-06-30 14:00 ` Jonas Köppeler
2026-06-30 19:07 ` Simon Schippers
2026-07-09 10:03 ` Jonas Köppeler
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®