Skip to contentSkip to conversation

veth: add Byte Queue Limits (BQL) support

PATCH Superseded [email protected] 7 participants 39 messages May 5 – May 27 1 message not in the archive

Subsystems (5)

v5 of 7 Superseded 1 of 5 applied Latest is v7 →

lore
Share & tools
lore
#0 lore raw

v5 cover veth: add Byte Queue Limits (BQL) support

From: Jesper Dangaard Brouer <[email protected]>

This series adds BQL (Byte Queue Limits) to the veth driver, reducing latency by dynamically limiting in-flight packets in the ptr_ring and moving buffering into the qdisc where AQM algorithms can act on it.

Problem:
veth's 256-entry ptr_ring acts as a "dark buffer" -- packets queued there are invisible to the qdisc's AQM. Under load, the ring fills completely (DRV_XOFF backpressure), adding up to 256 packets of unmanaged latency before the qdisc even sees congestion.

Solution:
BQL (STACK_XOFF) dynamically limits in-flight packets, stopping the queue before the ring fills. This keeps the ring shallow and pushes excess packets into the qdisc, where sojourn-based AQM can measure and drop them.

Test setup: veth pair, UDP flood, 13000 iptables rules in consumer namespace (slows NAPI-64 cycle to ~6-7ms), ping measures RTT under load.

BQL off BQL on

  fq_codel:  RTT ~22ms, 4% loss         RTT ~1.3ms, 0% loss
  sfq:       RTT ~24ms, 0% loss         RTT ~1.5ms, 0% loss

BQL reduces ping RTT by ~17x for both qdiscs. Consumer throughput is unchanged (~10K pps) -- BQL adds no overhead.

CoDel bug discovered during BQL development:
Our original motivation for BQL was fq_codel ping loss observed under load (4-26% depending on NAPI cycle time). Investigating this led us to discover a bug in the CoDel implementation: codel_dequeue() does not reset vars->first_above_time when a flow goes empty, contrary to the reference algorithm. This causes stale CoDel state to persist across empty periods in fq_codel's per-flow queues, penalizing sparse flows like ICMP ping. A fix for this has been applied to the net tree 815980fe6dbb ("net_sched: codel: fix stale state for empty flows in fq_codel")

BQL remains valuable independently: it reduces RTT by ~17x by moving buffering from the dark ptr_ring into the qdisc. Additionally, BQL clears STACK_XOFF per-SKB as each packet completes, rather than batch-waking after 64 packets (DRV_XOFF). This keeps sojourn times below fq_codel's target, preventing CoDel from entering dropping state on non-congested flows in the first place.

Key design decisions:
- Charge-under-lock in veth_xdp_rx(): The BQL charge must precede the ptr_ring produce, because the NAPI consumer can run on another CPU and complete the SKB immediately after it becomes visible. To avoid a pre-charge/undo pattern, the charge is done under the ptr_ring producer_lock after confirming the ring is not full. BQL is only charged when produce is guaranteed to succeed, keeping num_queued monotonically increasing. HARD_TX_LOCK already serializes dql_queued() (veth requires a qdisc for BQL); the ptr_ring lock additionally would allow noqueue to work correctly.

- Per-SKB BQL tracking via pointer tag: A VETH_BQL_FLAG bit in the ptr_ring pointer records whether each SKB was BQL-charged. This is necessary because the qdisc can be replaced live (noqueue->sfq or vice versa) while SKBs are in-flight -- the completion side must know the charge state that was decided at enqueue time.

- IFF_NO_QUEUE + BQL coexistence: A new dev->bql flag enables BQL sysfs exposure for IFF_NO_QUEUE devices that opt in to DQL accounting, without changing IFF_NO_QUEUE semantics.

Background and acknowledgments:
Mike Freemon reported the veth dark buffer problem internally at Cloudflare and showed that recompiling the kernel with a ptr_ring size of 30 (down from 256) made fq_codel work dramatically better. This was the primary motivation for a proper BQL solution that achieves the same effect dynamically without a kernel rebuild.

Chris Arges wrote a reproducer for the dark buffer latency problem:
https://github.com/netoptimizer/veth-backpressure-performance-testing
This is where we first observed ping packets being dropped under fq_codel, which became our secondary motivation for BQL. In production we switched to SFQ on veth devices as a workaround.

Jonas Koeppeler provided extensive testing and code review. Together we discovered that the fq_codel ping loss was actually a 12-year-old CoDel bug (stale first_above_time in empty flows), not caused by the dark buffer itself.

Patch overview:
1. veth: fix OOB txq access in veth_poll() with asymmetric queue counts
2. net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices
3. veth: implement Byte Queue Limits (BQL) for latency reduction
4. veth: add tx_timeout watchdog as BQL safety net
5. net: sched: add timeout count to NETDEV WATCHDOG message

Jesper Dangaard Brouer (5):
veth: fix OOB txq access in veth_poll() with asymmetric queue counts
net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices
veth: implement Byte Queue Limits (BQL) for latency reduction
veth: add tx_timeout watchdog as BQL safety net
net: sched: add timeout count to NETDEV WATCHDOG message

 .../networking/net_cachelines/net_device.rst  |   1 +
 drivers/net/veth.c                            | 107 ++++++++++++++++--
 include/linux/netdevice.h                     |   2 +
 net/core/net-sysfs.c                          |   8 +-
 net/sched/sch_generic.c                       |   8 +-
 5 files changed, 109 insertions(+), 17 deletions(-)

V4: https://lore.kernel.org/all/[email protected]/

Changes since V4:
- New patch 1: fix OOB txq access in veth_poll() when veth peers have asymmetric RX/TX queue counts. XDP redirect can deliver frames to an RX queue index that exceeds the peer's TX queue count, causing an out-of-bounds netdev_get_tx_queue() access. Found by sashiko-bot.
- Patch 3 (veth BQL): wake stopped peer txqs in veth_napi_del_range() to clear DRV_XOFF after NAPI teardown. A concurrent veth_xmit() can set DRV_XOFF between rcu_assign_pointer(napi, NULL) and synchronize_net(); with NAPI gone, no veth_poll() clears it. Guarded by netif_running() to skip during device close.

V3: https://lore.kernel.org/all/[email protected]/

Changes since V3:
- Drop selftest patch (patch 5 from V3) per maintainer request.
- Rebase on latest net-next.

V2: https://lore.kernel.org/all/[email protected]/

Changes since V2:
- Patch 2 (veth BQL): fix syzbot WARNING in veth_napi_del_range():
clamp BQL reset loop to peer's real_num_tx_queues. The loop was iterating dev->real_num_rx_queues but indexing peer's txq[], which goes out of bounds when the peer has fewer TX queues (e.g. veth enslaved to a bond with XDP attached).

V1: https://lore.kernel.org/all/[email protected]/

Changes since V1:
- Patch 1 (dev->bql flag): add kdoc entry for @bql in struct net_device.
- Patch 2 (veth BQL): charge fixed VETH_BQL_UNIT (1) per packet instead of skb->len. veth has no link speed; the ptr_ring is packet-indexed. Byte-based charging lets small packets sneak many entries into the ring.
Testing: min-size packet flood causes 3.7x ping RTT degradation with skb->len vs no change with fixed-unit charging.
- Patch 3 (tx_timeout watchdog): fix race with peer NAPI: replace netdev_tx_reset_queue() with clear_bit(STACK_XOFF) + netif_tx_wake_queue() to avoid dql_reset() racing with concurrent dql_completed().
- Cover letter: update CoDel fix reference to merged commit in net tree.

Cc "David S. Miller" <[email protected]>
Cc Eric Dumazet <[email protected]>
Cc Jakub Kicinski <[email protected]>
Cc Paolo Abeni <[email protected]>
Cc Simon Horman <[email protected]>
Cc Chris Arges <[email protected]>
Cc Mike Freemon <[email protected]>
Cc Toke Høiland-Jørgensen <[email protected]>
Cc Jonas Köppeler <[email protected]>
Cc Breno Leitao <[email protected]>
Cc Simon Schippers <[email protected]>
Signature · 1 line
2.43.0
#1 lore raw

PATCH v5 1/5 veth: fix OOB txq access in veth_poll() with asymmetric queue counts

From: Jesper Dangaard Brouer <[email protected]>

XDP redirect into a veth device (via bpf_redirect()) calls veth_xdp_xmit(), which enqueues frames into the peer's ptr_ring using
smp_processor_id() % peer->real_num_rx_queues
as the ring index. With an asymmetric veth pair where the peer has fewer TX queues than RX queues, that index can exceed peer->real_num_tx_queues.

veth_poll() then resolves peer_txq for the ring via:

  peer_txq = peer_dev ? netdev_get_tx_queue(peer_dev, queue_idx) : NULL;

where queue_idx = rq->xdp_rxq.queue_index. When queue_idx exceeds peer_dev->real_num_tx_queues this is an out-of-bounds (OOB) access into the peer's netdev_queue array, triggering DEBUG_NET_WARN_ON_ONCE in netdev_get_tx_queue().

The normal ndo_start_xmit path is not affected: the stack clamps skb->queue_mapping via netdev_cap_txqueue() before invoking ndo_start_xmit, so rxq in veth_xmit() never exceeds real_num_tx_queues.

Fix veth_poll() by clamping: only dereference peer_txq when queue_idx is within bounds, otherwise set it to NULL. The out-of-range rings are fed exclusively via XDP redirect (veth_xdp_xmit), never via ndo_start_xmit (veth_xmit), so the peer txq was never stopped and there is nothing to wake; NULL is the correct fallback.

Fixes dc82a33297fc ("veth: apply qdisc backpressure on full ptr_ring to reduce TX drops")
---
 drivers/net/veth.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
drivers/net/veth.c +2 -1
index e35df717e65e..0cfb19b760dd 100644
@@ -972,7 +972,8 @@ static int veth_poll(struct napi_struct *napi, int budget)
 
 	/* NAPI functions as RCU section */
 	peer_dev = rcu_dereference_check(priv->peer, rcu_read_lock_bh_held());
-	peer_txq = peer_dev ? netdev_get_tx_queue(peer_dev, queue_idx) : NULL;
+	peer_txq = (peer_dev && queue_idx < peer_dev->real_num_tx_queues) ?
+		   netdev_get_tx_queue(peer_dev, queue_idx) : NULL;
 
 	xdp_set_return_frame_no_direct();
 	done = veth_xdp_rcv(rq, budget, &bq, &stats);
Signature · 1 line
2.43.0
maintainer
#2 lore raw

Re: [PATCH net-next v5 1/5] veth: fix OOB txq access in veth_poll() with asymmetric queue counts

Quoted 28 lines · On 5/5/26 3:21 PM, [email protected] wrote:

From: Jesper Dangaard Brouer <[email protected]>

XDP redirect into a veth device (via bpf_redirect()) calls veth_xdp_xmit(), which enqueues frames into the peer's ptr_ring using
smp_processor_id() % peer->real_num_rx_queues
as the ring index. With an asymmetric veth pair where the peer has fewer TX queues than RX queues, that index can exceed peer->real_num_tx_queues.

veth_poll() then resolves peer_txq for the ring via:

  peer_txq = peer_dev ? netdev_get_tx_queue(peer_dev, queue_idx) : NULL;

where queue_idx = rq->xdp_rxq.queue_index. When queue_idx exceeds peer_dev->real_num_tx_queues this is an out-of-bounds (OOB) access into the peer's netdev_queue array, triggering DEBUG_NET_WARN_ON_ONCE in netdev_get_tx_queue().

The normal ndo_start_xmit path is not affected: the stack clamps skb->queue_mapping via netdev_cap_txqueue() before invoking ndo_start_xmit, so rxq in veth_xmit() never exceeds real_num_tx_queues.

Fix veth_poll() by clamping: only dereference peer_txq when queue_idx is within bounds, otherwise set it to NULL. The out-of-range rings are fed exclusively via XDP redirect (veth_xdp_xmit), never via ndo_start_xmit (veth_xmit), so the peer txq was never stopped and there is nothing to wake; NULL is the correct fallback.

Quoted 4 lines
Fixes dc82a33297fc ("veth: apply qdisc backpressure on full ptr_ring to reduce TX drops")

This looks fairly uncontroversial, but it's IMHO net material. Let me apply it there.

/P

#3 lore raw

PATCH v5 2/5 net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices

In reply to [email protected]

From: Jesper Dangaard Brouer <[email protected]>

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.

---
 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(-)
Documentation/networking/net_cachelines/net_device.rst +1 -0
index 1c19bb7705df..b775d3235a2d 100644
@@ -170,6 +170,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
include/linux/netdevice.h +2 -0
index 744ffa243501..d4c7b020b6a7 100644
@@ -2065,6 +2065,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
@@ -2479,6 +2480,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;
 
net/core/net-sysfs.c +7 -1
index 3318b5666e43..82833e5dae03 100644
@@ -1945,10 +1945,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)
Signature · 1 line
2.43.0
#4 lore raw

PATCH v5 3/5 veth: implement Byte Queue Limits (BQL) for latency reduction

In reply to [email protected]

From: Jesper Dangaard Brouer <[email protected]>

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. Reset BQL state in veth_napi_del_range() after synchronize_net() to avoid racing with in-flight veth_poll() calls. Clamp the reset loop to the peer's real_num_tx_queues, since the peer may have fewer TX queues than the local device has RX queues (e.g. when veth is enslaved to a bond with XDP attached).

---
 drivers/net/veth.c | 86 ++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 75 insertions(+), 11 deletions(-)
drivers/net/veth.c +75 -11
index 0cfb19b760dd..86b78900c48e 100644
@@ -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,7 @@ 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));
 }
 
 static void __veth_xdp_flush(struct veth_rq *rq)
@@ -309,19 +328,33 @@ 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(!ring->size) || __ptr_ring_full(ring)) {
+		spin_unlock(&ring->producer_lock);
 		return NETDEV_TX_BUSY; /* signal qdisc layer */
+	}
+
+	/* BQL charge before produce; consumer cannot see entry yet */
+	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);
 }
 
@@ -348,10 +381,11 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
 {
 	struct veth_priv *rcv_priv, *priv = netdev_priv(dev);
 	struct veth_rq *rq = NULL;
-	struct netdev_queue *txq;
+	struct netdev_queue *txq = NULL;
 	struct net_device *rcv;
 	int length = skb->len;
 	bool use_napi = false;
+	bool do_bql = false;
 	int ret, rxq;
 
 	rcu_read_lock();
@@ -375,8 +409,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 +450,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 +939,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 +968,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 +1020,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 +1118,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++) {
@@ -1092,6 +1137,24 @@ static void veth_napi_del_range(struct net_device *dev, int start, int end)
 		ptr_ring_cleanup(&rq->xdp_ring, veth_ptr_free);
 	}
 
+	/* Reset BQL and wake stopped peer txqs.  A concurrent veth_xmit()
+	 * may have set DRV_XOFF between rcu_assign_pointer(napi, NULL) and
+	 * synchronize_net(), and NAPI can no longer clear it.
+	 * Only wake when the device is still up.
+	 */
+	peer = rtnl_dereference(priv->peer);
+	if (peer) {
+		int peer_end = min_t(int, end, peer->real_num_tx_queues);
+
+		for (i = start; i < peer_end; i++) {
+			struct netdev_queue *txq = netdev_get_tx_queue(peer, i);
+
+			netdev_tx_reset_queue(txq);
+			if (netif_running(dev))
+				netif_tx_wake_queue(txq);
+		}
+	}
+
 	for (i = start; i < end; i++) {
 		page_pool_destroy(priv->rq[i].page_pool);
 		priv->rq[i].page_pool = NULL;
@@ -1741,6 +1804,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;
Signature · 1 line
2.43.0
#5 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [High] Resetting the peer's TX queue during NAPI deletion fails to explicitly schedule the qdisc if it…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Resetting the peer's TX queue during NAPI deletion fails to explicitly schedule the qdisc if it was stopped solely by BQL, potentially stranding buffered packets indefinitely.
- [High] veth_napi_del_range() conditionally skips waking the peer's stopped TX queue when the local device is going down, permanently leaking the DRV_XOFF stop state on the peer.
- [Medium] DRV_XOFF queue wakeups are bypassed for deactivated TX queues, leading to permanently stuck queues if reactivated.
--

commit 1b05a5559481675536cd1059fcf1bd7e7b6e721c
Author: Jesper Dangaard Brouer <[email protected]>
Subject: veth: implement Byte Queue Limits (BQL) for latency reduction

This commit adds Byte Queue Limits (BQL) support to veth to give qdiscs visibility into the ptr_ring queue depth, reducing base latency. It charges a fixed unit per packet under the producer lock and resets BQL state on NAPI deletion.

Quoted 4 lines
drivers/net/veth.c +0 -0
index 0cfb19b760dd5..86b78900c48e3 100644

[ ... ]

Quoted 17 lines
drivers/net/veth.c +6 -0
@@ -1074,6 +1118,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++) {
@@ -1092,6 +1137,24 @@ static void veth_napi_del_range(struct net_device *dev, int start, int end)
 		ptr_ring_cleanup(&rq->xdp_ring, veth_ptr_free);
 	}
 
+	/* Reset BQL and wake stopped peer txqs.  A concurrent veth_xmit()
+	 * may have set DRV_XOFF between rcu_assign_pointer(napi, NULL) and
+	 * synchronize_net(), and NAPI can no longer clear it.
+	 * Only wake when the device is still up.
+	 */

drivers/net/veth.c static void veth_napi_del_range(struct net_device *dev, int start, int end) · line 1154

Quoted 10 lines
drivers/net/veth.c +10 -0
+	peer = rtnl_dereference(priv->peer);
+	if (peer) {
+		int peer_end = min_t(int, end, peer->real_num_tx_queues);
+
+		for (i = start; i < peer_end; i++) {
+			struct netdev_queue *txq = netdev_get_tx_queue(peer, i);
+
+			netdev_tx_reset_queue(txq);
+			if (netif_running(dev))
+				netif_tx_wake_queue(txq);

Does netdev_tx_reset_queue() clear STACK_XOFF without scheduling the qdisc?

If netif_tx_wake_queue() only schedules the qdisc when DRV_XOFF was set, wouldn't a queue stopped solely by BQL (STACK_XOFF set, DRV_XOFF clear) be stalled indefinitely here?

By checking netif_running(dev), the wakeup is skipped when the local device is brought down. Does this permanently leak the DRV_XOFF state on the peer? If the local device is brought back up later, it appears the peer's TX queue would remain wedged.

Additionally, if a user reduces the peer's active TX queues (e.g., via ethtool), the loop bound peer->real_num_tx_queues will skip deactivated queues.

If those inactive queues had DRV_XOFF set, do they retain that state permanently when reactivated later, since BQL resets on real_num changes but DRV_XOFF does not?

Quoted 5 lines
drivers/net/veth.c +3 -0
+		}
+	}
+
 	for (i = start; i < end; i++) {
 		page_pool_destroy(priv->rq[i].page_pool);
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
#6 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

In reply to [email protected]
Quoted 193 lines · On 5/5/26 15:21, [email protected] wrote:

From: Jesper Dangaard Brouer <[email protected]>

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. Reset BQL state in veth_napi_del_range() after synchronize_net() to avoid racing with in-flight veth_poll() calls. Clamp the reset loop to the peer's real_num_tx_queues, since the peer may have fewer TX queues than the local device has RX queues (e.g. when veth is enslaved to a bond with XDP attached).

---
 drivers/net/veth.c | 86 ++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 75 insertions(+), 11 deletions(-)
drivers/net/veth.c +49 -9
index 0cfb19b760dd..86b78900c48e 100644
@@ -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,7 @@ 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));
 }
 
 static void __veth_xdp_flush(struct veth_rq *rq)
@@ -309,19 +328,33 @@ 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(!ring->size) || __ptr_ring_full(ring)) {
+		spin_unlock(&ring->producer_lock);
 		return NETDEV_TX_BUSY; /* signal qdisc layer */
+	}
+
+	/* BQL charge before produce; consumer cannot see entry yet */
+	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);
 }
 
@@ -348,10 +381,11 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
 {
 	struct veth_priv *rcv_priv, *priv = netdev_priv(dev);
 	struct veth_rq *rq = NULL;
-	struct netdev_queue *txq;
+	struct netdev_queue *txq = NULL;
 	struct net_device *rcv;
 	int length = skb->len;
 	bool use_napi = false;
+	bool do_bql = false;
 	int ret, rxq;
 
 	rcu_read_lock();
@@ -375,8 +409,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 +450,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 +939,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];

drivers/net/veth.c static int veth_xdp_rcv(struct veth_rq *rq, int budget, · line 976

Quoted 10 lines
drivers/net/veth.c +4 -1
@@ -928,9 +968,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);

In the discussion with Jonas [1], I left a comment explaining why I think this doesn’t work.

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Thanks!

[1] Link: https://lore.kernel.org/netdev/[email protected]/

Quoted 54 lines
drivers/net/veth.c +22 -1
+
 			skb = veth_xdp_rcv_skb(rq, skb, bq, stats);
 			if (skb) {
 				if (skb_shared(skb) || skb_unclone(skb, GFP_ATOMIC))
@@ -976,7 +1020,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 +1118,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++) {
@@ -1092,6 +1137,24 @@ static void veth_napi_del_range(struct net_device *dev, int start, int end)
 		ptr_ring_cleanup(&rq->xdp_ring, veth_ptr_free);
 	}
 
+	/* Reset BQL and wake stopped peer txqs.  A concurrent veth_xmit()
+	 * may have set DRV_XOFF between rcu_assign_pointer(napi, NULL) and
+	 * synchronize_net(), and NAPI can no longer clear it.
+	 * Only wake when the device is still up.
+	 */
+	peer = rtnl_dereference(priv->peer);
+	if (peer) {
+		int peer_end = min_t(int, end, peer->real_num_tx_queues);
+
+		for (i = start; i < peer_end; i++) {
+			struct netdev_queue *txq = netdev_get_tx_queue(peer, i);
+
+			netdev_tx_reset_queue(txq);
+			if (netif_running(dev))
+				netif_tx_wake_queue(txq);
+		}
+	}
+
 	for (i = start; i < end; i++) {
 		page_pool_destroy(priv->rq[i].page_pool);
 		priv->rq[i].page_pool = NULL;
@@ -1741,6 +1804,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;
maintainer
#7 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 15 lines · Simon Schippers

drivers/net/veth.c static int veth_xdp_rcv(struct veth_rq *rq, int budget, · line 976

Quoted 11 lines
drivers/net/veth.c +4 -1
@@ -928,9 +968,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);

In the discussion with Jonas [1], I left a comment explaining why I think this doesn’t work.

Quoted 2 lines

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Isn't the veth_poll() (towards the end of the function) a more natural place to issue completion events?

/P

maintainer
#8 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

In reply to Simon Schippers
Quoted 16 lines · Simon Schippers

drivers/net/veth.c static int veth_xdp_rcv(struct veth_rq *rq, int budget, · line 976

Quoted 11 lines · On 5/5/26 15:21, [email protected] wrote:
drivers/net/veth.c +4 -1
@@ -928,9 +968,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);

In the discussion with Jonas [1], I left a comment explaining why I think this doesn’t work.

Quoted 6 lines

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Thanks!

[1] Link: https://lore.kernel.org/netdev/[email protected]/

In the above discussion a 20% regression is reported, which IMHO can't be ignored. Still the tput figures in the data are extremely low, something is possibly off?!? I would expect a few Mpps with pktgen on top of veth, while the reported data is ~20-30Kpps.

/P

#9 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 22 lines · Paolo Abeni
Quoted 21 lines · Simon Schippers

drivers/net/veth.c static int veth_xdp_rcv(struct veth_rq *rq, int budget, · line 976

Quoted 11 lines · On 5/5/26 15:21, [email protected] wrote:
drivers/net/veth.c +4 -1
@@ -928,9 +968,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);

In the discussion with Jonas [1], I left a comment explaining why I think this doesn’t work.

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Thanks!

Quoted 8 lines
Quoted 1 line

[1] Link: https://lore.kernel.org/netdev/[email protected]/

In the above discussion a 20% regression is reported, which IMHO can't be ignored. Still the tput figures in the data are extremely low, something is possibly off?!? I would expect a few Mpps with pktgen on top of veth, while the reported data is ~20-30Kpps.

/P

The ~20-30Kpps occur when thousands of iptables rules are applied and an UDP userspace application is sending.

And there is a 20% pktgen regression (no iptables rules applied).

I am pretty sure the reason is because the BQL limit is stuck at 2 packets (because the completed queue is always called with 1 packet and not in a interrupt/timer with multiple packets...).

#10 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 13 lines · Simon Schippers
Quoted 10 lines · Paolo Abeni
Quoted 9 lines · Simon Schippers
Quoted 8 lines · On 5/5/26 15:21, [email protected] wrote:
drivers/net/veth.c +2 -1
@@ -928,9 +968,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);
 
Quoted 7 lines
Quoted 7 lines
Quoted 7 lines

drivers/net/veth.c static int veth_xdp_rcv(struct veth_rq *rq, int budget, · line 976

Quoted 3 lines
drivers/net/veth.c +2 -0
  			stats->xdp_bytes += skb->len;
+			if (peer_txq && bql_charged)
+				netdev_tx_completed_queue(peer_txq, 1, VETH_BQL_UNIT);

In the discussion with Jonas [1], I left a comment explaining why I think this doesn’t work.

I've experimented with doing the "completion" at NAPI-end in veth_poll(), but that resulted in BQL limit being 128 packets, which leads to bad latency results (not acceptable).
(See detailed report later)

Quoted 3 lines
Quoted 3 lines
Quoted 3 lines

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

The simply solution is the configure BQL limit_min:
/sys/class/net/<dev>/queues/tx-N/byte_queue_limits/limit_min

My experiments (below) find that limit_min=8 is gives good performance. We can simply set default to 8 as this still allows userspace to change this later if lower latency is preferred.

Quoted 9 lines
Quoted 9 lines
Quoted 3 lines

Thanks!

[1] Link: https://lore.kernel.org/netdev/[email protected]/

In the above discussion a 20% regression is reported, which IMHO can't be ignored. Still the tput figures in the data are extremely low, something is possibly off?!? I would expect a few Mpps with pktgen on top of veth, while the reported data is ~20-30Kpps.

Quoted 8 lines
Quoted 2 lines

/P

The ~20-30Kpps occur when thousands of iptables rules are applied and an UDP userspace application is sending.

And there is a 20% pktgen regression (no iptables rules applied).

The pktgen test is a little dubious/weird and Jonas had to modify pktgen
to test this. John Fastabend added a config to pktgen that allows us
to benchmarking egress qdisc path, this might be better to use this. The samples/pktgen/pktgen_bench_xmit_mode_queue_xmit.sh is a demo usage.

If redoing the tests, can you adjust limit_min to see the effect?
/sys/class/net/<dev>/queues/tx-N/byte_queue_limits/limit_min

20% throughput performance regression is of-cause too much, but I will remind us, that adding a qdisc will "cost" some overhead, that is a configuration choice. Our purpose here is to reduce bufferbloat and latency, not optimize for throughput.

Quoted 4 lines

I am pretty sure the reason is because the BQL limit is stuck at 2 packets (because the completed queue is always called with 1 packet and not in a interrupt/timer with multiple packets...).

I've run a lot of experiments, which I made AI write a report over, see attachment. The TL;DR is that best performance vs latency tradeoff is defaulting BQL/DQL limit_min to be 8 packets.

I fear this patchset will stall forever, if we keep searching for a perfect solution without any overhead. The qdisc layer will be a baseline overhead. The limit=2 packets is actually the optimal darkbuffer queue size, but I acknowledge that this causes too many qdisc requeue events (leading to overhead). I suggest that I add another patch in V6, that defaults limit_min to 8 (separate patch to make it easier to revert/adjust later).

I've talked with Jonas, and we want to experiment with different solutions to make BQL/DQL work better with virtual devices.

This patchset helps our (production) use-case reduce mice-flow latency from approx 22ms to 1.3ms for latency under-load. Due to the consumer namespace being the bottleneck the requeue overhead is negligible in comparison.

-Jesper

# PERF-2651: BQL Completion Batching Experiment (2026-05-05)

## Background

Simon Schippers and Jonas Koeppeler raised concerns that DQL settles at limit=2 with veth BQL, citing the netdevice.h comment:

Quoted 2 lines

"Must be called at most once per TX completion round (and not per
individual packet), so that BQL can adjust its limits appropriately."

And Tom Herbert's original BQL cover letter:

Quoted 2 lines

"BQL accounting is in the transmit path for every packet, and the
function to recompute the byte limit is run once per transmit completion."

Thread: https://lore.kernel.org/all/[email protected]/

## Experiment: Batch BQL completion at end of veth_poll

Created stg patch experiment-batch-bql-completion that moves netdev_tx_completed_queue() from per-SKB inside veth_xdp_rcv() to a single batched call at the end of veth_poll().

### Code change (drivers/net/veth.c)

In veth_xdp_rcv(): replace per-SKB completion with counter accumulation:

```c
// Before (V5, per-packet):
if (peer_txq && bql_charged)
    netdev_tx_completed_queue(peer_txq, 1, VETH_BQL_UNIT);

// After (experiment, accumulate):
if (peer_txq && bql_charged)
    stats->bql_completed += VETH_BQL_UNIT;
```

In veth_poll(): single batched call after veth_xdp_rcv() returns:

```c
if (peer_txq && stats.bql_completed)
    netdev_tx_completed_queue(peer_txq, stats.bql_completed,
                              stats.bql_completed);
```

Note: cannot use done (return value of veth_xdp_rcv) because it counts all consumed ring entries including XDP frames that were never BQL-charged. Using done would over-complete and hit BUG_ON in dql_completed().

## Why DQL settles at limit=2 with per-packet completion

The DQL slack calculation in dql_completed() uses:

```c
slack = POSDIFF(limit + prev_ovlimit, 2 * (completed - num_completed));
```

completed - num_completed equals the count parameter (bytes completed this call). Per-packet: count=1, so slack = limit + prev_ovlimit - 2. With limit=2, slack=0, so the algorithm holds steady at 2.

With batched completion: count=~64, slack calculation sees the real batch size, and DQL converges to limit=128 (~2x NAPI budget).

## Results: nrules=3500 (sfq + tiny-flood)

| Metric        | No BQL    | Per-pkt   | limit=4 | limit=8 | limit=16 | Batched     |
|               |           | (limit=2) |         |         |          | (limit=128) |
|---------------|-----------|-----------|---------|---------|----------|-------------|
| BQL limit     | unlimited | 2         | 4       | 8       | 16       | 128         |
| BQL inflight  | 254       | 3         | 5       | 9-17    | 25       | 133         |
| Ping RTT avg  | 9.3ms     | 0.94ms    | 1.07ms  | 1.24ms  | 1.69ms   | 4.0ms       |
| requeues      | 52K       | 454K      | 426K    | 399K    | 356K     | 112K        |
| NAPI avg_work | 63        | 5         | 15      | 63      | 63       | 63          |
| NAPI polls    | ~2.2K     | ~27K      | ~10.5K  | ~2.5K   | ~2.3K    | ~2.5K       |
| Consumer pps  | ~26K      | ~30K      | ~30K    | ~30K    | ~29K     | ~30K        |

## Results: nrules=15000 (sfq + tiny-flood, slower consumer)

| Metric        | No BQL     | Per-pkt   | limit=4   | limit=8   | limit=16  | Batched     |
|               |            | (limit=2) |           |           |           | (limit=128) |
|---------------|------------|-----------|-----------|-----------|-----------|-------------|
| BQL limit     | unlimited  | 2         | 4         | 8         | 16        | 128         |
| BQL inflight  | 211-227    | 3         | 5-12      | 9         | 17        | 132-136     |
| Ping RTT avg  | **37.8ms** | **4.5ms** | **5.0ms** | **6.0ms** | **6.4ms** | **20.0ms**  |
| Ping RTT min  | 27.7ms     | 1.4ms     | 1.7ms     | 2.4ms     | 3.0ms     | 10.4ms      |
| requeues      | 12.9K      | 93K       | 87K       | 80K       | 86K       | 22.8K       |
| NAPI avg_work | 61         | 6         | 17        | 60        | 61        | 61          |
| NAPI polls    | ~540       | ~4.9K     | ~1.9K     | ~540      | ~550      | ~540        |
| Consumer pps  | ~6.7K      | ~6.7K     | ~6.9K     | ~6.8K     | ~7.0K     | ~6.7K       |

## Analysis

### Batched completion is clearly worse for latency

At nrules=15000, batched completion gives 20ms ping RTT -- only 2x better than no-BQL (37.8ms). Per-packet gives 4.5ms -- an 8x improvement.

The math confirms this: 128 packets / 6.7K pps = 19ms of uncontrolled queuing delay. This matches the measured 20ms almost exactly.

### Per-packet completion (limit=2) is correct for veth

Simon's concern that limit=2 is a DQL defect is wrong. limit=2 is the ideal behavior for dark-buffer elimination:
- Only 2-3 packets in the ptr_ring at any time
- Qdisc gets immediate control over all buffering
- 8x latency reduction vs no-BQL

The DQL comment "once per TX completion round" was written for HW NICs where interrupt coalescing batches completions naturally. For veth, each per-SKB completion within a NAPI poll technically violates the letter of the comment, but the resulting limit=2 is correct for the use case.

The concern with limit=2 is the overhead it introduces:

### Trade-off: NAPI polling overhead

Per-packet (limit=2) causes many more NAPI polls:
- nrules=3500: 27K polls (avg_work=5) vs 2.5K polls (avg_work=63)
- nrules=15000: 4.9K polls (avg_work=6) vs 540 polls (avg_work=61)

This is because with only 2-3 items in the ring, each NAPI poll drains the ring quickly -> napi_complete_done -> reschedule. More scheduling overhead, but no throughput impact when consumer is the bottleneck.

### limit_min tuning via sysfs

DQL limit_min can be set via:
/sys/class/net/<dev>/queues/tx-0/byte_queue_limits/limit_min

The selftest --bql-min-limit N flag writes to this sysfs.

- **limit_min=4**: half a cache-line (32 bytes of ptr_ring pointers). avg_work=17, 1.9K polls. Ping 5.0ms -- close to limit=2 (4.5ms).
- **limit_min=8**: one cache-line (64 bytes of ptr_ring pointers). avg_work=60, 540 polls. Ping 6.0ms -- efficient full-budget polls.

### Dark buffer formula

At consumer rate R (pps) and BQL limit L (packets):
- Dark buffer latency = L / R
- limit=2: 2/6700 = 0.3ms (negligible)
- limit=8: 8/6700 = 1.2ms
- limit=128: 128/6700 = 19ms (matches measured 20ms)
- unlimited (254): 254/6700 = 38ms (matches measured 37.8ms)

## Results: nrules=0 (no consumer overhead, max throughput)

This tests the raw throughput overhead of BQL stop/start oscillation. All values are averages of 4 runs (VM noise is ~15-20% per-run variance).

| Metric          | No BQL | limit=2 | limit=4 | limit=8 | limit=16 |
|-----------------|--------|---------|---------|---------|----------|
| Sink pps (large)| 841K   | 759K    | 692K    | 762K    | 736K     |
| Sink pps (small)| 950K   | 874K    | 807K    | 874K    | 844K     |
| qdisc pkts      | 48.6M  | 44.8M   | 40.1M   | 45.0M   | 44.8M    |
| requeues        | 311K   | 6.1M    | 13.4M   | 5.8M    | 5.2M     |
| NAPI avg_work   | 22     | 27      | 12      | 19      | 21       |
| Ping RTT avg    | 0.17ms | 0.11ms  | 0.10ms  | 0.085ms | 0.095ms  |
| Runs            | 4      | 4       | 4       | 4       | 4        |

Observations:
- **limit=2 is NOT the worst** -- limit=4 has higher requeues (13.4M) and lower throughput (692K sink) due to more stop/start cycles at a less efficient NAPI batch size (avg_work=12)
- **limit=8 and limit=16 match No-BQL throughput** within noise (~762K vs 841K sink pps for large pkts, ~3-10% difference)
- **Requeue overhead**: 311K (No BQL) -> 5.2-5.8M (limit=8/16) -> 13.4M (limit=4)
- Latency sub-0.2ms for all settings at this speed -- not a differentiator

## Comparison: limit=8 vs limit=16

Multi-run (4 iterations each, nrules=0) to cut through VM noise:

### limit=8 (4 runs)

| Run | Sink pps (large/small) | qdisc pkts | requeues | avg_work | Ping avg |
|-----|------------------------|-----------|----------|----------|----------|
| 1   | 796K / 911K            | 46.2M     | 5.6M     | 20       | 0.062ms  |
| 2   | 796K / 883K            | 45.5M     | 4.7M     | 16       | 0.081ms  |
| 3   | 654K / 836K            | 43.5M     | 8.3M     | 22       | 0.100ms  |
| 4   | 803K / 865K            | 44.8M     | 4.4M     | 16       | 0.095ms  |
| **avg** | **762K / 874K**    | **45.0M** | **5.8M** | **19**   | **0.085ms** |

### limit=16 (4 runs)

| Run | Sink pps (large/small) | qdisc pkts | requeues | avg_work | Ping avg |
|-----|------------------------|-----------|----------|----------|----------|
| 1   | 844K / 940K            | 48.1M     | 3.3M     | 20       | 0.081ms  |
| 2   | 768K / 873K            | 45.6M     | 4.1M     | 15       | 0.097ms  |
| 3   | 733K / 804K            | 44.8M     | 6.5M     | 26       | 0.085ms  |
| 4   | 597K / 757K            | 40.7M     | 6.9M     | 23       | 0.115ms  |
| **avg** | **736K / 844K**    | **44.8M** | **5.2M** | **21**   | **0.095ms** |

### Averaged comparison (nrules=0, 4 runs)

| Metric              | limit=8   | limit=16  |
|---------------------|-----------|-----------|
| Sink pps (large)    | 762K      | 736K      |
| Sink pps (small)    | 874K      | 844K      |
| qdisc pkts          | 45.0M     | 44.8M     |
| requeues            | 5.8M      | 5.2M      |
| avg_work            | 19        | 21        |
| Ping RTT avg        | 0.085ms   | 0.095ms   |

At max throughput, limit=8 and limit=16 are within VM noise (~3-4%).

### Cross-load comparison (all averages of 4 runs)

| Metric        | limit=8 | limit=16 | Winner        |
|---------------|---------|----------|---------------|
| nrules=15000: |         |          |               |
|   Ping RTT    | 6.73ms  | 8.00ms   | 8 (+1.3ms)    |
|   requeues    | 71K     | 73K      | ~same         |
|   avg_work    | 59      | 59       | ~same         |
| nrules=3500:  |         |          |               |
|   Ping RTT    | 1.77ms  | 2.11ms   | 8 (+0.34ms)   |
|   requeues    | 279K    | 282K     | ~same         |
|   avg_work    | 62      | 62       | ~same         |
| nrules=0:     |         |          |               |
|   Sink pps    | 762K    | 736K     | ~same (noise) |
|   requeues    | 5.8M    | 5.2M     | ~same (noise) |

**Verdict: limit=8 is the better default.**
- Consistent latency advantage under load: +1.3ms at nrules=15000, +0.34ms at nrules=3500 (reproducible across 4 runs each)
- Throughput indistinguishable from limit=16 after averaging
- One cache-line (64 bytes) is a clean hardware alignment
- More conservative -- smaller dark buffer

## Proposed patch: dql_set_min_limit() + veth default min_limit=8

Two-part solution in stg patch veth-set-bql-min-limit-8:

### 1. New DQL API helper (include/linux/dynamic_queue_limits.h)

```c
static inline void dql_set_min_limit(struct dql *dql, unsigned int min_limit)
{
    dql->min_limit = min_limit;
}
```

Gives drivers a clean API to set a default floor. Currently no driver sets min_limit -- all rely on the dql_init() default of 0 or user sysfs.

### 2. Veth sets min_limit=8 at device creation (drivers/net/veth.c)

In veth_init_queues(), after TX queue setup:

```c
#ifdef CONFIG_BQL
    for (i = 0; i < dev->num_tx_queues; i++)
        dql_set_min_limit(&netdev_get_tx_queue(dev, i)->dql,
                          VETH_BQL_UNIT * 8);
#endif
```

Called for both dev and peer in veth_newlink(). Uses num_tx_queues (all pre-allocated queues), not real_num_tx_queues, so channel changes via ethtool -L are covered -- no new queues are ever created at runtime.

### Why min_limit=8

- One cache-line of ptr_ring pointers (8 x 8 = 64 bytes)
- Lowest requeue count at max throughput (5.3M vs 16.9M at limit=2)
- Keeps full-budget NAPI polls (avg_work=63) -- no scheduling overhead
- Latency only 0.3ms worse than limit=2 at moderate load (1.24ms vs 0.94ms)
- Still 6x better latency than no-BQL at heavy load (6ms vs 37.8ms)
- User can lower to 0 or raise via sysfs limit_min at any time

### Verified: driver default works (nrules=15000, --hist)

Tested with veth-set-bql-min-limit-8 patch applied, no --bql-min-limit sysfs override. BQL limit=8 held stable, ping RTT ~6.5ms (matches sysfs override results).

BQL inflight histogram (bpftrace, 169K samples):

```
[1]              15 |                                  |
[2, 4)        21193 |@@@@@@@@@@@@@                     |
[4, 8)        63615 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[8, 16)       80116 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[16, 32)       4709 |@@@                               |
```

- Inflight avg=7, max=17 -- ring stays shallow
- Peak at [8,16): inflight near the limit=8 floor most of the time
- [4,8) second: ring draining between NAPI polls
- [16,32) rare: brief producer bursts
- stack_xoff ~15K/5s, drv_xoff=0 -- BQL stops queue well before ring fills
- NAPI avg_work=61, almost all full-budget polls

## Conclusion

Per-packet BQL completion in V5 is the right design. It gives DQL the information it needs to keep the dark buffer minimal, which is exactly what we want for latency reduction.

Simon's suggestion to call netdev_tx_completed_queue() once per NAPI poll would regress ping latency from 4.5ms to 20ms at production-like iptables rule counts.

The default min_limit=8 (via dql_set_min_limit) is the proposed follow-up to address the requeue overhead that per-packet completion causes. It keeps latency close to optimal while reducing the ~10% throughput loss and 20x requeue increase (6.1M vs 311K) that limit=2 causes at max speed. Users wanting tighter latency can set limit_min=0 via sysfs to get the original limit=2 behavior.

#11 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 84 lines · Jesper Dangaard Brouer
Quoted 5 lines · Simon Schippers
Quoted 2 lines · Paolo Abeni
Quoted 1 line · Simon Schippers

On 5/5/26 15:21, [email protected] wrote:

Quoted 14 lines
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines
drivers/net/veth.c +1 -1
@@ -928,9 +968,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);
Quoted 8 lines
Quoted 8 lines

drivers/net/veth.c static int veth_xdp_rcv(struct veth_rq *rq, int budget, · line 976

Quoted 4 lines
drivers/net/veth.c +3 -0
+            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);

In the discussion with Jonas [1], I left a comment explaining why I think this doesn’t work.

I've experimented with doing the "completion" at NAPI-end in veth_poll(), but that resulted in BQL limit being 128 packets, which leads to bad latency results (not acceptable).
(See detailed report later)

Quoted 3 lines
Quoted 3 lines
Quoted 3 lines

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

The simply solution is the configure BQL limit_min:
/sys/class/net/<dev>/queues/tx-N/byte_queue_limits/limit_min

My experiments (below) find that limit_min=8 is gives good performance. We can simply set default to 8 as this still allows userspace to change this later if lower latency is preferred.

Quoted 9 lines
Quoted 9 lines
Quoted 3 lines

Thanks!

[1] Link: https://lore.kernel.org/netdev/[email protected]/

In the above discussion a 20% regression is reported, which IMHO can't be ignored. Still the tput figures in the data are extremely low, something is possibly off?!? I would expect a few Mpps with pktgen on top of veth, while the reported data is ~20-30Kpps.

Quoted 8 lines
Quoted 2 lines

/P

The ~20-30Kpps occur when thousands of iptables rules are applied and an UDP userspace application is sending.

And there is a 20% pktgen regression (no iptables rules applied).

The pktgen test is a little dubious/weird and Jonas had to modify pktgen
to test this. John Fastabend added a config to pktgen that allows us
to benchmarking egress qdisc path, this might be better to use this. The samples/pktgen/pktgen_bench_xmit_mode_queue_xmit.sh is a demo usage.

If redoing the tests, can you adjust limit_min to see the effect?
/sys/class/net/<dev>/queues/tx-N/byte_queue_limits/limit_min

20% throughput performance regression is of-cause too much, but I will remind us, that adding a qdisc will "cost" some overhead, that is a configuration choice. Our purpose here is to reduce bufferbloat and latency, not optimize for throughput.

Quoted 4 lines

I am pretty sure the reason is because the BQL limit is stuck at 2 packets (because the completed queue is always called with 1 packet and not in a interrupt/timer with multiple packets...).

I've run a lot of experiments, which I made AI write a report over, see attachment. The TL;DR is that best performance vs latency tradeoff is defaulting BQL/DQL limit_min to be 8 packets.

I fear this patchset will stall forever, if we keep searching for a perfect solution without any overhead. The qdisc layer will be a baseline overhead. The limit=2 packets is actually the optimal darkbuffer queue size, but I acknowledge that this causes too many qdisc requeue events (leading to overhead). I suggest that I add another patch in V6, that defaults limit_min to 8 (separate patch to make it easier to revert/adjust later).

Quoted 8 lines

I've talked with Jonas, and we want to experiment with different solutions to make BQL/DQL work better with virtual devices.

This patchset helps our (production) use-case reduce mice-flow latency from approx 22ms to 1.3ms for latency under-load. Due to the consumer namespace being the bottleneck the requeue overhead is negligible in comparison.

-Jesper

First of all thanks for you work and I really see the advantages of avoiding bufferbloat :)

But the key of the BQL algorithm, which is the *dynamic* adaption of the limit, is not working. Always calling netdev_completed_queue() with 1 packet results in a static limit of 2 packets (as seen by Jonas measurements), which you force up to 8 packets.

So in the end this patchset has the same effect as just setting VETH_RING_SIZE to 8 (and giving an option to change this value).

#12 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 97 lines · Simon Schippers
Quoted 84 lines · Jesper Dangaard Brouer
Quoted 5 lines · Simon Schippers
Quoted 2 lines · Paolo Abeni
Quoted 1 line · Simon Schippers

On 5/5/26 15:21, [email protected] wrote:

Quoted 14 lines
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines
(quoted diff) +1 -1
@@ -928,9 +968,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);
Quoted 8 lines
Quoted 8 lines

Review static int veth_xdp_rcv(struct veth_rq *rq, int budget, · line 975

Quoted 4 lines
(quoted diff) +3 -0
+            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);

In the discussion with Jonas [1], I left a comment explaining why I think this doesn’t work.

I've experimented with doing the "completion" at NAPI-end in veth_poll(), but that resulted in BQL limit being 128 packets, which leads to bad latency results (not acceptable).
(See detailed report later)

Quoted 3 lines
Quoted 3 lines
Quoted 3 lines

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

The simply solution is the configure BQL limit_min:
/sys/class/net/<dev>/queues/tx-N/byte_queue_limits/limit_min

My experiments (below) find that limit_min=8 is gives good performance. We can simply set default to 8 as this still allows userspace to change this later if lower latency is preferred.

Quoted 9 lines
Quoted 9 lines
Quoted 3 lines

Thanks!

[1] Link: https://lore.kernel.org/netdev/[email protected]/

In the above discussion a 20% regression is reported, which IMHO can't be ignored. Still the tput figures in the data are extremely low, something is possibly off?!? I would expect a few Mpps with pktgen on top of veth, while the reported data is ~20-30Kpps.

Quoted 8 lines
Quoted 2 lines

/P

The ~20-30Kpps occur when thousands of iptables rules are applied and an UDP userspace application is sending.

And there is a 20% pktgen regression (no iptables rules applied).

The pktgen test is a little dubious/weird and Jonas had to modify pktgen
to test this. John Fastabend added a config to pktgen that allows us
to benchmarking egress qdisc path, this might be better to use this. The samples/pktgen/pktgen_bench_xmit_mode_queue_xmit.sh is a demo usage.

If redoing the tests, can you adjust limit_min to see the effect?
/sys/class/net/<dev>/queues/tx-N/byte_queue_limits/limit_min

20% throughput performance regression is of-cause too much, but I will remind us, that adding a qdisc will "cost" some overhead, that is a configuration choice. Our purpose here is to reduce bufferbloat and latency, not optimize for throughput.

Quoted 4 lines

I am pretty sure the reason is because the BQL limit is stuck at 2 packets (because the completed queue is always called with 1 packet and not in a interrupt/timer with multiple packets...).

I've run a lot of experiments, which I made AI write a report over, see attachment. The TL;DR is that best performance vs latency tradeoff is defaulting BQL/DQL limit_min to be 8 packets.

I fear this patchset will stall forever, if we keep searching for a perfect solution without any overhead. The qdisc layer will be a baseline overhead. The limit=2 packets is actually the optimal darkbuffer queue size, but I acknowledge that this causes too many qdisc requeue events (leading to overhead). I suggest that I add another patch in V6, that defaults limit_min to 8 (separate patch to make it easier to revert/adjust later).

Quoted 8 lines

I've talked with Jonas, and we want to experiment with different solutions to make BQL/DQL work better with virtual devices.

This patchset helps our (production) use-case reduce mice-flow latency from approx 22ms to 1.3ms for latency under-load. Due to the consumer namespace being the bottleneck the requeue overhead is negligible in comparison.

-Jesper

First of all thanks for you work and I really see the advantages of avoiding bufferbloat :)

Quoted 8 lines

But the key of the BQL algorithm, which is the *dynamic* adaption of the limit, is not working. Always calling netdev_completed_queue() with 1 packet results in a static limit of 2 packets (as seen by Jonas measurements), which you force up to 8 packets.

So in the end this patchset has the same effect as just setting VETH_RING_SIZE to 8 (and giving an option to change this value).

I've code up a time based BQL implementation, see attachment. WDYT?

--Jesper

veth: time-based BQL completion coalescing via ethtool tx-usecs

From: Jesper Dangaard Brouer <[email protected]>

Bufferbloat is fundamentally a latency problem -- what matters is the time packets spend waiting in queues, as perceived by users and applications. Base BQL completion coalescing on elapsed time rather than packet counts to directly control queuing delay.

Add ethtool tx-usecs support to veth for tuning BQL completion coalescing. Instead of completing BQL per-packet (which forces DQL to limit=2 with high NAPI scheduling overhead) or per-NAPI-poll (which over-buffers at budget=64), accumulate completions and flush them when a configurable time threshold is exceeded. This lets DQL discover a limit that bounds the actual queuing delay to the configured interval.

The default of 10 usecs (VETH_BQL_COAL_TX_USECS) provides a good
balance: DQL converges to a small limit that keeps queuing delay bounded while allowing efficient NAPI batch processing. Setting tx-usecs to 0 disables coalescing and falls back to per-packet completion (limit=2, lowest latency, highest NAPI overhead).

Usage:
ethtool -C <veth-dev> tx-usecs 500 # 500us coalescing
ethtool -C <veth-dev> tx-usecs 0 # per-packet (no coalescing)

Uses local_clock() (rdtsc on x86, ~20ns) for sub-jiffy resolution.

---
 drivers/net/veth.c |   59 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 57 insertions(+), 2 deletions(-)
drivers/net/veth.c +57 -2
index 4103d298aa9b..6035f7ec92b4 100644
@@ -43,6 +43,7 @@
 
 #define VETH_XDP_TX_BULK_SIZE	16
 #define VETH_XDP_BATCH		16
+#define VETH_BQL_COAL_TX_USECS	10	/* default tx-usecs for BQL batching */
 
 struct veth_stats {
 	u64	rx_drops;
@@ -81,6 +82,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 {
@@ -265,7 +267,30 @@ 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);
+
+	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,
@@ -275,6 +300,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 */
@@ -942,8 +969,14 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget,
 			struct veth_stats *stats,
 			struct netdev_queue *peer_txq)
 {
+	u64 bql_flush_ns, bql_flush_time = 0;
 	int i, done = 0, n_xdpf = 0;
 	void *xdpf[VETH_XDP_BATCH];
+	struct veth_priv *priv;
+	int n_bql = 0;
+
+	priv = netdev_priv(rq->dev);
+	bql_flush_ns = (u64)priv->tx_coal_usecs * 1000;
 
 	for (i = 0; i < budget; i++) {
 		void *ptr = __ptr_ring_consume(&rq->xdp_ring);
@@ -972,8 +1005,23 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget,
 			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);
+			if (peer_txq && bql_charged) {
+				if (!bql_flush_ns) {
+					netdev_tx_completed_queue(peer_txq, 1,
+								  VETH_BQL_UNIT);
+				} else {
+					u64 now = local_clock();
+					n_bql++;
+					if (!bql_flush_time) {
+						bql_flush_time = now;
+					} else if (now - bql_flush_time >= bql_flush_ns) {
+						netdev_tx_completed_queue(peer_txq, n_bql,
+									  n_bql * VETH_BQL_UNIT);
+						n_bql = 0;
+						bql_flush_time = 0;
+					}
+				}
+			}
 
 			skb = veth_xdp_rcv_skb(rq, skb, bq, stats);
 			if (skb) {
@@ -989,6 +1037,9 @@ 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 (peer_txq && n_bql)
+		netdev_tx_completed_queue(peer_txq, n_bql, n_bql * VETH_BQL_UNIT);
+
 	u64_stats_update_begin(&rq->stats.syncp);
 	rq->stats.vs.xdp_redirect += stats->xdp_redirect;
 	rq->stats.vs.xdp_bytes += stats->xdp_bytes;
@@ -1813,6 +1864,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;
@@ -1838,6 +1891,8 @@ static void veth_setup(struct net_device *dev)
 	dev->max_mtu = ETH_MAX_MTU;
 	dev->watchdog_timeo = msecs_to_jiffies(16000);
 
+	priv->tx_coal_usecs = VETH_BQL_COAL_TX_USECS;
+
 	dev->hw_features = VETH_FEATURES;
 	dev->hw_enc_features = VETH_FEATURES;
 	dev->mpls_features = NETIF_F_HW_CSUM | NETIF_F_GSO_SOFTWARE;
#13 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 105 lines · Jesper Dangaard Brouer
Quoted 102 lines · Simon Schippers
Quoted 84 lines · Jesper Dangaard Brouer
Quoted 5 lines · Simon Schippers
Quoted 2 lines · Paolo Abeni
Quoted 1 line · Simon Schippers

On 5/5/26 15:21, [email protected] wrote:

Quoted 14 lines
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines
(quoted diff) +1 -1
@@ -928,9 +968,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);
Quoted 8 lines
Quoted 8 lines

Review static int veth_xdp_rcv(struct veth_rq *rq, int budget, · line 975

Quoted 4 lines
(quoted diff) +3 -0
+            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);

In the discussion with Jonas [1], I left a comment explaining why I think this doesn’t work.

I've experimented with doing the "completion" at NAPI-end in veth_poll(), but that resulted in BQL limit being 128 packets, which leads to bad latency results (not acceptable).
(See detailed report later)

Quoted 3 lines
Quoted 3 lines
Quoted 3 lines

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

The simply solution is the configure BQL limit_min:
/sys/class/net/<dev>/queues/tx-N/byte_queue_limits/limit_min

My experiments (below) find that limit_min=8 is gives good performance. We can simply set default to 8 as this still allows userspace to change this later if lower latency is preferred.

Quoted 9 lines
Quoted 9 lines
Quoted 3 lines

Thanks!

[1] Link: https://lore.kernel.org/netdev/[email protected]/

In the above discussion a 20% regression is reported, which IMHO can't be ignored. Still the tput figures in the data are extremely low, something is possibly off?!? I would expect a few Mpps with pktgen on top of veth, while the reported data is ~20-30Kpps.

Quoted 8 lines
Quoted 2 lines

/P

The ~20-30Kpps occur when thousands of iptables rules are applied and an UDP userspace application is sending.

And there is a 20% pktgen regression (no iptables rules applied).

The pktgen test is a little dubious/weird and Jonas had to modify pktgen
to test this. John Fastabend added a config to pktgen that allows us
to benchmarking egress qdisc path, this might be better to use this. The samples/pktgen/pktgen_bench_xmit_mode_queue_xmit.sh is a demo usage.

If redoing the tests, can you adjust limit_min to see the effect?
/sys/class/net/<dev>/queues/tx-N/byte_queue_limits/limit_min

20% throughput performance regression is of-cause too much, but I will remind us, that adding a qdisc will "cost" some overhead, that is a configuration choice. Our purpose here is to reduce bufferbloat and latency, not optimize for throughput.

Quoted 4 lines

I am pretty sure the reason is because the BQL limit is stuck at 2 packets (because the completed queue is always called with 1 packet and not in a interrupt/timer with multiple packets...).

I've run a lot of experiments, which I made AI write a report over, see attachment. The TL;DR is that best performance vs latency tradeoff is defaulting BQL/DQL limit_min to be 8 packets.

I fear this patchset will stall forever, if we keep searching for a perfect solution without any overhead. The qdisc layer will be a baseline overhead. The limit=2 packets is actually the optimal darkbuffer queue size, but I acknowledge that this causes too many qdisc requeue events (leading to overhead). I suggest that I add another patch in V6, that defaults limit_min to 8 (separate patch to make it easier to revert/adjust later).

Quoted 8 lines

I've talked with Jonas, and we want to experiment with different solutions to make BQL/DQL work better with virtual devices.

This patchset helps our (production) use-case reduce mice-flow latency from approx 22ms to 1.3ms for latency under-load. Due to the consumer namespace being the bottleneck the requeue overhead is negligible in comparison.

-Jesper

First of all thanks for you work and I really see the advantages of avoiding bufferbloat :)

But the key of the BQL algorithm, which is the *dynamic* adaption of the limit, is not working. Always calling netdev_completed_queue() with 1 packet results in a static limit of 2 packets (as seen by Jonas measurements), which you force up to 8 packets.

Quoted 9 lines
Quoted 3 lines

So in the end this patchset has the same effect as just setting VETH_RING_SIZE to 8 (and giving an option to change this value).

I've code up a time based BQL implementation, see attachment. WDYT?

--Jesper

A step in the right direction, but I dislike that you call netdev_sent_queue() with at least 1 packet (never 0 packets). I am not sure if it works, and I am not sure about the parameter.

I would propose doing it like other BQL implementations do (for example usbnet for which I adapted BQL [1] :) ):

Call netdev_sent_queue() with n_bql in a periodic work. n_bql would still be counted in veth_xdp_rcv() like you currently do (synchronized with the work via ring.consumer_lock?).

The only weird thing that remains is that BQL's inflight != number of packets in the ring and BQL's limit != "current ring size". Instead the BQL limit describes the number of maximal allowed packets between calls of netdev_sent_queue(), which occur periodically in a somewhat fixed time interval.
I guess that could be fine, but it surely needs testing.

[1] Link: https://lore.kernel.org/netdev/[email protected]/

#14 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 113 lines · Simon Schippers
Quoted 112 lines · Jesper Dangaard Brouer
Quoted 97 lines · Simon Schippers
Quoted 84 lines · Jesper Dangaard Brouer
Quoted 5 lines · Simon Schippers
Quoted 2 lines · Paolo Abeni
Quoted 1 line · Simon Schippers

On 5/5/26 15:21, [email protected] wrote:

Quoted 14 lines
Quoted 6 lines
Quoted 6 lines
Quoted 6 lines
(quoted diff) +1 -1
@@ -928,9 +968,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);
Quoted 8 lines
Quoted 8 lines

Review static int veth_xdp_rcv(struct veth_rq *rq, int budget, · line 975

Quoted 4 lines
(quoted diff) +3 -0
+            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);

In the discussion with Jonas [1], I left a comment explaining why I think this doesn’t work.

I've experimented with doing the "completion" at NAPI-end in veth_poll(), but that resulted in BQL limit being 128 packets, which leads to bad latency results (not acceptable).
(See detailed report later)

Quoted 3 lines
Quoted 3 lines
Quoted 3 lines

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

The simply solution is the configure BQL limit_min:
/sys/class/net/<dev>/queues/tx-N/byte_queue_limits/limit_min

My experiments (below) find that limit_min=8 is gives good performance. We can simply set default to 8 as this still allows userspace to change this later if lower latency is preferred.

Quoted 9 lines
Quoted 9 lines
Quoted 3 lines

Thanks!

[1] Link: https://lore.kernel.org/netdev/[email protected]/

In the above discussion a 20% regression is reported, which IMHO can't be ignored. Still the tput figures in the data are extremely low, something is possibly off?!? I would expect a few Mpps with pktgen on top of veth, while the reported data is ~20-30Kpps.

Quoted 8 lines
Quoted 2 lines

/P

The ~20-30Kpps occur when thousands of iptables rules are applied and an UDP userspace application is sending.

And there is a 20% pktgen regression (no iptables rules applied).

The pktgen test is a little dubious/weird and Jonas had to modify pktgen
to test this. John Fastabend added a config to pktgen that allows us
to benchmarking egress qdisc path, this might be better to use this. The samples/pktgen/pktgen_bench_xmit_mode_queue_xmit.sh is a demo usage.

If redoing the tests, can you adjust limit_min to see the effect?
/sys/class/net/<dev>/queues/tx-N/byte_queue_limits/limit_min

20% throughput performance regression is of-cause too much, but I will remind us, that adding a qdisc will "cost" some overhead, that is a configuration choice. Our purpose here is to reduce bufferbloat and latency, not optimize for throughput.

Quoted 4 lines

I am pretty sure the reason is because the BQL limit is stuck at 2 packets (because the completed queue is always called with 1 packet and not in a interrupt/timer with multiple packets...).

I've run a lot of experiments, which I made AI write a report over, see attachment. The TL;DR is that best performance vs latency tradeoff is defaulting BQL/DQL limit_min to be 8 packets.

I fear this patchset will stall forever, if we keep searching for a perfect solution without any overhead. The qdisc layer will be a baseline overhead. The limit=2 packets is actually the optimal darkbuffer queue size, but I acknowledge that this causes too many qdisc requeue events (leading to overhead). I suggest that I add another patch in V6, that defaults limit_min to 8 (separate patch to make it easier to revert/adjust later).

Quoted 8 lines

I've talked with Jonas, and we want to experiment with different solutions to make BQL/DQL work better with virtual devices.

This patchset helps our (production) use-case reduce mice-flow latency from approx 22ms to 1.3ms for latency under-load. Due to the consumer namespace being the bottleneck the requeue overhead is negligible in comparison.

-Jesper

First of all thanks for you work and I really see the advantages of avoiding bufferbloat :)

Quoted 8 lines

But the key of the BQL algorithm, which is the *dynamic* adaption of the limit, is not working. Always calling netdev_completed_queue() with 1 packet results in a static limit of 2 packets (as seen by Jonas measurements), which you force up to 8 packets.

So in the end this patchset has the same effect as just setting VETH_RING_SIZE to 8 (and giving an option to change this value).

I've code up a time based BQL implementation, see attachment. WDYT?

Quoted 7 lines
Quoted 2 lines

--Jesper

A step in the right direction, but I dislike that you call netdev_sent_queue() with at least 1 packet (never 0 packets). I am not sure if it works, and I am not sure about the parameter.

Rethinking of it this could be fine, but really needs testing because:

The weird thing is that is that BQL's inflight != number of packets in the ring and BQL's limit != "current ring size". Instead the BQL limit describes the number of maximal allowed packets between calls of netdev_sent_queue().

I messed up in my approach below. Forget it :P

Quoted 17 lines

I would propose doing it like other BQL implementations do (for example usbnet for which I adapted BQL [1] :) ):

Call netdev_sent_queue() with n_bql in a periodic work. n_bql would still be counted in veth_xdp_rcv() like you currently do (synchronized with the work via ring.consumer_lock?).

The only weird thing that remains is that BQL's inflight != number of packets in the ring and BQL's limit != "current ring size". Instead the BQL limit describes the number of maximal allowed packets between calls of netdev_sent_queue(), which occur periodically in a somewhat fixed time interval.
I guess that could be fine, but it surely needs testing.

[1] Link: https://lore.kernel.org/netdev/[email protected]/

maintainer
#15 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

In reply to Jesper Dangaard Brouer
Quoted 5 lines · On Thu, 7 May 2026 21:09:09 +0200 Jesper Dangaard Brouer wrote:
Quoted 2 lines
Quoted 2 lines
Quoted 2 lines

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

Was it evaluated, tho?

It's obviously super easy these days have AI spew no end of complex code. So it'd be great to have some solid, ideally production-like data to back this all up.

VETH_RING_SIZE seems trivial, ethtool set ringparam

#16 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 7 lines · Jakub Kicinski
Quoted 5 lines · On Thu, 7 May 2026 21:09:09 +0200 Jesper Dangaard Brouer wrote:
Quoted 2 lines
Quoted 2 lines
Quoted 2 lines

I still think first that adding an option to modify the hard-coded VETH_RING_SIZE is the way to go.

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

Quoted 7 lines

Was it evaluated, tho?

It's obviously super easy these days have AI spew no end of complex code. So it'd be great to have some solid, ideally production-like data to back this all up.

VETH_RING_SIZE seems trivial, ethtool set ringparam

No, unfortunately we cannot just decrease the VETH_RING_SIZE. The reason is that XDP-redirect into veth don't have any back-pressure and would simply drop packets if queue size becomes less than the NAPI budget (64). (Yes, we use both normal path and XDP-redirect in production). My benchmarking shows that an optimal BQL limit is dynamically adjusted between 17-55 depending on veth consumer namespace overhead/speed, when balancing throughput and latency.

--Jesper

maintainer
#17 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 7 lines · On Sat, 9 May 2026 11:09:51 +0200 Jesper Dangaard Brouer wrote:
Quoted 6 lines · Jakub Kicinski
Quoted 2 lines · On Thu, 7 May 2026 21:09:09 +0200 Jesper Dangaard Brouer wrote:

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

Was it evaluated, tho?

Quoted 7 lines
Quoted 5 lines

It's obviously super easy these days have AI spew no end of complex code. So it'd be great to have some solid, ideally production-like data to back this all up.

VETH_RING_SIZE seems trivial, ethtool set ringparam

No, unfortunately we cannot just decrease the VETH_RING_SIZE.

To be clear - I said may it configurable with ethtool -G not change the default.

Quoted 4 lines

The reason is that XDP-redirect into veth don't have any back-pressure and would simply drop packets if queue size becomes less than the NAPI budget (64). (Yes, we use both normal path and XDP-redirect in production).

Doesn't this mean you have a queue which is not under BQL control?

Quoted 3 lines

My benchmarking shows that an optimal BQL limit is dynamically adjusted between 17-55 depending on veth consumer namespace overhead/speed, when balancing throughput and latency.

Testing with prod-approximating traffic pattern and load would be great.

#18 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 12 lines · Jakub Kicinski
Quoted 11 lines · On Sat, 9 May 2026 11:09:51 +0200 Jesper Dangaard Brouer wrote:
Quoted 10 lines · Jakub Kicinski
Quoted 2 lines · On Thu, 7 May 2026 21:09:09 +0200 Jesper Dangaard Brouer wrote:

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

Was it evaluated, tho?

It's obviously super easy these days have AI spew no end of complex code. So it'd be great to have some solid, ideally production-like data to back this all up.

Quoted 7 lines
Quoted 3 lines
Quoted 1 line

VETH_RING_SIZE seems trivial, ethtool set ringparam

No, unfortunately we cannot just decrease the VETH_RING_SIZE.

To be clear - I said may it configurable with ethtool -G not change the default.

Sure, I understand the desire to make VETH_RING_SIZE configurable. If doing so we are making Linux network stack harder to tune and setup correctly. E.g. adding a qdisc to veth would also require changing the ring size, but if system also uses XDP then tuning below 64 (likely 128) will lead to hard-to-find packet drops.

I prefer adding something (like BQL) that auto-tune how much of the ring queue we are using. Good queues function as shock absorbers when concurrent processes in the OS have scheduling noise.

I acknowledge that Simon Schippers found that the BQL implementation was actually not auto-tuning. We need to work on this, my prototype implementation [1] [2] works surprisingly well.

- [1] https://lore.kernel.org/all/[email protected]/2-09-veth-time-based-bql-coalescing.patch
- [2] https://lore.kernel.org/all/[email protected]/

Quoted 7 lines
Quoted 4 lines

The reason is that XDP-redirect into veth don't have any back-pressure and would simply drop packets if queue size becomes less than the NAPI budget (64). (Yes, we use both normal path and XDP-redirect in production).

Doesn't this mean you have a queue which is not under BQL control?

It is a matter of perspective. BQL needs between 17-55 elements in the 256 queue. At the same time we handle if the ring runs full, e.g. due to a sudden burst of XDP redirected packets, which pushes packets into the qdisc layer.

Quoted 5 lines
Quoted 3 lines

My benchmarking shows that an optimal BQL limit is dynamically adjusted between 17-55 depending on veth consumer namespace overhead/speed, when balancing throughput and latency.

Testing with prod-approximating traffic pattern and load would be great.

That is what I'm doing. I'm testing with prod-approximating traffic pattern and changing the number of iptables rules to simulate the overhead I measured from production. I think I explained this in the cover letter. We are going to use this in a production environment (to be clear).

Simon found an issue testing the overload scenario.

--Jesper

#19 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 23 lines · Jesper Dangaard Brouer
Quoted 12 lines · Jakub Kicinski
Quoted 11 lines · On Sat, 9 May 2026 11:09:51 +0200 Jesper Dangaard Brouer wrote:
Quoted 10 lines · Jakub Kicinski
Quoted 2 lines · On Thu, 7 May 2026 21:09:09 +0200 Jesper Dangaard Brouer wrote:

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

Was it evaluated, tho?

It's obviously super easy these days have AI spew no end of complex code. So it'd be great to have some solid, ideally production-like data to back this all up.

Quoted 7 lines
Quoted 3 lines
Quoted 1 line

VETH_RING_SIZE seems trivial, ethtool set ringparam

No, unfortunately we cannot just decrease the VETH_RING_SIZE.

To be clear - I said may it configurable with ethtool -G not change the default.

Quoted 5 lines

Sure, I understand the desire to make VETH_RING_SIZE configurable. If doing so we are making Linux network stack harder to tune and setup correctly. E.g. adding a qdisc to veth would also require changing the ring size, but if system also uses XDP then tuning below 64 (likely 128) will lead to hard-to-find packet drops.

I mean 64 still could be a 4x improvement at least.

Quoted 19 lines

I prefer adding something (like BQL) that auto-tune how much of the ring queue we are using. Good queues function as shock absorbers when concurrent processes in the OS have scheduling noise.

I acknowledge that Simon Schippers found that the BQL implementation was actually not auto-tuning. We need to work on this, my prototype implementation [1] [2] works surprisingly well.

- [1] https://lore.kernel.org/all/[email protected]/2-09-veth-time-based-bql-coalescing.patch
- [2] https://lore.kernel.org/all/[email protected]/

Quoted 5 lines
Quoted 4 lines

The reason is that XDP-redirect into veth don't have any back-pressure and would simply drop packets if queue size becomes less than the NAPI budget (64). (Yes, we use both normal path and XDP-redirect in production).

Quoted 7 lines
Quoted 2 lines

Doesn't this mean you have a queue which is not under BQL control?

It is a matter of perspective. BQL needs between 17-55 elements in the 256 queue. At the same time we handle if the ring runs full, e.g. due to a sudden burst of XDP redirected packets, which pushes packets into the qdisc layer.

You are checking inflight/limit in /sys directory to get the 17-55 number, right?

I think those elements are not really in the queue.

As written before:
The weird thing in this implementation is that is that BQL's inflight != number of packets in the ring and BQL's limit != "current ring size". Instead the BQL limit describes the number of maximal allowed packets between calls of netdev_sent_queue().
And in our case here we do not complete (in our case forward) the packets when calling netdev_sent_queue() but instead immediately and therefore they are not in the queue anymore when netdev_sent_queue() is called.

Also that means that the number strongly depends on the VETH_BQL_COAL_TX_USECS parameter.
For a fixed PPS the limit should be approx.:

Limit = VETH_BQL_COAL_TX_USECS * PPS

Assuming the default 10us coal and a fixed 1 MPPS:

Limit = 0.00001s * 1_000_000 = 10 packet

Can you follow my theory?

Judging from that I personally think VETH_BQL_COAL_TX_USECS needs to bigger. More like 100us/1ms. With 10us the bql limit gets adjusted very often I think..

Thanks.

Quoted 18 lines
Quoted 7 lines
Quoted 5 lines

My benchmarking shows that an optimal BQL limit is dynamically adjusted between 17-55 depending on veth consumer namespace overhead/speed, when balancing throughput and latency.

Testing with prod-approximating traffic pattern and load would be great.

That is what I'm doing. I'm testing with prod-approximating traffic pattern and changing the number of iptables rules to simulate the overhead I measured from production. I think I explained this in the cover letter. We are going to use this in a production environment (to be clear).

Simon found an issue testing the overload scenario.

--Jesper

#20 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 24 lines · Simon Schippers
Quoted 23 lines · Jesper Dangaard Brouer
Quoted 19 lines · Jakub Kicinski
Quoted 7 lines · On Sat, 9 May 2026 11:09:51 +0200 Jesper Dangaard Brouer wrote:
Quoted 6 lines · Jakub Kicinski
Quoted 2 lines · On Thu, 7 May 2026 21:09:09 +0200 Jesper Dangaard Brouer wrote:

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

Was it evaluated, tho?

Quoted 7 lines
Quoted 5 lines

It's obviously super easy these days have AI spew no end of complex code. So it'd be great to have some solid, ideally production-like data to back this all up.

VETH_RING_SIZE seems trivial, ethtool set ringparam

No, unfortunately we cannot just decrease the VETH_RING_SIZE.

To be clear - I said may it configurable with ethtool -G not change the default.

Quoted 8 lines
Quoted 5 lines

Sure, I understand the desire to make VETH_RING_SIZE configurable. If doing so we are making Linux network stack harder to tune and setup correctly. E.g. adding a qdisc to veth would also require changing the ring size, but if system also uses XDP then tuning below 64 (likely 128) will lead to hard-to-find packet drops.

I mean 64 still could be a 4x improvement at least.

No not really, setting it to 64 will give same (bad) latency from "BQL off" which that patchset is trying to address.

Quoted 22 lines
Quoted 22 lines

I prefer adding something (like BQL) that auto-tune how much of the ring queue we are using. Good queues function as shock absorbers when concurrent processes in the OS have scheduling noise.

I acknowledge that Simon Schippers found that the BQL implementation was actually not auto-tuning. We need to work on this, my prototype implementation [1] [2] works surprisingly well.

- [1] https://lore.kernel.org/all/[email protected]/2-09-veth-time-based-bql-coalescing.patch
- [2] https://lore.kernel.org/all/[email protected]/

Quoted 7 lines
Quoted 4 lines

The reason is that XDP-redirect into veth don't have any back-pressure and would simply drop packets if queue size becomes less than the NAPI budget (64). (Yes, we use both normal path and XDP-redirect in production).

Doesn't this mean you have a queue which is not under BQL control?

Quoted 8 lines
Quoted 4 lines

It is a matter of perspective. BQL needs between 17-55 elements in the 256 queue. At the same time we handle if the ring runs full, e.g. due to a sudden burst of XDP redirected packets, which pushes packets into the qdisc layer.

You are checking inflight/limit in /sys directory to get the 17-55 number, right?

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1]
https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2]
https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Sorry for cutting the remaining of the message, but I ran out of time, as things are a bit challenging/hectic here at Cloudflare at the moment.

--Jesper

#21 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 66 lines · Jesper Dangaard Brouer
Quoted 24 lines · Simon Schippers
Quoted 23 lines · Jesper Dangaard Brouer
Quoted 19 lines · Jakub Kicinski
Quoted 7 lines · On Sat, 9 May 2026 11:09:51 +0200 Jesper Dangaard Brouer wrote:
Quoted 6 lines · Jakub Kicinski
Quoted 2 lines · On Thu, 7 May 2026 21:09:09 +0200 Jesper Dangaard Brouer wrote:

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

Was it evaluated, tho?

Quoted 7 lines
Quoted 5 lines

It's obviously super easy these days have AI spew no end of complex code. So it'd be great to have some solid, ideally production-like data to back this all up.

VETH_RING_SIZE seems trivial, ethtool set ringparam

No, unfortunately we cannot just decrease the VETH_RING_SIZE.

To be clear - I said may it configurable with ethtool -G not change the default.

Quoted 8 lines
Quoted 5 lines

Sure, I understand the desire to make VETH_RING_SIZE configurable. If doing so we are making Linux network stack harder to tune and setup correctly. E.g. adding a qdisc to veth would also require changing the ring size, but if system also uses XDP then tuning below 64 (likely 128) will lead to hard-to-find packet drops.

I mean 64 still could be a 4x improvement at least.

No not really, setting it to 64 will give same (bad) latency from "BQL off" which that patchset is trying to address.

Quoted 27 lines
Quoted 26 lines

I prefer adding something (like BQL) that auto-tune how much of the ring queue we are using. Good queues function as shock absorbers when concurrent processes in the OS have scheduling noise.

I acknowledge that Simon Schippers found that the BQL implementation was actually not auto-tuning. We need to work on this, my prototype implementation [1] [2] works surprisingly well.

- [1] https://lore.kernel.org/all/[email protected]/2-09-veth-time-based-bql-coalescing.patch
- [2] https://lore.kernel.org/all/[email protected]/

Quoted 7 lines
Quoted 4 lines

The reason is that XDP-redirect into veth don't have any back-pressure and would simply drop packets if queue size becomes less than the NAPI budget (64). (Yes, we use both normal path and XDP-redirect in production).

Doesn't this mean you have a queue which is not under BQL control?

It is a matter of perspective. BQL needs between 17-55 elements in the 256 queue. At the same time we handle if the ring runs full, e.g. due to a sudden burst of XDP redirected packets, which pushes packets into the qdisc layer.

Quoted 6 lines
Quoted 3 lines

You are checking inflight/limit in /sys directory to get the 17-55 number, right?

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Quoted 8 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1] https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2] https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 5 lines

Sorry for cutting the remaining of the message, but I ran out of time, as things are a bit challenging/hectic here at Cloudflare at the moment.

--Jesper

All good, just ignore it. I think I misunderstood something anyway.

#22 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 67 lines · Simon Schippers
Quoted 66 lines · Jesper Dangaard Brouer
Quoted 24 lines · Simon Schippers
Quoted 23 lines · Jesper Dangaard Brouer
Quoted 19 lines · Jakub Kicinski
Quoted 7 lines · On Sat, 9 May 2026 11:09:51 +0200 Jesper Dangaard Brouer wrote:
Quoted 6 lines · Jakub Kicinski
Quoted 2 lines · On Thu, 7 May 2026 21:09:09 +0200 Jesper Dangaard Brouer wrote:

Not against being able to modify VETH_RING_SIZE, but I don't think it is the solution here.

Was it evaluated, tho?

Quoted 7 lines
Quoted 5 lines

It's obviously super easy these days have AI spew no end of complex code. So it'd be great to have some solid, ideally production-like data to back this all up.

VETH_RING_SIZE seems trivial, ethtool set ringparam

No, unfortunately we cannot just decrease the VETH_RING_SIZE.

To be clear - I said may it configurable with ethtool -G not change the default.

Quoted 8 lines
Quoted 5 lines

Sure, I understand the desire to make VETH_RING_SIZE configurable. If doing so we are making Linux network stack harder to tune and setup correctly. E.g. adding a qdisc to veth would also require changing the ring size, but if system also uses XDP then tuning below 64 (likely 128) will lead to hard-to-find packet drops.

I mean 64 still could be a 4x improvement at least.

No not really, setting it to 64 will give same (bad) latency from "BQL off" which that patchset is trying to address.

Quoted 27 lines
Quoted 26 lines

I prefer adding something (like BQL) that auto-tune how much of the ring queue we are using. Good queues function as shock absorbers when concurrent processes in the OS have scheduling noise.

I acknowledge that Simon Schippers found that the BQL implementation was actually not auto-tuning. We need to work on this, my prototype implementation [1] [2] works surprisingly well.

- [1] https://lore.kernel.org/all/[email protected]/2-09-veth-time-based-bql-coalescing.patch
- [2] https://lore.kernel.org/all/[email protected]/

Quoted 7 lines
Quoted 4 lines

The reason is that XDP-redirect into veth don't have any back-pressure and would simply drop packets if queue size becomes less than the NAPI budget (64). (Yes, we use both normal path and XDP-redirect in production).

Doesn't this mean you have a queue which is not under BQL control?

It is a matter of perspective. BQL needs between 17-55 elements in the 256 queue. At the same time we handle if the ring runs full, e.g. due to a sudden burst of XDP redirected packets, which pushes packets into the qdisc layer.

Quoted 8 lines
Quoted 6 lines
Quoted 3 lines

You are checking inflight/limit in /sys directory to get the 17-55 number, right?

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 16 lines
Quoted 8 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1] https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2] https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

Quoted 7 lines
Quoted 5 lines

Sorry for cutting the remaining of the message, but I ran out of time, as things are a bit challenging/hectic here at Cloudflare at the moment.

--Jesper

All good, just ignore it. I think I misunderstood something anyway.

Okay, I'll ignore it as I couldn't make sense of it ;-) --Jesper

--- BQL inflight histogram (VETH_BQL_UNIT=1, values = packets) --- @inflight:
[0, 1) 306565 |@

     |
[1, 2)           9250454 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2, 3)           5561919 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 
     |
[3, 4)            354341 |@ 
     |
[4, 5)             50137 | 
     |
[5, 6)             16771 | 
     |
[6, 7)              6001 | 
     |
[7, 8)              3076 | 
     |
[8, 9)              1949 | 
     |
[9, 10)             1965 | 
     |
[10, 11)            1954 | 
     |
[11, 12)            1914 | 
     |
[12, 13)            1732 | 
     |
[13, 14)            1559 | 
     |
[14, 15)            1405 | 
     |
[15, 16)            1269 | 
     |
[16, 17)            1194 | 
     |
[17, 18)            1190 | 
     |
[18, 19)            1148 | 
     |
[19, 20)            1079 | 
     |
[20, 21)            1008 | 
     |
[21, 22)             951 | 
     |
[22, 23)             870 | 
     |
[23, 24)             826 | 
     |
[24, 25)             775 | 
     |
[25, 26)             764 | 
     |
[26, 27)             740 | 
     |
[27, 28)             714 | 
     |
[28, 29)             665 | 
     |
[29, 30)             626 | 
     |
[30, 31)             607 | 
     |
[31, 32)             601 | 
     |
[32, 33)             583 | 
     |
[33, 34)             593 | 
     |
[34, 35)             574 | 
     |
[35, 36)             562 | 
     |
[36, 37)             554 | 
     |
[37, 38)             538 | 
     |
[38, 39)             528 | 
     |
[39, 40)             525 | 
     |
[40, 41)             512 | 
     |
[41, 42)             542 | 
     |
[42, 43)             529 | 
     |
[43, 44)             526 | 
     |
[44, 45)             513 | 
     |
[45, 46)             503 | 
     |
[46, 47)             485 | 
     |
[47, 48)             480 | 
     |
[48, 49)             473 | 
     |
[49, 50)             474 | 
     |
[50, 51)             476 | 
     |
[51, 52)             476 | 
     |
[52, 53)             465 | 
     |
[53, 54)             454 | 
     |
[54, 55)             446 | 
     |
[55, 56)             430 | 
     |
[56, 57)             425 | 
     |
[57, 58)             425 | 
     |
[58, 59)             422 | 
     |
[59, 60)             407 | 
     |
[60, 61)             390 | 
     |
[61, 62)             370 | 
     |
[62, 63)             354 | 
     |
[63, 64)             343 | 
     |
[64, 65)             325 | 
     |
[65, 66)             303 | 
     |
[66, 67)             158 | 
     |
[67, 68)             136 | 
     |
[68, 69)             124 | 
     |
[69, 70)             110 | 
     |
[70, 71)              99 | 
     |
[71, 72)              94 | 
     |
[72, 73)              82 | 
     |
[73, 74)              74 | 
     |
[74, 75)              58 | 
     |
[75, 76)              52 | 
     |
[76, 77)              45 | 
     |
[77, 78)              40 | 
     |
[78, 79)              39 | 
     |
[79, 80)              38 | 
     |
[80, 81)              21 | 
     |
[81, 82)               4 | 
     |
[82, 83)               4 | 
     |
[83, 84)               4 | 
     |
[84, 85)               2 | 
     |
[85, 86)               2 | 
     |
[86, 87)               2 | 
     |
[87, 88)               2 | 
     |
[88, 89)               1 | 
     |

--- BQL limit histogram (auto-tuned, values = packets) --- @limit_val:
[61, 62) 221346 |@

     |
[62, 63)               0 | 
     |
[63, 64)          772169 |@@@ 
     |
[64, 65)        10053949 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[65, 66)               0 | 
     |
[66, 67)               0 | 
     |
[67, 68)               0 | 
     |
[68, 69)               0 | 
     |
[69, 70)               0 | 
     |
[70, 71)          457838 |@@ 
     |
[71, 72)               0 | 
     |
[72, 73)          610198 |@@@ 
     |
[73, 74)               0 | 
     |
[74, 75)               0 | 
     |
[75, 76)               0 | 
     |
[76, 77)               0 | 
     |
[77, 78)               0 | 
     |
[78, 79)         2328284 |@@@@@@@@@@@@ 
     |
[79, 80)         1150181 |@@@@@ 
     |

@inflight_stats: count 15593965, average 1, total 23078061

@limit_stats: count 15593965, average 67, total 1054054856

#23 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 33 lines · Jesper Dangaard Brouer
Quoted 4 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 16 lines
Quoted 8 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1] https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2] https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 7 lines

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

I ran the benchmarks with --hist and I see what you mean. I have very similar results.

Is Jonas way [1] of modifiying pktgen maybe the best option to ensure that the producer is faster than the consumer?

[1] Link: https://lore.kernel.org/netdev/[email protected]/

Review

Quoted 7 lines

Hi, so what I found is that pktgen does not respect __QUEUE_STATE_STACK_OFF. So the test data above is invalid, since it just sent packets even if the BQL "stopped" the queue. So I patched pktgen with the following:

(quoted diff) +1 -1
-       if (unlikely(netif_xmit_frozen_or_drv_stopped(txq))) {
+       if (unlikely(netif_xmit_frozen_or_stopped(txq))) {

After thinking more about the implementation I see possible issues:

1. netdev_tx_completed_queue() never reports more than burst=64 packets:

BQL only increments the limit if the queue was starved. That means:
"The queue was over-limit in the last interval (the last time completion processing ran), and there is no more data in the queue (i.e. it’s empty)" [2]
But as only 64 packets are reported at max, the queue can only grow when it is <= 64 packets. And then it can only stay at a limit >64 until the next decrease of the limit.

2. netdev_tx_completed_queue() is called in irregular intervals:

If the consumer is slow it is called approx each tx_coal_usecs. But if the consumer is fast it is called way more frequent, probably in irregular intervals depending on the scheduling. However, "BQL depends on periodic completion interrupts" [2].

--> How about adding something like an interrupt that triggers every
10us and calls netdev_tx_completed_queue() with n_bql collected from (multiple) veth_xdp_rcv runs? That could solve 1. and 2.

[2] Link: https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 7 lines

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

I understand..

Quoted 135 lines
Quoted 9 lines
Quoted 7 lines

Sorry for cutting the remaining of the message, but I ran out of time, as things are a bit challenging/hectic here at Cloudflare at the moment.

--Jesper

All good, just ignore it. I think I misunderstood something anyway.

Okay, I'll ignore it as I couldn't make sense of it ;-) --Jesper

--- BQL inflight histogram (VETH_BQL_UNIT=1, values = packets) --- @inflight:

[0, 1)            306565 |@      |
[1, 2)           9250454 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2, 3)           5561919 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      |
[3, 4)            354341 |@      |
[4, 5)             50137 |      |
[5, 6)             16771 |      |
[6, 7)              6001 |      |
[7, 8)              3076 |      |
[8, 9)              1949 |      |
[9, 10)             1965 |      |
[10, 11)            1954 |      |
[11, 12)            1914 |      |
[12, 13)            1732 |      |
[13, 14)            1559 |      |
[14, 15)            1405 |      |
[15, 16)            1269 |      |
[16, 17)            1194 |      |
[17, 18)            1190 |      |
[18, 19)            1148 |      |
[19, 20)            1079 |      |
[20, 21)            1008 |      |
[21, 22)             951 |      |
[22, 23)             870 |      |
[23, 24)             826 |      |
[24, 25)             775 |      |
[25, 26)             764 |      |
[26, 27)             740 |      |
[27, 28)             714 |      |
[28, 29)             665 |      |
[29, 30)             626 |      |
[30, 31)             607 |      |
[31, 32)             601 |      |
[32, 33)             583 |      |
[33, 34)             593 |      |
[34, 35)             574 |      |
[35, 36)             562 |      |
[36, 37)             554 |      |
[37, 38)             538 |      |
[38, 39)             528 |      |
[39, 40)             525 |      |
[40, 41)             512 |      |
[41, 42)             542 |      |
[42, 43)             529 |      |
[43, 44)             526 |      |
[44, 45)             513 |      |
[45, 46)             503 |      |
[46, 47)             485 |      |
[47, 48)             480 |      |
[48, 49)             473 |      |
[49, 50)             474 |      |
[50, 51)             476 |      |
[51, 52)             476 |      |
[52, 53)             465 |      |
[53, 54)             454 |      |
[54, 55)             446 |      |
[55, 56)             430 |      |
[56, 57)             425 |      |
[57, 58)             425 |      |
[58, 59)             422 |      |
[59, 60)             407 |      |
[60, 61)             390 |      |
[61, 62)             370 |      |
[62, 63)             354 |      |
[63, 64)             343 |      |
[64, 65)             325 |      |
[65, 66)             303 |      |
[66, 67)             158 |      |
[67, 68)             136 |      |
[68, 69)             124 |      |
[69, 70)             110 |      |
[70, 71)              99 |      |
[71, 72)              94 |      |
[72, 73)              82 |      |
[73, 74)              74 |      |
[74, 75)              58 |      |
[75, 76)              52 |      |
[76, 77)              45 |      |
[77, 78)              40 |      |
[78, 79)              39 |      |
[79, 80)              38 |      |
[80, 81)              21 |      |
[81, 82)               4 |      |
[82, 83)               4 |      |
[83, 84)               4 |      |
[84, 85)               2 |      |
[85, 86)               2 |      |
[86, 87)               2 |      |
[87, 88)               2 |      |
[88, 89)               1 |      |

--- BQL limit histogram (auto-tuned, values = packets) --- @limit_val:

[61, 62)          221346 |@      |
[62, 63)               0 |      |
[63, 64)          772169 |@@@      |
[64, 65)        10053949 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[65, 66)               0 |      |
[66, 67)               0 |      |
[67, 68)               0 |      |
[68, 69)               0 |      |
[69, 70)               0 |      |
[70, 71)          457838 |@@      |
[71, 72)               0 |      |
[72, 73)          610198 |@@@      |
[73, 74)               0 |      |
[74, 75)               0 |      |
[75, 76)               0 |      |
[76, 77)               0 |      |
[77, 78)               0 |      |
[78, 79)         2328284 |@@@@@@@@@@@@      |
[79, 80)         1150181 |@@@@@      |

@inflight_stats: count 15593965, average 1, total 23078061

@limit_stats: count 15593965, average 67, total 1054054856

#24 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 74 lines · Simon Schippers
Quoted 33 lines · Jesper Dangaard Brouer
Quoted 4 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 16 lines
Quoted 8 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1] https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2] https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 7 lines

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

I ran the benchmarks with --hist and I see what you mean. I have very similar results.

Is Jonas way [1] of modifiying pktgen maybe the best option to ensure that the producer is faster than the consumer?

[1] Link: https://lore.kernel.org/netdev/[email protected]/

Review

Quoted 7 lines

Hi, so what I found is that pktgen does not respect __QUEUE_STATE_STACK_OFF. So the test data above is invalid, since it just sent packets even if the BQL "stopped" the queue. So I patched pktgen with the following:

(quoted diff) +1 -1
-       if (unlikely(netif_xmit_frozen_or_drv_stopped(txq))) {
+       if (unlikely(netif_xmit_frozen_or_stopped(txq))) {

After thinking more about the implementation I see possible issues:

1. netdev_tx_completed_queue() never reports more than burst=64 packets:

BQL only increments the limit if the queue was starved. That means:
"The queue was over-limit in the last interval (the last time completion processing ran), and there is no more data in the queue (i.e. it’s empty)" [2]
But as only 64 packets are reported at max, the queue can only grow when it is <= 64 packets. And then it can only stay at a limit >64 until the next decrease of the limit.

2. netdev_tx_completed_queue() is called in irregular intervals:

Quoted 8 lines

If the consumer is slow it is called approx each tx_coal_usecs. But if the consumer is fast it is called way more frequent, probably in irregular intervals depending on the scheduling. However, "BQL depends on periodic completion interrupts" [2].

--> How about adding something like an interrupt that triggers every
10us and calls netdev_tx_completed_queue() with n_bql collected from (multiple) veth_xdp_rcv runs? That could solve 1. and 2.

Hi,

I worked on a new version (see attachment) that addresses both issues.

The major change is that instead of tracking the timestamp and packet count as local variables in veth_xdp_rcv(), they are now stored persistently in veth_rq as struct veth_bql_state. This allows completions to accumulate across multiple NAPI poll calls, so netdev_tx_completed_queue() can report more than 64 packets at once (see point 1). To get the time I am using (the fast) sched_clock() with a trick to avoid issues when switching between CPUs.

For point 2, the coalescing deadline is now checked both before the receive loop (to flush completions that timed out since the previous poll) and after each consumed packet, making completion intervals more regular. Still the intervals can be smaller than VETH_BQL_COAL_TX_USECS, but I guess this is fine.

I also found out that the BQL limit correlates closely with VETH_BQL_COAL_TX_USECS. It essentially reflects the latency we are targeting. I raised the default to 100 µs to allow DQL to converge to a higher limit (for reaching 255 in the testing below).

With the patched pktgen (respecting __QUEUE_STATE_STACK_XOFF), testing shows:
- --nrules 0: DQL limit reaches (up to) ~255
- --nrules 10000: DQL limit converges to ~0 (with --gro-disable)

These results are what I would expect from a BQL algorithm, but more testing is needed of course.

What do you think?

Thanks!

BTW: I think that this implementation could also work for other
software interfaces.

Quoted 4 lines

[2] Link: https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 1 line
Quoted 9 lines
Quoted 6 lines

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

I understand..

From 4af80d3db4b828c6ffdb81a44c8b6227b318e7da Mon Sep 17 00:00:00 2001
From: Simon Schippers <[email protected]>
Date: Tue, 12 May 2026 17:34:54 +0200
Subject: [PATCH] veth: time-based BQL completion coalescing via ethtool
tx-usecs

Bufferbloat is fundamentally a latency problem -- what matters is the time packets spend waiting in queues, as perceived by users and applications. Base BQL completion coalescing on elapsed time rather than packet counts to directly control queuing delay.

Add ethtool tx-usecs support to veth for tuning BQL completion coalescing. Instead of completing BQL per-packet (which forces DQL to limit=2 with high NAPI scheduling overhead) or per-NAPI-poll (which over-buffers at budget=64), accumulate completions and flush them when a configurable time threshold is exceeded. This lets DQL discover a limit that bounds the actual queuing delay to the configured interval.

Coalescing state (timestamp + pending count) is stored in struct veth_bql_state embedded in veth_rq, persisting across NAPI polls so that completions whose deadline expired in a previous poll are released at the start of the next one.

Two helpers centralise the logic:
veth_bql_complete() - flush + reset state
veth_bql_maybe_complete() - flush only if deadline elapsed

A min(state->time, sched_clock()) guard at veth_xdp_rcv() entry prevents the coalescing deadline from stalling if NAPI migrates to a CPU whose sched_clock() is slightly behind the stored timestamp.

At end-of-function, pending completions are released immediately only when the ring has drained and __QUEUE_STATE_STACK_XOFF is set, ensuring DQL backpressure is lifted without bypassing coalescing in the common case. The smp_rmb() is paired with smp_wmb() in __ptr_ring_produce().

bql_state is initialised at NAPI enable (sched_clock() timestamp) and cleared at NAPI teardown, where netdev_tx_reset_queue() already resets DQL on the peer side.

Default tx-usecs raised to 100 us (was 10 us) to allow DQL to converge to a more useful limit. Setting tx-usecs to 0 disables coalescing and falls back to per-packet completion (limit=2, lowest latency).

Usage:
ethtool -C <veth-dev> tx-usecs 500 # 500us coalescing
ethtool -C <veth-dev> tx-usecs 0 # per-packet (no coalescing)

---
 drivers/net/veth.c | 95 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 93 insertions(+), 2 deletions(-)
drivers/net/veth.c +93 -2
index 4103d298aa9b..c9d53ea9598b 100644
@@ -43,6 +43,7 @@
 
 #define VETH_XDP_TX_BULK_SIZE	16
 #define VETH_XDP_BATCH		16
+#define VETH_BQL_COAL_TX_USECS	100	/* default tx-usecs for BQL batching */
 
 struct veth_stats {
 	u64	rx_drops;
@@ -62,6 +63,11 @@ struct veth_rq_stats {
 	struct u64_stats_sync	syncp;
 };
 
+struct veth_bql_state {
+	u64	time;	/* sched_clock() when current coalescing window started */
+	int	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 */
@@ -69,6 +75,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;
@@ -81,6 +88,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 {
@@ -265,7 +273,30 @@ 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);
+
+	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,
@@ -275,6 +306,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 */
@@ -937,13 +970,45 @@ 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)
+{
+	netdev_tx_completed_queue(peer_txq, state->n_bql,
+				  state->n_bql * VETH_BQL_UNIT);
+	state->n_bql = 0;
+	state->time = sched_clock();
+}
+
+static void veth_bql_maybe_complete(struct veth_bql_state *state,
+				    struct netdev_queue *peer_txq,
+				    u64 coalescing_ns)
+{
+	if (state->n_bql && sched_clock() >= state->time + coalescing_ns)
+		veth_bql_complete(state, peer_txq);
+}
+
 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_bql_state *state = &rq->bql_state;
 	int i, done = 0, n_xdpf = 0;
 	void *xdpf[VETH_XDP_BATCH];
+	struct veth_priv *priv;
+	u64 bql_flush_ns;
+
+	priv = netdev_priv(rq->dev);
+	bql_flush_ns = (u64)priv->tx_coal_usecs * 1000;
+
+	/* Clamp stored timestamp in case we migrated to a CPU with a behind
+	 * sched_clock(); prevents the deadline from never firing.
+	 */
+	state->time = min(state->time, sched_clock());
+
+	/* Flush completions that timed out since the previous NAPI poll. */
+	if (peer_txq && bql_flush_ns)
+		veth_bql_maybe_complete(state, peer_txq, bql_flush_ns);
 
 	for (i = 0; i < budget; i++) {
 		void *ptr = __ptr_ring_consume(&rq->xdp_ring);
@@ -972,8 +1037,16 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget,
 			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);
+			if (peer_txq && bql_charged) {
+				if (!bql_flush_ns) {
+					netdev_tx_completed_queue(peer_txq, 1,
+								  VETH_BQL_UNIT);
+				} else {
+					state->n_bql++;
+					veth_bql_maybe_complete(state, peer_txq,
+								bql_flush_ns);
+				}
+			}
 
 			skb = veth_xdp_rcv_skb(rq, skb, bq, stats);
 			if (skb) {
@@ -989,6 +1062,16 @@ 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 is now empty and the peer TX queue is stalled by DQL
+	 * backpressure, release completions immediately to unblock it.
+	 * The smp_rmb() is paired with smp_wmb() in __ptr_ring_produce().
+	 */
+	if (peer_txq && state->n_bql && ptr_ring_empty(&rq->xdp_ring)) {
+		smp_rmb();
+		if (test_bit(__QUEUE_STATE_STACK_XOFF, &peer_txq->state))
+			veth_bql_complete(state, peer_txq);
+	}
+
 	u64_stats_update_begin(&rq->stats.syncp);
 	rq->stats.vs.xdp_redirect += stats->xdp_redirect;
 	rq->stats.vs.xdp_bytes += stats->xdp_bytes;
@@ -1093,6 +1176,8 @@ static int __veth_napi_enable_range(struct net_device *dev, int start, int end)
 
 		napi_enable(&rq->xdp_napi);
 		rcu_assign_pointer(priv->rq[i].napi, &priv->rq[i].xdp_napi);
+		rq->bql_state.time = sched_clock();
+		rq->bql_state.n_bql = 0;
 	}
 
 	return 0;
@@ -1134,6 +1219,8 @@ static void veth_napi_del_range(struct net_device *dev, int start, int end)
 		struct veth_rq *rq = &priv->rq[i];
 
 		rq->rx_notify_masked = false;
+		rq->bql_state.n_bql = 0;
+		rq->bql_state.time = 0;
 		ptr_ring_cleanup(&rq->xdp_ring, veth_ptr_free);
 	}
 
@@ -1813,6 +1900,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;
@@ -1838,6 +1927,8 @@ static void veth_setup(struct net_device *dev)
 	dev->max_mtu = ETH_MAX_MTU;
 	dev->watchdog_timeo = msecs_to_jiffies(16000);
 
+	priv->tx_coal_usecs = VETH_BQL_COAL_TX_USECS;
+
 	dev->hw_features = VETH_FEATURES;
 	dev->hw_enc_features = VETH_FEATURES;
 	dev->mpls_features = NETIF_F_HW_CSUM | NETIF_F_GSO_SOFTWARE;
Signature · 1 line
2.43.0
#25 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 104 lines · Simon Schippers
Quoted 67 lines · Simon Schippers
Quoted 28 lines · Jesper Dangaard Brouer
Quoted 3 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 14 lines
Quoted 7 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1] https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2] https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 7 lines

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

I ran the benchmarks with --hist and I see what you mean. I have very similar results.

Is Jonas way [1] of modifiying pktgen maybe the best option to ensure that the producer is faster than the consumer?

[1] Link: https://lore.kernel.org/netdev/[email protected]/

Review

Quoted 7 lines

Hi, so what I found is that pktgen does not respect __QUEUE_STATE_STACK_OFF. So the test data above is invalid, since it just sent packets even if the BQL "stopped" the queue. So I patched pktgen with the following:

(quoted diff) +1 -1
-       if (unlikely(netif_xmit_frozen_or_drv_stopped(txq))) {
+       if (unlikely(netif_xmit_frozen_or_stopped(txq))) {

After thinking more about the implementation I see possible issues:

1. netdev_tx_completed_queue() never reports more than burst=64 packets:

BQL only increments the limit if the queue was starved. That means:
"The queue was over-limit in the last interval (the last time completion processing ran), and there is no more data in the queue (i.e. it’s empty)" [2]
But as only 64 packets are reported at max, the queue can only grow when it is <= 64 packets. And then it can only stay at a limit >64 until the next decrease of the limit.

2. netdev_tx_completed_queue() is called in irregular intervals:

Quoted 8 lines

If the consumer is slow it is called approx each tx_coal_usecs. But if the consumer is fast it is called way more frequent, probably in irregular intervals depending on the scheduling. However, "BQL depends on periodic completion interrupts" [2].

--> How about adding something like an interrupt that triggers every
10us and calls netdev_tx_completed_queue() with n_bql collected from (multiple) veth_xdp_rcv runs? That could solve 1. and 2.

Hi,

I worked on a new version (see attachment) that addresses both issues.

The major change is that instead of tracking the timestamp and packet count as local variables in veth_xdp_rcv(), they are now stored persistently in veth_rq as struct veth_bql_state. This allows completions to accumulate across multiple NAPI poll calls, so netdev_tx_completed_queue() can report more than 64 packets at once (see point 1). To get the time I am using (the fast) sched_clock() with a trick to avoid issues when switching between CPUs.

For point 2, the coalescing deadline is now checked both before the receive loop (to flush completions that timed out since the previous poll) and after each consumed packet, making completion intervals more regular. Still the intervals can be smaller than VETH_BQL_COAL_TX_USECS, but I guess this is fine.

I also found out that the BQL limit correlates closely with VETH_BQL_COAL_TX_USECS. It essentially reflects the latency we are targeting. I raised the default to 100 µs to allow DQL to converge to a higher limit (for reaching 255 in the testing below).

With the patched pktgen (respecting __QUEUE_STATE_STACK_XOFF), testing shows:
- --nrules 0: DQL limit reaches (up to) ~255
- --nrules 10000: DQL limit converges to ~0 (with --gro-disable)

Quoted 4 lines

These results are what I would expect from a BQL algorithm, but more testing is needed of course.

What do you think?

Hi,

This is exactly what I had in mind for implementing the BQL algorithm in this case. I did some testing with pktgen of this patch and also compared it to the v5 version.

You can find an extension of the benchmark script with pktgen here [1], as well as a wrapper script (veth_bql_bench.sh) to run the test script with and without --bql-disable to report the difference. I also configured pktgen to use the qdisc as suggested by Jesper.

Note: bpftrace needs to be disabled, otherwise it becomes the bottleneck (at least on my machine) and pktgen throughput is halved when enabled.

Here are the results:

v5 (not time-based):
--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
                       BQL on    BQL off
---                    ------    ------- Throughput (pps)      1980871    2169898
Ping RTT avg (ms)       0.065      0.162 Throughput diff         -8.7%   // BQL 8.7% lower throughput RTT diff               -59.9%   // BQL 60% lower latency
========================================

Simon's time-based version:

Test args: --nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
                           BQL on      BQL off
---                        ------      -------
Throughput (pps)          2166335      2153398
Ping RTT avg (ms)           0.165        0.165
Throughput diff              0.6%
RTT diff                     0.0%

--pktgen --no-bpftrace --nrules 3500
========================================
Results (average over 10 runs):
========================================
                           BQL on      BQL off
---                        ------      -------
Throughput (pps)            28569        28696
Ping RTT avg (ms)           1.327        8.409
Throughput diff             -0.4%
RTT diff                   -84.2%

Seems to work now as expected.

[1]https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark

Thanks,
Jonas

Quoted 16 lines

Thanks!

BTW: I think that this implementation could also work for other
software interfaces.

Quoted 10 lines

[2] Link: https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 6 lines

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

I understand..

A message that is not in the archive (sent off-list or not yet received): [email protected]

#27 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 112 lines · Jonas Köppeler
Quoted 104 lines · Simon Schippers
Quoted 67 lines · Simon Schippers
Quoted 28 lines · Jesper Dangaard Brouer
Quoted 3 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 14 lines
Quoted 7 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1]https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2]https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 7 lines

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

I ran the benchmarks with --hist and I see what you mean. I have very similar results.

Is Jonas way [1] of modifiying pktgen maybe the best option to ensure that the producer is faster than the consumer?

[1] Link:https://lore.kernel.org/netdev/[email protected]/

Review

Quoted 7 lines

Hi, so what I found is that pktgen does not respect __QUEUE_STATE_STACK_OFF. So the test data above is invalid, since it just sent packets even if the BQL "stopped" the queue. So I patched pktgen with the following:

(quoted diff) +1 -1
-       if (unlikely(netif_xmit_frozen_or_drv_stopped(txq))) {
+       if (unlikely(netif_xmit_frozen_or_stopped(txq))) {

After thinking more about the implementation I see possible issues:

1. netdev_tx_completed_queue() never reports more than burst=64 packets:

BQL only increments the limit if the queue was starved. That means:
"The queue was over-limit in the last interval (the last time completion processing ran), and there is no more data in the queue (i.e. it’s empty)" [2]
But as only 64 packets are reported at max, the queue can only grow when it is <= 64 packets. And then it can only stay at a limit >64 until the next decrease of the limit.

2. netdev_tx_completed_queue() is called in irregular intervals:

Quoted 8 lines

If the consumer is slow it is called approx each tx_coal_usecs. But if the consumer is fast it is called way more frequent, probably in irregular intervals depending on the scheduling. However, "BQL depends on periodic completion interrupts" [2].

--> How about adding something like an interrupt that triggers every
10us and calls netdev_tx_completed_queue() with n_bql collected from (multiple) veth_xdp_rcv runs? That could solve 1. and 2.

Hi,

I worked on a new version (see attachment) that addresses both issues.

The major change is that instead of tracking the timestamp and packet count as local variables in veth_xdp_rcv(), they are now stored persistently in veth_rq as struct veth_bql_state. This allows completions to accumulate across multiple NAPI poll calls, so netdev_tx_completed_queue() can report more than 64 packets at once (see point 1). To get the time I am using (the fast) sched_clock() with a trick to avoid issues when switching between CPUs.

For point 2, the coalescing deadline is now checked both before the receive loop (to flush completions that timed out since the previous poll) and after each consumed packet, making completion intervals more regular. Still the intervals can be smaller than VETH_BQL_COAL_TX_USECS, but I guess this is fine.

I also found out that the BQL limit correlates closely with VETH_BQL_COAL_TX_USECS. It essentially reflects the latency we are targeting. I raised the default to 100 µs to allow DQL to converge to a higher limit (for reaching 255 in the testing below).

With the patched pktgen (respecting __QUEUE_STATE_STACK_XOFF), testing shows:
- --nrules 0: DQL limit reaches (up to) ~255
- --nrules 10000: DQL limit converges to ~0 (with --gro-disable)

Quoted 4 lines

These results are what I would expect from a BQL algorithm, but more testing is needed of course.

What do you think?

Hi,

Quoted 8 lines

This is exactly what I had in mind for implementing the BQL algorithm in this case. I did some testing with pktgen of this patch and also compared it to the v5 version.

You can find an extension of the benchmark script with pktgen here [1], as well as a wrapper script (veth_bql_bench.sh) to run the test script with and without --bql-disable to report the difference. I also configured pktgen to use the qdisc as suggested by Jesper.

Great, I will use your pktgen solution from now on.

Didn't know about the qdisc option, is there a performance difference with/without it? Or is it to have ping working next to pktgen?

Consider to do a pull request :)

Quoted 4 lines

Note: bpftrace needs to be disabled, otherwise it becomes the bottleneck (at least on my machine) and pktgen throughput is halved when enabled.

Good to know.

Quoted 35 lines

Here are the results:

v5 (not time-based):
--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================

                       BQL on    BQL off
---                    ------    -------
Throughput (pps)      1980871    2169898

Ping RTT avg (ms) 0.065 0.162
Throughput diff -8.7% // BQL 8.7% lower throughput
RTT diff -59.9% // BQL 60% lower latency
========================================

Simon's time-based version:

Test args: --nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================

                           BQL on      BQL off
---                        ------      -------
Throughput (pps)          2166335      2153398

Ping RTT avg (ms) 0.165 0.165
Throughput diff 0.6%
RTT diff 0.0%

--pktgen --no-bpftrace --nrules 3500
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------

Quoted 5 lines
Throughput (pps)            28569        28696
Ping RTT avg (ms)           1.327        8.409
Throughput diff             -0.4%

RTT diff -84.2%

I think we should run benchmarks against the stock net-next to be safe.

Quoted 1 line

Seems to work now as expected.

Yes, but I think we have to keep these points in mind:

1. Limit/Inflight can be bigger than VETH_RING_SIZE, because packets can be enqueued in the same time as they are read out, so netdev_tx_completed_queue() can theoretically be called with many number of packets.
I do not think it is deal-breaking though.
I could see such high limits/inflights when looking at the /sys BQL statistics..

2. sched_clock() is only valid on the same CPU. When a different CPU starts executing its sched_clock() can be in the past compared to the sched_clock() value saved by the previous CPU.
My trick...
min(s->time, sched_clock())
... avoids potentially extremely long intervals between netdev_tx_completed_queue() calls but is not perfect of course. I think CPU hopping happens rarely enough for this to matter.. And also we have to keep this in mind [1]:
"An architecture may or may not provide an implementation of sched_clock() on its own. If a local implementation is not provided, the system jiffy counter will be used as sched_clock()."

3. Inflight can be stuck at a value>0 for a long time when packet enqueueing stops. Only when packets are enqueued again, (on the next veth_xdp_rcv() call,) netdev_tx_completed_queue() is executed and inflight is set to 0 again.
Can also be seen when looking at the /sys BQL statistics.

BTW: Yesterday, I worked on and refactored the code into its own .h
file as a library and it also works fine for TUN/TAP (+vhost-net) for me :)

Thanks for your work!
Simon

[1] Link: https://docs.kernel.org/timers/timekeeping.html

Quoted 21 lines

[1]https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark

Thanks,
Jonas

Quoted 15 lines

Thanks!

BTW: I think that this implementation could also work for other
software interfaces.

Quoted 10 lines

[2] Link:https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 6 lines

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

I understand..

#28 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 115 lines · Simon Schippers
Quoted 114 lines · Jonas Köppeler
Quoted 103 lines · Simon Schippers
Quoted 66 lines · Simon Schippers
Quoted 28 lines · Jesper Dangaard Brouer
Quoted 3 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 14 lines
Quoted 7 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1]https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2]https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 7 lines

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

I ran the benchmarks with --hist and I see what you mean. I have very similar results.

Is Jonas way [1] of modifiying pktgen maybe the best option to ensure that the producer is faster than the consumer?

[1] Link:https://lore.kernel.org/netdev/[email protected]/

Review

Quoted 7 lines

Hi, so what I found is that pktgen does not respect __QUEUE_STATE_STACK_OFF. So the test data above is invalid, since it just sent packets even if the BQL "stopped" the queue. So I patched pktgen with the following:

(quoted diff) +1 -1
-       if (unlikely(netif_xmit_frozen_or_drv_stopped(txq))) {
+       if (unlikely(netif_xmit_frozen_or_stopped(txq))) {

After thinking more about the implementation I see possible issues:

1. netdev_tx_completed_queue() never reports more than burst=64 packets:

BQL only increments the limit if the queue was starved. That means:
"The queue was over-limit in the last interval (the last time completion processing ran), and there is no more data in the queue (i.e. it’s empty)" [2]
But as only 64 packets are reported at max, the queue can only grow when it is <= 64 packets. And then it can only stay at a limit >64 until the next decrease of the limit.

2. netdev_tx_completed_queue() is called in irregular intervals:

Quoted 8 lines

If the consumer is slow it is called approx each tx_coal_usecs. But if the consumer is fast it is called way more frequent, probably in irregular intervals depending on the scheduling. However, "BQL depends on periodic completion interrupts" [2].

--> How about adding something like an interrupt that triggers every
10us and calls netdev_tx_completed_queue() with n_bql collected from (multiple) veth_xdp_rcv runs? That could solve 1. and 2.

Hi,

I worked on a new version (see attachment) that addresses both issues.

The major change is that instead of tracking the timestamp and packet count as local variables in veth_xdp_rcv(), they are now stored persistently in veth_rq as struct veth_bql_state. This allows completions to accumulate across multiple NAPI poll calls, so netdev_tx_completed_queue() can report more than 64 packets at once (see point 1). To get the time I am using (the fast) sched_clock() with a trick to avoid issues when switching between CPUs.

For point 2, the coalescing deadline is now checked both before the receive loop (to flush completions that timed out since the previous poll) and after each consumed packet, making completion intervals more regular. Still the intervals can be smaller than VETH_BQL_COAL_TX_USECS, but I guess this is fine.

I also found out that the BQL limit correlates closely with VETH_BQL_COAL_TX_USECS. It essentially reflects the latency we are targeting. I raised the default to 100 µs to allow DQL to converge to a higher limit (for reaching 255 in the testing below).

With the patched pktgen (respecting __QUEUE_STATE_STACK_XOFF), testing shows:
- --nrules 0: DQL limit reaches (up to) ~255
- --nrules 10000: DQL limit converges to ~0 (with --gro-disable)

Quoted 4 lines

These results are what I would expect from a BQL algorithm, but more testing is needed of course.

What do you think?

Hi,

This is exactly what I had in mind for implementing the BQL algorithm in this case. I did some testing with pktgen of this patch and also compared it to the v5 version.

Quoted 8 lines
Quoted 4 lines

You can find an extension of the benchmark script with pktgen here [1], as well as a wrapper script (veth_bql_bench.sh) to run the test script with and without --bql-disable to report the difference. I also configured pktgen to use the qdisc as suggested by Jesper.

Great, I will use your pktgen solution from now on.

Didn't know about the qdisc option, is there a performance difference with/without it? Or is it to have ping working next to pktgen?

Did not see any big difference performance-wise, but as you say, ping works better with pktgen then.

Quoted 46 lines

Consider to do a pull request :)

Quoted 3 lines

Note: bpftrace needs to be disabled, otherwise it becomes the bottleneck (at least on my machine) and pktgen throughput is halved when enabled.

Good to know.

Quoted 34 lines

Here are the results:

v5 (not time-based):
--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 1980871 2169898
Ping RTT avg (ms) 0.065 0.162
Throughput diff -8.7% // BQL 8.7% lower throughput
RTT diff -59.9% // BQL 60% lower latency
========================================

Simon's time-based version:

Test args: --nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 2166335 2153398
Ping RTT avg (ms) 0.165 0.165
Throughput diff 0.6%
RTT diff 0.0%

--pktgen --no-bpftrace --nrules 3500
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------

Quoted 5 lines
Throughput (pps)            28569        28696
Ping RTT avg (ms)           1.327        8.409
Throughput diff             -0.4%

RTT diff -84.2%

Quoted 2 lines

I think we should run benchmarks against the stock net-next to be safe.

--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
                     net-next
---                  -------
Throughput (pps)     2285421
Ping RTT avg (ms)    0.161

(slightly adjusted the output to better communicate the results)

So in my case this means BQL implementation has ~5% lower throughput compared to net-next. But please double check.

Quoted 10 lines
Quoted 1 line

Seems to work now as expected.

Yes, but I think we have to keep these points in mind:

1. Limit/Inflight can be bigger than VETH_RING_SIZE, because packets can be enqueued in the same time as they are read out, so netdev_tx_completed_queue() can theoretically be called with many number of packets.
I do not think it is deal-breaking though.
I could see such high limits/inflights when looking at the /sys BQL statistics..

For me this makes sense, that inflight just means the number of packets not yet 'completed' or the number of packets that you can send between two completion calls. I think this is not specific to this implementation. But for long intervals this might result in some problems because you can just fill the veth_ring to its capacity quickly, and increasing latency if the receiver is slow. To illustrate this have a look at [1]. There are some plots that show the rtt vs. tx-usec config depending on nrules.

[1] https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark/results/tx-usecs

Quoted 4 lines

2. sched_clock() is only valid on the same CPU. When a different CPU starts executing its sched_clock() can be in the past compared to the sched_clock() value saved by the previous CPU. My trick...

Quoted 8 lines

min(s->time, sched_clock())
... avoids potentially extremely long intervals between netdev_tx_completed_queue() calls but is not perfect of course. I think CPU hopping happens rarely enough for this to matter.. And also we have to keep this in mind [1]:
"An architecture may or may not provide an implementation of sched_clock() on its own. If a local implementation is not provided, the system jiffy counter will be used as sched_clock()."

So the problem with this is that with jiffies you have like millisecond interval granularity, which might be too long in order to work properly. Given the receiver completes 128 packet in 1ms (queue_completed interval), the bql will set the limit at 256. Then the tx thread can quickly fill the ring, and it then basically stopped until the 1ms interval is over.

I don't know how much this matters in practice. Not sure which architectures still hit the jiffies fallback.When in doubt we could disable BQL for those, or pin it to a "virtual queue size" via limit_min / limit_max similar to the v5 behavior and call queue_completed for every packet. Wdyt?

Quoted 5 lines

3. Inflight can be stuck at a value>0 for a long time when packet enqueueing stops. Only when packets are enqueued again, (on the next veth_xdp_rcv() call,) netdev_tx_completed_queue() is executed and inflight is set to 0 again.
Can also be seen when looking at the /sys BQL statistics.

This happens if the ring empties before the next queue_completed fires, right?

Another thing: Is it counterintuitive to set the tx_usec_coal on the receive device? Because this is a bql related config that is normally configured on the TX side?

Quoted 3 lines

BTW: Yesterday, I worked on and refactored the code into its own .h
file as a library and it also works fine for TUN/TAP (+vhost-net) for me :)

Nice! Thank you.
Jonas

Quoted 26 lines

Thanks for your work!
Simon

[1] Link: https://docs.kernel.org/timers/timekeeping.html

Quoted 20 lines

[1]https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark

Thanks,
Jonas

Quoted 15 lines

Thanks!

BTW: I think that this implementation could also work for other
software interfaces.

Quoted 10 lines

[2] Link:https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 6 lines

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

I understand..

#29 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 182 lines · Jonas Köppeler
Quoted 115 lines · Simon Schippers
Quoted 114 lines · Jonas Köppeler
Quoted 103 lines · Simon Schippers
Quoted 66 lines · Simon Schippers
Quoted 28 lines · Jesper Dangaard Brouer
Quoted 3 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 14 lines
Quoted 7 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1]https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2]https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 7 lines

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

I ran the benchmarks with --hist and I see what you mean. I have very similar results.

Is Jonas way [1] of modifiying pktgen maybe the best option to ensure that the producer is faster than the consumer?

[1] Link:https://lore.kernel.org/netdev/[email protected]/

Review

Quoted 7 lines

Hi, so what I found is that pktgen does not respect __QUEUE_STATE_STACK_OFF. So the test data above is invalid, since it just sent packets even if the BQL "stopped" the queue. So I patched pktgen with the following:

(quoted diff) +1 -1
-       if (unlikely(netif_xmit_frozen_or_drv_stopped(txq))) {
+       if (unlikely(netif_xmit_frozen_or_stopped(txq))) {

After thinking more about the implementation I see possible issues:

1. netdev_tx_completed_queue() never reports more than burst=64 packets:

BQL only increments the limit if the queue was starved. That means:
"The queue was over-limit in the last interval (the last time completion processing ran), and there is no more data in the queue (i.e. it’s empty)" [2]
But as only 64 packets are reported at max, the queue can only grow when it is <= 64 packets. And then it can only stay at a limit >64 until the next decrease of the limit.

2. netdev_tx_completed_queue() is called in irregular intervals:

Quoted 8 lines

If the consumer is slow it is called approx each tx_coal_usecs. But if the consumer is fast it is called way more frequent, probably in irregular intervals depending on the scheduling. However, "BQL depends on periodic completion interrupts" [2].

--> How about adding something like an interrupt that triggers every
10us and calls netdev_tx_completed_queue() with n_bql collected from (multiple) veth_xdp_rcv runs? That could solve 1. and 2.

Hi,

I worked on a new version (see attachment) that addresses both issues.

The major change is that instead of tracking the timestamp and packet count as local variables in veth_xdp_rcv(), they are now stored persistently in veth_rq as struct veth_bql_state. This allows completions to accumulate across multiple NAPI poll calls, so netdev_tx_completed_queue() can report more than 64 packets at once (see point 1). To get the time I am using (the fast) sched_clock() with a trick to avoid issues when switching between CPUs.

For point 2, the coalescing deadline is now checked both before the receive loop (to flush completions that timed out since the previous poll) and after each consumed packet, making completion intervals more regular. Still the intervals can be smaller than VETH_BQL_COAL_TX_USECS, but I guess this is fine.

I also found out that the BQL limit correlates closely with VETH_BQL_COAL_TX_USECS. It essentially reflects the latency we are targeting. I raised the default to 100 µs to allow DQL to converge to a higher limit (for reaching 255 in the testing below).

With the patched pktgen (respecting __QUEUE_STATE_STACK_XOFF), testing shows:
- --nrules 0: DQL limit reaches (up to) ~255
- --nrules 10000: DQL limit converges to ~0 (with --gro-disable)

Quoted 4 lines

These results are what I would expect from a BQL algorithm, but more testing is needed of course.

What do you think?

Hi,

This is exactly what I had in mind for implementing the BQL algorithm in this case. I did some testing with pktgen of this patch and also compared it to the v5 version.

Quoted 8 lines
Quoted 4 lines

You can find an extension of the benchmark script with pktgen here [1], as well as a wrapper script (veth_bql_bench.sh) to run the test script with and without --bql-disable to report the difference. I also configured pktgen to use the qdisc as suggested by Jesper.

Great, I will use your pktgen solution from now on.

Didn't know about the qdisc option, is there a performance difference with/without it? Or is it to have ping working next to pktgen?

Did not see any big difference performance-wise, but as you say, ping works better with pktgen then.

Quoted 46 lines

Consider to do a pull request :)

Quoted 3 lines

Note: bpftrace needs to be disabled, otherwise it becomes the bottleneck (at least on my machine) and pktgen throughput is halved when enabled.

Good to know.

Quoted 34 lines

Here are the results:

v5 (not time-based):
--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 1980871 2169898
Ping RTT avg (ms) 0.065 0.162
Throughput diff -8.7% // BQL 8.7% lower throughput
RTT diff -59.9% // BQL 60% lower latency
========================================

Simon's time-based version:

Test args: --nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 2166335 2153398
Ping RTT avg (ms) 0.165 0.165
Throughput diff 0.6%
RTT diff 0.0%

--pktgen --no-bpftrace --nrules 3500
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------

Quoted 5 lines
Throughput (pps)            28569        28696
Ping RTT avg (ms)           1.327        8.409
Throughput diff             -0.4%

RTT diff -84.2%

Quoted 2 lines

I think we should run benchmarks against the stock net-next to be safe.

--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
net-next

Quoted 9 lines
---                  -------
Throughput (pps)     2285421
Ping RTT avg (ms)    0.161

(slightly adjusted the output to better communicate the results)

So in my case this means BQL implementation has ~5% lower throughput compared to net-next. But please double check.

Yes, I will benchmark myself.

There are probably some places where we can gain performance. For example, I see ptr_ring_empty() which could be swapped for __ptr_ring_empty() which would save a spinlock and unlock.

Quoted 11 lines
Quoted 10 lines
Quoted 1 line

Seems to work now as expected.

Yes, but I think we have to keep these points in mind:

1. Limit/Inflight can be bigger than VETH_RING_SIZE, because packets can be enqueued in the same time as they are read out, so netdev_tx_completed_queue() can theoretically be called with many number of packets.
I do not think it is deal-breaking though.
I could see such high limits/inflights when looking at the /sys BQL statistics..

Quoted 3 lines

For me this makes sense, that inflight just means the number of packets not yet 'completed' or the number of packets that you can send between two completion calls. I think this is not specific

From my understanding, I do think that this behavior is pretty specific.

Typically BQL-enabled NIC drivers clear packets out of some internal buffer in their completion interrupt (or something similar). And after that they call netdev_tx_completed_queue().

Quoted 3 lines

to this implementation. But for long intervals this might result in some problems because you can just fill the veth_ring to its capacity quickly, and increasing latency if the receiver is slow.

Yes, but I think the latency can only be approx. as big as the interval (can be higher with GRO enabled).

Quoted 5 lines

To illustrate this have a look at [1]. There are some plots that show the rtt vs. tx-usec config depending on nrules.

[1] https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark/results/tx-usecs

Nice plots! Great for finding a sane default value.

Quoted 13 lines
Quoted 4 lines

2. sched_clock() is only valid on the same CPU. When a different CPU starts executing its sched_clock() can be in the past compared to the sched_clock() value saved by the previous CPU. My trick...

Quoted 8 lines

min(s->time, sched_clock())
... avoids potentially extremely long intervals between netdev_tx_completed_queue() calls but is not perfect of course. I think CPU hopping happens rarely enough for this to matter.. And also we have to keep this in mind [1]:
"An architecture may or may not provide an implementation of sched_clock() on its own. If a local implementation is not provided, the system jiffy counter will be used as sched_clock()."

Quoted 6 lines

So the problem with this is that with jiffies you have like millisecond interval granularity, which might be too long in order to work properly. Given the receiver completes 128 packet in 1ms (queue_completed interval), the bql will set the limit at 256. Then the tx thread can quickly fill the ring, and it then basically stopped until the 1ms interval is over.

Linux Mint only has 100Hz jiffies :^)

I think you misunderstand something.

+ if (peer_txq && state->n_bql && ptr_ring_empty(&rq->xdp_ring)) {
+ smp_rmb();
+ if (test_bit(__QUEUE_STATE_STACK_XOFF, &peer_txq->state))
+ veth_bql_complete(state, peer_txq);
+ }

This snippet completes whenever the queue is empty and the netdev queue is stopped due to BQL.
So then the interval is smaller than 1ms.
I think, that is the reason why the pps is fine for 1ms in your benchmarks.

Quoted 1 line

I don't know how much this matters in practice. Not sure which architectures still hit the jiffies fallback.When in doubt we could disable BQL for those, or pin it to a "virtual queue size" via limit_min / limit_max similar to the v5 behavior and call queue_completed for every packet. Wdyt?

Yes, probably not relevant, I think I would just disable it for those architectures, avoiding possible regressions.

Quoted 10 lines
Quoted 6 lines

3. Inflight can be stuck at a value>0 for a long time when packet enqueueing stops. Only when packets are enqueued again, (on the next veth_xdp_rcv() call,) netdev_tx_completed_queue() is executed and inflight is set to 0 again.
Can also be seen when looking at the /sys BQL statistics.

This happens if the ring empties before the next queue_completed fires, right?

Exactly.

Quoted 3 lines

Another thing: Is it counterintuitive to set the tx_usec_coal on the receive device? Because this is a bql related config that is normally configured on the TX side?

Yes, I would say so.

Quoted 34 lines
Quoted 4 lines

BTW: Yesterday, I worked on and refactored the code into its own .h
file as a library and it also works fine for TUN/TAP (+vhost-net) for me :)

Nice! Thank you.
Jonas

Quoted 26 lines

Thanks for your work!
Simon

[1] Link: https://docs.kernel.org/timers/timekeeping.html

Quoted 20 lines

[1]https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark

Thanks,
Jonas

Quoted 15 lines

Thanks!

BTW: I think that this implementation could also work for other
software interfaces.

Quoted 10 lines

[2] Link:https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 6 lines

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

I understand..

#30 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 209 lines · Simon Schippers
Quoted 180 lines · Jonas Köppeler
Quoted 115 lines · Simon Schippers
Quoted 114 lines · Jonas Köppeler
Quoted 103 lines · Simon Schippers
Quoted 66 lines · Simon Schippers
Quoted 28 lines · Jesper Dangaard Brouer
Quoted 3 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 14 lines
Quoted 7 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1]https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2]https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 7 lines

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

I ran the benchmarks with --hist and I see what you mean. I have very similar results.

Is Jonas way [1] of modifiying pktgen maybe the best option to ensure that the producer is faster than the consumer?

[1] Link:https://lore.kernel.org/netdev/[email protected]/

Review

Quoted 7 lines

Hi, so what I found is that pktgen does not respect __QUEUE_STATE_STACK_OFF. So the test data above is invalid, since it just sent packets even if the BQL "stopped" the queue. So I patched pktgen with the following:

(quoted diff) +1 -1
-       if (unlikely(netif_xmit_frozen_or_drv_stopped(txq))) {
+       if (unlikely(netif_xmit_frozen_or_stopped(txq))) {

After thinking more about the implementation I see possible issues:

1. netdev_tx_completed_queue() never reports more than burst=64 packets:

BQL only increments the limit if the queue was starved. That means:
"The queue was over-limit in the last interval (the last time completion processing ran), and there is no more data in the queue (i.e. it’s empty)" [2]
But as only 64 packets are reported at max, the queue can only grow when it is <= 64 packets. And then it can only stay at a limit >64 until the next decrease of the limit.

2. netdev_tx_completed_queue() is called in irregular intervals:

Quoted 8 lines

If the consumer is slow it is called approx each tx_coal_usecs. But if the consumer is fast it is called way more frequent, probably in irregular intervals depending on the scheduling. However, "BQL depends on periodic completion interrupts" [2].

--> How about adding something like an interrupt that triggers every
10us and calls netdev_tx_completed_queue() with n_bql collected from (multiple) veth_xdp_rcv runs? That could solve 1. and 2.

Hi,

I worked on a new version (see attachment) that addresses both issues.

The major change is that instead of tracking the timestamp and packet count as local variables in veth_xdp_rcv(), they are now stored persistently in veth_rq as struct veth_bql_state. This allows completions to accumulate across multiple NAPI poll calls, so netdev_tx_completed_queue() can report more than 64 packets at once (see point 1). To get the time I am using (the fast) sched_clock() with a trick to avoid issues when switching between CPUs.

For point 2, the coalescing deadline is now checked both before the receive loop (to flush completions that timed out since the previous poll) and after each consumed packet, making completion intervals more regular. Still the intervals can be smaller than VETH_BQL_COAL_TX_USECS, but I guess this is fine.

I also found out that the BQL limit correlates closely with VETH_BQL_COAL_TX_USECS. It essentially reflects the latency we are targeting. I raised the default to 100 µs to allow DQL to converge to a higher limit (for reaching 255 in the testing below).

With the patched pktgen (respecting __QUEUE_STATE_STACK_XOFF), testing shows:
- --nrules 0: DQL limit reaches (up to) ~255
- --nrules 10000: DQL limit converges to ~0 (with --gro-disable)

Quoted 4 lines

These results are what I would expect from a BQL algorithm, but more testing is needed of course.

What do you think?

Hi,

This is exactly what I had in mind for implementing the BQL algorithm in this case. I did some testing with pktgen of this patch and also compared it to the v5 version.

Quoted 8 lines
Quoted 4 lines

You can find an extension of the benchmark script with pktgen here [1], as well as a wrapper script (veth_bql_bench.sh) to run the test script with and without --bql-disable to report the difference. I also configured pktgen to use the qdisc as suggested by Jesper.

Great, I will use your pktgen solution from now on.

Didn't know about the qdisc option, is there a performance difference with/without it? Or is it to have ping working next to pktgen?

Did not see any big difference performance-wise, but as you say, ping works better with pktgen then.

Quoted 46 lines

Consider to do a pull request :)

Quoted 3 lines

Note: bpftrace needs to be disabled, otherwise it becomes the bottleneck (at least on my machine) and pktgen throughput is halved when enabled.

Good to know.

Quoted 34 lines

Here are the results:

v5 (not time-based):
--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 1980871 2169898
Ping RTT avg (ms) 0.065 0.162
Throughput diff -8.7% // BQL 8.7% lower throughput
RTT diff -59.9% // BQL 60% lower latency
========================================

Simon's time-based version:

Test args: --nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 2166335 2153398
Ping RTT avg (ms) 0.165 0.165
Throughput diff 0.6%
RTT diff 0.0%

--pktgen --no-bpftrace --nrules 3500
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------

Quoted 5 lines
Throughput (pps)            28569        28696
Ping RTT avg (ms)           1.327        8.409
Throughput diff             -0.4%

RTT diff -84.2%

Quoted 2 lines

I think we should run benchmarks against the stock net-next to be safe.

--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
net-next

Quoted 9 lines
---                  -------
Throughput (pps)     2285421
Ping RTT avg (ms)    0.161

(slightly adjusted the output to better communicate the results)

So in my case this means BQL implementation has ~5% lower throughput compared to net-next. But please double check.

Yes, I will benchmark myself.

There are probably some places where we can gain performance. For example, I see ptr_ring_empty() which could be swapped for __ptr_ring_empty() which would save a spinlock and unlock.

Quoted 10 lines
Quoted 10 lines
Quoted 1 line

Seems to work now as expected.

Yes, but I think we have to keep these points in mind:

1. Limit/Inflight can be bigger than VETH_RING_SIZE, because packets can be enqueued in the same time as they are read out, so netdev_tx_completed_queue() can theoretically be called with many number of packets.
I do not think it is deal-breaking though.
I could see such high limits/inflights when looking at the /sys BQL statistics..

Quoted 3 lines

For me this makes sense, that inflight just means the number of packets not yet 'completed' or the number of packets that you can send between two completion calls. I think this is not specific

Quoted 6 lines

From my understanding, I do think that this behavior is
pretty specific.

Typically BQL-enabled NIC drivers clear packets out of some internal buffer in their completion interrupt (or something similar). And after that they call netdev_tx_completed_queue().

You are right, because descriptors are freed at the same time when they are accounted for.

Quoted 5 lines
Quoted 3 lines

to this implementation. But for long intervals this might result in some problems because you can just fill the veth_ring to its capacity quickly, and increasing latency if the receiver is slow.

Yes, but I think the latency can only be approx. as big as the interval (can be higher with GRO enabled).

I think it's more like ~2.x times added latency in the worst case, which is normal BQL behavior, but for large intervals 2x is a lot. Of course this value is also capped by the time it takes to process the 256 packets that fit in the ring buffer.

For example, for a test run with fq_codel and 20K rules, and a ping baseline of 1.7 ms without pktgen load, the results are:
tx-usecs p99_ping_rtt (ms)
-------- -----------------

     0           5.223
   100           6.910
   500           7.197
  1000           6.967
  5000          16.233
 10000          25.033
 20000          44.133

Also, the interval is sometimes dominated by the processing time of a packet batch (and other effects). The default is 8 packets/batch (see gro_normal_batch). This means that for 7 packets you see processing times on the order of a few hundred nanoseconds, but for the 8th you see the processing time of the whole batch (see table below). With a ruleset > 1000 entries, this exceeds the default tx-usecs value, which is why the p99 RTT increase is even larger than 2x tx-usecs -- as you can see in the table for tx-usecs values 0-5000.

tx-usecs 0 nrules 0
Percentile per-packet processing time (µs)
---------- -------------

    p5             0.316
   p25             0.382
   p50             0.442
   p75             0.508 -- fast enqueue to skb_list

p95 68.304 -- batch processing time

Setting gro_normal_batch to 1 makes the interval times more accurate and improves p99 RTT slightly, though it's still > 2x the tx-usecs latency increase.

However, using smaller values > 0 means that you can benefit from bql, but the actual interval might be larger than the configured value. I do not think this is a deal breaker, but something worth noting where the latencies come from.

Also worth noting: using fq_codel is better to use in this case because it can fast track sparse flows. using sfq the ping packet can get head of line blocked by the current quantum of sfq, if my understanding is correct.

Quoted 6 lines
Quoted 5 lines

To illustrate this have a look at [1]. There are some plots that show the rtt vs. tx-usec config depending on nrules.

[1] https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark/results/tx-usecs

Nice plots! Great for finding a sane default value.

I did some more measurements to better understand whats happening. I will upload the results latest by tomorrow to the github repository mentioned above.

Quoted 12 lines
Quoted 12 lines
Quoted 12 lines

2. sched_clock() is only valid on the same CPU. When a different CPU starts executing its sched_clock() can be in the past compared to the sched_clock() value saved by the previous CPU.
My trick...
min(s->time, sched_clock())
... avoids potentially extremely long intervals between netdev_tx_completed_queue() calls but is not perfect of course. I think CPU hopping happens rarely enough for this to matter.. And also we have to keep this in mind [1]:
"An architecture may or may not provide an implementation of sched_clock() on its own. If a local implementation is not provided, the system jiffy counter will be used as sched_clock()."

Quoted 8 lines
Quoted 6 lines

So the problem with this is that with jiffies you have like millisecond interval granularity, which might be too long in order to work properly. Given the receiver completes 128 packet in 1ms (queue_completed interval), the bql will set the limit at 256. Then the tx thread can quickly fill the ring, and it then basically stopped until the 1ms interval is over.

Linux Mint only has 100Hz jiffies :^)

ok so 10ms granularity. Effectively disables BQL for the most part. However, you can still benefit if your receive side is very slow.

Quoted 9 lines

I think you misunderstand something.

drivers/net/veth.c +5 -0
+	if (peer_txq && state->n_bql && ptr_ring_empty(&rq->xdp_ring)) {
+		smp_rmb();
+		if (test_bit(__QUEUE_STATE_STACK_XOFF, &peer_txq->state))
+			veth_bql_complete(state, peer_txq);
+	}
Quoted 5 lines

This snippet completes whenever the queue is empty and the netdev queue is stopped due to BQL.
So then the interval is smaller than 1ms.
I think, that is the reason why the pps is fine for 1ms in your benchmarks.

Without that snippet the queue stalls during ramp-up and throughput never gets off the ground. But once the BQL limit has stabilized, the snippet is no longer invoked -- with --nrules 0 the queue is never stopped, and with a slow receiver the queue is not empty. So the snippet is needed for correct ramp-up, but it doesn't affect steady-state throughput in these tests. This is also confirmed by a test where I measure the time between intervals. For 1000us across all nrules configurations the time ranges between 1000-1400us.

The variance of the time between intervals is also quite good in general, but not always exact the configured value. For most cases +-5% from p50 value of the real interval.
Migrations between CPUs can also shorten an interval: if the new CPU's sched_clock() is ahead of the previous one (say by 15 ms), the apparent elapsed time within the current interval is inflated, so only ~5 ms of real time remains before the deadline fires (with the configured tx-usecs at 20 ms). Without migrations the interval stays consistently close to the configured value. I don't think this is a problem in practice -- it just adds some variance to the p5 of the measured interval.

Quoted 50 lines
Quoted 1 line

I don't know how much this matters in practice. Not sure which architectures still hit the jiffies fallback.When in doubt we could disable BQL for those, or pin it to a "virtual queue size" via limit_min / limit_max similar to the v5 behavior and call queue_completed for every packet. Wdyt?

Yes, probably not relevant, I think I would just disable it for those architectures, avoiding possible regressions.

Quoted 8 lines
Quoted 5 lines

3. Inflight can be stuck at a value>0 for a long time when packet enqueueing stops. Only when packets are enqueued again, (on the next veth_xdp_rcv() call,) netdev_tx_completed_queue() is executed and inflight is set to 0 again.
Can also be seen when looking at the /sys BQL statistics.

This happens if the ring empties before the next queue_completed fires, right?

Exactly.

Quoted 3 lines

Another thing: Is it counterintuitive to set the tx_usec_coal on the receive device? Because this is a bql related config that is normally configured on the TX side?

Yes, I would say so.

Quoted 31 lines
Quoted 3 lines

BTW: Yesterday, I worked on and refactored the code into its own .h
file as a library and it also works fine for TUN/TAP (+vhost-net) for me :)

Nice! Thank you.
Jonas

Quoted 25 lines

Thanks for your work!
Simon

[1] Link: https://docs.kernel.org/timers/timekeeping.html

Quoted 20 lines

[1]https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark

Thanks,
Jonas

Quoted 15 lines

Thanks!

BTW: I think that this implementation could also work for other
software interfaces.

Quoted 10 lines

[2] Link:https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 6 lines

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

I understand..

#31 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 260 lines · Jonas Köppeler
Quoted 209 lines · Simon Schippers
Quoted 180 lines · Jonas Köppeler
Quoted 115 lines · Simon Schippers
Quoted 114 lines · Jonas Köppeler
Quoted 103 lines · Simon Schippers
Quoted 66 lines · Simon Schippers
Quoted 28 lines · Jesper Dangaard Brouer
Quoted 3 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 14 lines
Quoted 7 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1]https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2]https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 7 lines

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

I ran the benchmarks with --hist and I see what you mean. I have very similar results.

Is Jonas way [1] of modifiying pktgen maybe the best option to ensure that the producer is faster than the consumer?

[1] Link:https://lore.kernel.org/netdev/[email protected]/

Review

Quoted 7 lines

Hi, so what I found is that pktgen does not respect __QUEUE_STATE_STACK_OFF. So the test data above is invalid, since it just sent packets even if the BQL "stopped" the queue. So I patched pktgen with the following:

(quoted diff) +1 -1
-       if (unlikely(netif_xmit_frozen_or_drv_stopped(txq))) {
+       if (unlikely(netif_xmit_frozen_or_stopped(txq))) {

After thinking more about the implementation I see possible issues:

1. netdev_tx_completed_queue() never reports more than burst=64 packets:

BQL only increments the limit if the queue was starved. That means:
"The queue was over-limit in the last interval (the last time completion processing ran), and there is no more data in the queue (i.e. it’s empty)" [2]
But as only 64 packets are reported at max, the queue can only grow when it is <= 64 packets. And then it can only stay at a limit >64 until the next decrease of the limit.

2. netdev_tx_completed_queue() is called in irregular intervals:

Quoted 8 lines

If the consumer is slow it is called approx each tx_coal_usecs. But if the consumer is fast it is called way more frequent, probably in irregular intervals depending on the scheduling. However, "BQL depends on periodic completion interrupts" [2].

--> How about adding something like an interrupt that triggers every
10us and calls netdev_tx_completed_queue() with n_bql collected from (multiple) veth_xdp_rcv runs? That could solve 1. and 2.

Hi,

I worked on a new version (see attachment) that addresses both issues.

The major change is that instead of tracking the timestamp and packet count as local variables in veth_xdp_rcv(), they are now stored persistently in veth_rq as struct veth_bql_state. This allows completions to accumulate across multiple NAPI poll calls, so netdev_tx_completed_queue() can report more than 64 packets at once (see point 1). To get the time I am using (the fast) sched_clock() with a trick to avoid issues when switching between CPUs.

For point 2, the coalescing deadline is now checked both before the receive loop (to flush completions that timed out since the previous poll) and after each consumed packet, making completion intervals more regular. Still the intervals can be smaller than VETH_BQL_COAL_TX_USECS, but I guess this is fine.

I also found out that the BQL limit correlates closely with VETH_BQL_COAL_TX_USECS. It essentially reflects the latency we are targeting. I raised the default to 100 µs to allow DQL to converge to a higher limit (for reaching 255 in the testing below).

With the patched pktgen (respecting __QUEUE_STATE_STACK_XOFF), testing shows:
- --nrules 0: DQL limit reaches (up to) ~255
- --nrules 10000: DQL limit converges to ~0 (with --gro-disable)

Quoted 4 lines

These results are what I would expect from a BQL algorithm, but more testing is needed of course.

What do you think?

Hi,

This is exactly what I had in mind for implementing the BQL algorithm in this case. I did some testing with pktgen of this patch and also compared it to the v5 version.

Quoted 8 lines
Quoted 4 lines

You can find an extension of the benchmark script with pktgen here [1], as well as a wrapper script (veth_bql_bench.sh) to run the test script with and without --bql-disable to report the difference. I also configured pktgen to use the qdisc as suggested by Jesper.

Great, I will use your pktgen solution from now on.

Didn't know about the qdisc option, is there a performance difference with/without it? Or is it to have ping working next to pktgen?

Did not see any big difference performance-wise, but as you say, ping works better with pktgen then.

Quoted 46 lines

Consider to do a pull request :)

Quoted 3 lines

Note: bpftrace needs to be disabled, otherwise it becomes the bottleneck (at least on my machine) and pktgen throughput is halved when enabled.

Good to know.

Quoted 34 lines

Here are the results:

v5 (not time-based):
--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 1980871 2169898
Ping RTT avg (ms) 0.065 0.162
Throughput diff -8.7% // BQL 8.7% lower throughput
RTT diff -59.9% // BQL 60% lower latency
========================================

Simon's time-based version:

Test args: --nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 2166335 2153398
Ping RTT avg (ms) 0.165 0.165
Throughput diff 0.6%
RTT diff 0.0%

--pktgen --no-bpftrace --nrules 3500
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------

Quoted 5 lines
Throughput (pps)            28569        28696
Ping RTT avg (ms)           1.327        8.409
Throughput diff             -0.4%

RTT diff -84.2%

Quoted 2 lines

I think we should run benchmarks against the stock net-next to be safe.

--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
net-next

Quoted 9 lines
---                  -------
Throughput (pps)     2285421
Ping RTT avg (ms)    0.161

(slightly adjusted the output to better communicate the results)

So in my case this means BQL implementation has ~5% lower throughput compared to net-next. But please double check.

Yes, I will benchmark myself.

There are probably some places where we can gain performance. For example, I see ptr_ring_empty() which could be swapped for __ptr_ring_empty() which would save a spinlock and unlock.

Quoted 10 lines
Quoted 10 lines
Quoted 1 line

Seems to work now as expected.

Yes, but I think we have to keep these points in mind:

1. Limit/Inflight can be bigger than VETH_RING_SIZE, because packets can be enqueued in the same time as they are read out, so netdev_tx_completed_queue() can theoretically be called with many number of packets.
I do not think it is deal-breaking though.
I could see such high limits/inflights when looking at the /sys BQL statistics..

Quoted 3 lines

For me this makes sense, that inflight just means the number of packets not yet 'completed' or the number of packets that you can send between two completion calls. I think this is not specific

Quoted 6 lines

From my understanding, I do think that this behavior is
pretty specific.

Typically BQL-enabled NIC drivers clear packets out of some internal buffer in their completion interrupt (or something similar). And after that they call netdev_tx_completed_queue().

You are right, because descriptors are freed at the same time when they are accounted for.

Quoted 5 lines
Quoted 3 lines

to this implementation. But for long intervals this might result in some problems because you can just fill the veth_ring to its capacity quickly, and increasing latency if the receiver is slow.

Yes, but I think the latency can only be approx. as big as the interval (can be higher with GRO enabled).

I think it's more like ~2.x times added latency in the worst case, which is normal BQL behavior, but for large intervals 2x is a lot. Of course this value is also capped by the time it takes to process the 256 packets that fit in the ring buffer.

For example, for a test run with fq_codel and 20K rules, and a ping baseline of 1.7 ms without pktgen load, the results are:
tx-usecs p99_ping_rtt (ms)
-------- -----------------

     0           5.223
   100           6.910
   500           7.197
  1000           6.967
  5000          16.233
 10000          25.033
 20000          44.133

Also, the interval is sometimes dominated by the processing time of a packet batch (and other effects). The default is 8 packets/batch (see gro_normal_batch). This means that for 7 packets you see processing times on the order of a few hundred nanoseconds, but for the 8th you see the processing time of the whole batch (see table below). With a ruleset > 1000 entries, this exceeds the default tx-usecs value, which is why the p99 RTT increase is even larger than 2x tx-usecs -- as you can see in the table for tx-usecs values 0-5000.

tx-usecs 0 nrules 0
Percentile per-packet processing time (µs)
---------- -------------

    p5             0.316
   p25             0.382
   p50             0.442
   p75             0.508 -- fast enqueue to skb_list

p95 68.304 -- batch processing time

Quoted 8 lines

Setting gro_normal_batch to 1 makes the interval times more accurate and improves p99 RTT slightly, though it's still > 2x the tx-usecs latency increase.

However, using smaller values > 0 means that you can benefit from bql, but the actual interval might be larger than the configured value. I do not think this is a deal breaker, but something worth noting where the latencies come from.

I understand and agree with your points.
I also do not see anything deal-breaking. Yes, there is no guarantee for the latency, but BQL is best-effort anyway.

I will wait for your new measurements, but there is no argument against a default tx-usecs of ~100us for now, right?

Quoted 5 lines

Also worth noting: using fq_codel is better to use in this case because it can fast track sparse flows. using sfq the ping packet can get head of line blocked by the current quantum of sfq, if my understanding is correct.

Why don't we use a prio qdisc where ping gets prioritized? Then we do not have to worry about head of line blocking on the qdisc layer and instead can concentrate on veth.

Quoted 31 lines
Quoted 7 lines
Quoted 6 lines

To illustrate this have a look at [1]. There are some plots that show the rtt vs. tx-usec config depending on nrules.

[1] https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark/results/tx-usecs

Nice plots! Great for finding a sane default value.

I did some more measurements to better understand whats happening. I will upload the results latest by tomorrow to the github repository mentioned above.

Quoted 19 lines
Quoted 18 lines
Quoted 4 lines

2. sched_clock() is only valid on the same CPU. When a different CPU starts executing its sched_clock() can be in the past compared to the sched_clock() value saved by the previous CPU. My trick...

Quoted 8 lines

min(s->time, sched_clock())
... avoids potentially extremely long intervals between netdev_tx_completed_queue() calls but is not perfect of course. I think CPU hopping happens rarely enough for this to matter.. And also we have to keep this in mind [1]:
"An architecture may or may not provide an implementation of sched_clock() on its own. If a local implementation is not provided, the system jiffy counter will be used as sched_clock()."

So the problem with this is that with jiffies you have like millisecond interval granularity, which might be too long in order to work properly. Given the receiver completes 128 packet in 1ms (queue_completed interval), the bql will set the limit at 256. Then the tx thread can quickly fill the ring, and it then basically stopped until the 1ms interval is over.

Quoted 9 lines
Quoted 1 line

Linux Mint only has 100Hz jiffies :^)

ok so 10ms granularity. Effectively disables BQL for the most part. However, you can still benefit if your receive side is very slow.

Quoted 4 lines

I think you misunderstand something.

+ if (peer_txq && state->n_bql && ptr_ring_empty(&rq->xdp_ring)) {

            // Pairs with smp_wmb() in __ptr_ring_produce()
Quoted 11 lines
Quoted 10 lines
drivers/net/veth.c +4 -0
+        smp_rmb();
+        if (test_bit(__QUEUE_STATE_STACK_XOFF, &peer_txq->state))
+            veth_bql_complete(state, peer_txq);
+    }

This snippet completes whenever the queue is empty and the netdev queue is stopped due to BQL.
So then the interval is smaller than 1ms.
I think, that is the reason why the pps is fine for 1ms in your benchmarks.

Quoted 9 lines

Without that snippet the queue stalls during ramp-up and throughput never gets off the ground. But once the BQL limit has stabilized, the snippet is no longer invoked -- with --nrules 0 the queue is never stopped, and with a slow receiver the queue is not empty. So the snippet is needed for correct ramp-up, but it doesn't affect steady-state throughput in these tests. This is also confirmed by a test where I measure the time between intervals. For 1000us across all nrules configurations the time ranges between 1000-1400us.

Yes, I agree. It only acts in the case of starvation (see [1]) and I think it is an advantage that we react faster compared to other BQL implementations.

Additionally the snippet is *required* because if the ring is empty and the txq stopped due to BQL, there are no more veth_xdp_recv() calls, permanently stalling everything. That is the reason for the memory barrier pairing in the snippet.

[1] Link: https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 12 lines

The variance of the time between intervals is also quite good in general, but not always exact the configured value. For most cases +-5% from p50 value of the real interval.
Migrations between CPUs can also shorten an interval: if the new CPU's sched_clock() is ahead of the previous one (say by 15 ms), the apparent elapsed time within the current interval is inflated, so only ~5 ms of real time remains before the deadline fires (with the configured tx-usecs at 20 ms). Without migrations the interval stays consistently close to the configured value. I don't think this is a problem in practice -- it just adds some variance to the p5 of the measured interval.

Yes, and then we mistakenly decrease the BQL limit once. But we react fast on that starvation. So its fine.

Quoted 51 lines
Quoted 51 lines
Quoted 2 lines

I don't know how much this matters in practice. Not sure which architectures still hit the jiffies fallback.When in doubt we could disable BQL for those, or pin it to a "virtual queue size" via limit_min / limit_max similar to the v5 behavior and call queue_completed for every packet. Wdyt?

Yes, probably not relevant, I think I would just disable it for those architectures, avoiding possible regressions.

Quoted 8 lines
Quoted 5 lines

3. Inflight can be stuck at a value>0 for a long time when packet enqueueing stops. Only when packets are enqueued again, (on the next veth_xdp_rcv() call,) netdev_tx_completed_queue() is executed and inflight is set to 0 again.
Can also be seen when looking at the /sys BQL statistics.

This happens if the ring empties before the next queue_completed fires, right?

Exactly.

Quoted 3 lines

Another thing: Is it counterintuitive to set the tx_usec_coal on the receive device? Because this is a bql related config that is normally configured on the TX side?

Yes, I would say so.

Quoted 31 lines
Quoted 3 lines

BTW: Yesterday, I worked on and refactored the code into its own .h
file as a library and it also works fine for TUN/TAP (+vhost-net) for me :)

Nice! Thank you.
Jonas

Quoted 25 lines

Thanks for your work!
Simon

[1] Link: https://docs.kernel.org/timers/timekeeping.html

Quoted 20 lines

[1]https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark

Thanks,
Jonas

Quoted 15 lines

Thanks!

BTW: I think that this implementation could also work for other
software interfaces.

Quoted 10 lines

[2] Link:https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 6 lines

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

I understand..

#32 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 267 lines · Simon Schippers
Quoted 258 lines · Jonas Köppeler
Quoted 209 lines · Simon Schippers
Quoted 180 lines · Jonas Köppeler
Quoted 115 lines · Simon Schippers
Quoted 114 lines · Jonas Köppeler
Quoted 103 lines · Simon Schippers
Quoted 66 lines · Simon Schippers
Quoted 28 lines · Jesper Dangaard Brouer
Quoted 3 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 14 lines
Quoted 7 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1]https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2]https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 7 lines

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

I ran the benchmarks with --hist and I see what you mean. I have very similar results.

Is Jonas way [1] of modifiying pktgen maybe the best option to ensure that the producer is faster than the consumer?

[1] Link:https://lore.kernel.org/netdev/[email protected]/

Review

Quoted 7 lines

Hi, so what I found is that pktgen does not respect __QUEUE_STATE_STACK_OFF. So the test data above is invalid, since it just sent packets even if the BQL "stopped" the queue. So I patched pktgen with the following:

(quoted diff) +1 -1
-       if (unlikely(netif_xmit_frozen_or_drv_stopped(txq))) {
+       if (unlikely(netif_xmit_frozen_or_stopped(txq))) {

After thinking more about the implementation I see possible issues:

1. netdev_tx_completed_queue() never reports more than burst=64 packets:

BQL only increments the limit if the queue was starved. That means:
"The queue was over-limit in the last interval (the last time completion processing ran), and there is no more data in the queue (i.e. it’s empty)" [2]
But as only 64 packets are reported at max, the queue can only grow when it is <= 64 packets. And then it can only stay at a limit >64 until the next decrease of the limit.

2. netdev_tx_completed_queue() is called in irregular intervals:

Quoted 8 lines

If the consumer is slow it is called approx each tx_coal_usecs. But if the consumer is fast it is called way more frequent, probably in irregular intervals depending on the scheduling. However, "BQL depends on periodic completion interrupts" [2].

--> How about adding something like an interrupt that triggers every
10us and calls netdev_tx_completed_queue() with n_bql collected from (multiple) veth_xdp_rcv runs? That could solve 1. and 2.

Hi,

I worked on a new version (see attachment) that addresses both issues.

The major change is that instead of tracking the timestamp and packet count as local variables in veth_xdp_rcv(), they are now stored persistently in veth_rq as struct veth_bql_state. This allows completions to accumulate across multiple NAPI poll calls, so netdev_tx_completed_queue() can report more than 64 packets at once (see point 1). To get the time I am using (the fast) sched_clock() with a trick to avoid issues when switching between CPUs.

For point 2, the coalescing deadline is now checked both before the receive loop (to flush completions that timed out since the previous poll) and after each consumed packet, making completion intervals more regular. Still the intervals can be smaller than VETH_BQL_COAL_TX_USECS, but I guess this is fine.

I also found out that the BQL limit correlates closely with VETH_BQL_COAL_TX_USECS. It essentially reflects the latency we are targeting. I raised the default to 100 µs to allow DQL to converge to a higher limit (for reaching 255 in the testing below).

With the patched pktgen (respecting __QUEUE_STATE_STACK_XOFF), testing shows:
- --nrules 0: DQL limit reaches (up to) ~255
- --nrules 10000: DQL limit converges to ~0 (with --gro-disable)

Quoted 4 lines

These results are what I would expect from a BQL algorithm, but more testing is needed of course.

What do you think?

Hi,

This is exactly what I had in mind for implementing the BQL algorithm in this case. I did some testing with pktgen of this patch and also compared it to the v5 version.

Quoted 8 lines
Quoted 4 lines

You can find an extension of the benchmark script with pktgen here [1], as well as a wrapper script (veth_bql_bench.sh) to run the test script with and without --bql-disable to report the difference. I also configured pktgen to use the qdisc as suggested by Jesper.

Great, I will use your pktgen solution from now on.

Didn't know about the qdisc option, is there a performance difference with/without it? Or is it to have ping working next to pktgen?

Did not see any big difference performance-wise, but as you say, ping works better with pktgen then.

Quoted 46 lines

Consider to do a pull request :)

Quoted 3 lines

Note: bpftrace needs to be disabled, otherwise it becomes the bottleneck (at least on my machine) and pktgen throughput is halved when enabled.

Good to know.

Quoted 34 lines

Here are the results:

v5 (not time-based):
--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 1980871 2169898
Ping RTT avg (ms) 0.065 0.162
Throughput diff -8.7% // BQL 8.7% lower throughput
RTT diff -59.9% // BQL 60% lower latency
========================================

Simon's time-based version:

Test args: --nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 2166335 2153398
Ping RTT avg (ms) 0.165 0.165
Throughput diff 0.6%
RTT diff 0.0%

--pktgen --no-bpftrace --nrules 3500
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------

Quoted 5 lines
Throughput (pps)            28569        28696
Ping RTT avg (ms)           1.327        8.409
Throughput diff             -0.4%

RTT diff -84.2%

Quoted 2 lines

I think we should run benchmarks against the stock net-next to be safe.

--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
net-next

Quoted 9 lines
---                  -------
Throughput (pps)     2285421
Ping RTT avg (ms)    0.161

(slightly adjusted the output to better communicate the results)

So in my case this means BQL implementation has ~5% lower throughput compared to net-next. But please double check.

Yes, I will benchmark myself.

There are probably some places where we can gain performance. For example, I see ptr_ring_empty() which could be swapped for __ptr_ring_empty() which would save a spinlock and unlock.

Quoted 10 lines
Quoted 10 lines
Quoted 1 line

Seems to work now as expected.

Yes, but I think we have to keep these points in mind:

1. Limit/Inflight can be bigger than VETH_RING_SIZE, because packets can be enqueued in the same time as they are read out, so netdev_tx_completed_queue() can theoretically be called with many number of packets.
I do not think it is deal-breaking though.
I could see such high limits/inflights when looking at the /sys BQL statistics..

Quoted 3 lines

For me this makes sense, that inflight just means the number of packets not yet 'completed' or the number of packets that you can send between two completion calls. I think this is not specific

Quoted 6 lines

From my understanding, I do think that this behavior is
pretty specific.

Typically BQL-enabled NIC drivers clear packets out of some internal buffer in their completion interrupt (or something similar). And after that they call netdev_tx_completed_queue().

You are right, because descriptors are freed at the same time when they are accounted for.

Quoted 5 lines
Quoted 3 lines

to this implementation. But for long intervals this might result in some problems because you can just fill the veth_ring to its capacity quickly, and increasing latency if the receiver is slow.

Yes, but I think the latency can only be approx. as big as the interval (can be higher with GRO enabled).

I think it's more like ~2.x times added latency in the worst case, which is normal BQL behavior, but for large intervals 2x is a lot. Of course this value is also capped by the time it takes to process the 256 packets that fit in the ring buffer.

For example, for a test run with fq_codel and 20K rules, and a ping baseline of 1.7 ms without pktgen load, the results are:
tx-usecs p99_ping_rtt (ms)
-------- -----------------

      0           5.223
    100           6.910
    500           7.197
   1000           6.967
   5000          16.233
  10000          25.033
  20000          44.133

Also, the interval is sometimes dominated by the processing time of a packet batch (and other effects). The default is 8 packets/batch (see gro_normal_batch). This means that for 7 packets you see processing times on the order of a few hundred nanoseconds, but for the 8th you see the processing time of the whole batch (see table below). With a ruleset > 1000 entries, this exceeds the default tx-usecs value, which is why the p99 RTT increase is even larger than 2x tx-usecs -- as you can see in the table for tx-usecs values 0-5000.

tx-usecs 0 nrules 0
Percentile per-packet processing time (µs)
---------- -------------

     p5             0.316
    p25             0.382
    p50             0.442
    p75             0.508 -- fast enqueue to skb_list

p95 68.304 -- batch processing time

Quoted 8 lines

Setting gro_normal_batch to 1 makes the interval times more accurate and improves p99 RTT slightly, though it's still > 2x the tx-usecs latency increase.

However, using smaller values > 0 means that you can benefit from bql, but the actual interval might be larger than the configured value. I do not think this is a deal breaker, but something worth noting where the latencies come from.

Quoted 7 lines

I understand and agree with your points.
I also do not see anything deal-breaking. Yes, there is no guarantee for the latency, but BQL is best-effort anyway.

I will wait for your new measurements, but there is no argument against a default tx-usecs of ~100us for now, right?

Yes, I think 100us is perfectly fine. I guess most of it was just my curiosity why the latency values are as they are :) But it feels like this will need some documentation, because as we have seen, some values are a little different from what you expect from bql. Inflight > veth_ring_size, tx-usecs not necessarily achieving the configured value, inflight can get stuck > 0. Wdyt?
But I think it works nicely overall.

Quoted 139 lines
Quoted 4 lines

Also worth noting: using fq_codel is better to use in this case because it can fast track sparse flows. using sfq the ping packet can get head of line blocked by the current quantum of sfq, if my understanding is correct.

Why don't we use a prio qdisc where ping gets prioritized? Then we do not have to worry about head of line blocking on the qdisc layer and instead can concentrate on veth.

Quoted 28 lines
Quoted 6 lines
Quoted 5 lines

To illustrate this have a look at [1]. There are some plots that show the rtt vs. tx-usec config depending on nrules.

[1] https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark/results/tx-usecs

Nice plots! Great for finding a sane default value.

I did some more measurements to better understand whats happening. I will upload the results latest by tomorrow to the github repository mentioned above.

Quoted 18 lines
Quoted 18 lines
Quoted 4 lines

2. sched_clock() is only valid on the same CPU. When a different CPU starts executing its sched_clock() can be in the past compared to the sched_clock() value saved by the previous CPU. My trick...

Quoted 8 lines

min(s->time, sched_clock())
... avoids potentially extremely long intervals between netdev_tx_completed_queue() calls but is not perfect of course. I think CPU hopping happens rarely enough for this to matter.. And also we have to keep this in mind [1]:
"An architecture may or may not provide an implementation of sched_clock() on its own. If a local implementation is not provided, the system jiffy counter will be used as sched_clock()."

So the problem with this is that with jiffies you have like millisecond interval granularity, which might be too long in order to work properly. Given the receiver completes 128 packet in 1ms (queue_completed interval), the bql will set the limit at 256. Then the tx thread can quickly fill the ring, and it then basically stopped until the 1ms interval is over.

Quoted 7 lines
Quoted 1 line

Linux Mint only has 100Hz jiffies :^)

ok so 10ms granularity. Effectively disables BQL for the most part. However, you can still benefit if your receive side is very slow.

Quoted 3 lines

I think you misunderstand something.

+ if (peer_txq && state->n_bql && ptr_ring_empty(&rq->xdp_ring)) {

             // Pairs with smp_wmb() in __ptr_ring_produce()
Quoted 10 lines
Quoted 10 lines
(quoted diff) +4 -0
+        smp_rmb();
+        if (test_bit(__QUEUE_STATE_STACK_XOFF, &peer_txq->state))
+            veth_bql_complete(state, peer_txq);
+    }

This snippet completes whenever the queue is empty and the netdev queue is stopped due to BQL.
So then the interval is smaller than 1ms.
I think, that is the reason why the pps is fine for 1ms in your benchmarks.

Quoted 9 lines

Without that snippet the queue stalls during ramp-up and throughput never gets off the ground. But once the BQL limit has stabilized, the snippet is no longer invoked -- with --nrules 0 the queue is never stopped, and with a slow receiver the queue is not empty. So the snippet is needed for correct ramp-up, but it doesn't affect steady-state throughput in these tests. This is also confirmed by a test where I measure the time between intervals. For 1000us across all nrules configurations the time ranges between 1000-1400us.

Yes, I agree. It only acts in the case of starvation (see [1]) and I think it is an advantage that we react faster compared to other BQL implementations.

Additionally the snippet is *required* because if the ring is empty and the txq stopped due to BQL, there are no more veth_xdp_recv() calls, permanently stalling everything. That is the reason for the memory barrier pairing in the snippet.

[1] Link: https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 11 lines

The variance of the time between intervals is also quite good in general, but not always exact the configured value. For most cases +-5% from p50 value of the real interval.
Migrations between CPUs can also shorten an interval: if the new CPU's sched_clock() is ahead of the previous one (say by 15 ms), the apparent elapsed time within the current interval is inflated, so only ~5 ms of real time remains before the deadline fires (with the configured tx-usecs at 20 ms). Without migrations the interval stays consistently close to the configured value. I don't think this is a problem in practice -- it just adds some variance to the p5 of the measured interval.

Yes, and then we mistakenly decrease the BQL limit once. But we react fast on that starvation. So its fine.

Quoted 50 lines
Quoted 50 lines
Quoted 1 line

I don't know how much this matters in practice. Not sure which architectures still hit the jiffies fallback.When in doubt we could disable BQL for those, or pin it to a "virtual queue size" via limit_min / limit_max similar to the v5 behavior and call queue_completed for every packet. Wdyt?

Yes, probably not relevant, I think I would just disable it for those architectures, avoiding possible regressions.

Quoted 8 lines
Quoted 5 lines

3. Inflight can be stuck at a value>0 for a long time when packet enqueueing stops. Only when packets are enqueued again, (on the next veth_xdp_rcv() call,) netdev_tx_completed_queue() is executed and inflight is set to 0 again.
Can also be seen when looking at the /sys BQL statistics.

This happens if the ring empties before the next queue_completed fires, right?

Exactly.

Quoted 3 lines

Another thing: Is it counterintuitive to set the tx_usec_coal on the receive device? Because this is a bql related config that is normally configured on the TX side?

Yes, I would say so.

Quoted 31 lines
Quoted 3 lines

BTW: Yesterday, I worked on and refactored the code into its own .h
file as a library and it also works fine for TUN/TAP (+vhost-net) for me :)

Nice! Thank you.
Jonas

Quoted 25 lines

Thanks for your work!
Simon

[1] Link: https://docs.kernel.org/timers/timekeeping.html

Quoted 20 lines

[1]https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark

Thanks,
Jonas

Quoted 15 lines

Thanks!

BTW: I think that this implementation could also work for other
software interfaces.

Quoted 10 lines

[2] Link:https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 6 lines

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

I understand..

#33 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 273 lines · Jonas Köppeler
Quoted 272 lines · Simon Schippers
Quoted 258 lines · Jonas Köppeler
Quoted 209 lines · Simon Schippers
Quoted 180 lines · Jonas Köppeler
Quoted 115 lines · Simon Schippers
Quoted 114 lines · Jonas Köppeler
Quoted 103 lines · Simon Schippers
Quoted 66 lines · Simon Schippers
Quoted 28 lines · Jesper Dangaard Brouer
Quoted 3 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 14 lines
Quoted 7 lines

I moved the selftests into a github repo [1] to allow us to collaborate and evaluate the changes more easily. I explicitly kept the new BPF based BQL tracking as a commit[2] for your benefit.

[1]https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[2]https://github.com/netoptimizer/veth-backpressure-performance-testing/commit/f25c5dc92977

Thanks for sharing. After minor issues I was able to set it up (currently I am just using plain v5, will look at the coalescing patch when I find the time):

Can confirm the latency reduction with the default settings, in my case 4.888ms to 0.241ms.

Quoted 7 lines

With the same script I was also able to see a performance slow down:
veth_bql_test_virtme.sh --qdisc fq_codel --nrules 0
--> ~510 Kpps
Same with --bql-disable
--> ~570 Kpps
--> 12% faster

Quoted 7 lines

Thanks for running these benchmarks.

Notice that --nrules 0 can easily result in no-queuing (on average), because the veth NAPI consumer is faster than the producer. You will likely see BQL inflight=1 and sink reported avg latency very low (remember it okay that sink get high latency penalty as long at ping latency remains low, as that show AQM is working).

I ran the benchmarks with --hist and I see what you mean. I have very similar results.

Is Jonas way [1] of modifiying pktgen maybe the best option to ensure that the producer is faster than the consumer?

[1] Link:https://lore.kernel.org/netdev/[email protected]/

Review

Quoted 7 lines

Hi, so what I found is that pktgen does not respect __QUEUE_STATE_STACK_OFF. So the test data above is invalid, since it just sent packets even if the BQL "stopped" the queue. So I patched pktgen with the following:

(quoted diff) +1 -1
-       if (unlikely(netif_xmit_frozen_or_drv_stopped(txq))) {
+       if (unlikely(netif_xmit_frozen_or_stopped(txq))) {

After thinking more about the implementation I see possible issues:

1. netdev_tx_completed_queue() never reports more than burst=64 packets:

BQL only increments the limit if the queue was starved. That means:
"The queue was over-limit in the last interval (the last time completion processing ran), and there is no more data in the queue (i.e. it’s empty)" [2]
But as only 64 packets are reported at max, the queue can only grow when it is <= 64 packets. And then it can only stay at a limit >64 until the next decrease of the limit.

2. netdev_tx_completed_queue() is called in irregular intervals:

Quoted 8 lines

If the consumer is slow it is called approx each tx_coal_usecs. But if the consumer is fast it is called way more frequent, probably in irregular intervals depending on the scheduling. However, "BQL depends on periodic completion interrupts" [2].

--> How about adding something like an interrupt that triggers every
10us and calls netdev_tx_completed_queue() with n_bql collected from (multiple) veth_xdp_rcv runs? That could solve 1. and 2.

Hi,

I worked on a new version (see attachment) that addresses both issues.

The major change is that instead of tracking the timestamp and packet count as local variables in veth_xdp_rcv(), they are now stored persistently in veth_rq as struct veth_bql_state. This allows completions to accumulate across multiple NAPI poll calls, so netdev_tx_completed_queue() can report more than 64 packets at once (see point 1). To get the time I am using (the fast) sched_clock() with a trick to avoid issues when switching between CPUs.

For point 2, the coalescing deadline is now checked both before the receive loop (to flush completions that timed out since the previous poll) and after each consumed packet, making completion intervals more regular. Still the intervals can be smaller than VETH_BQL_COAL_TX_USECS, but I guess this is fine.

I also found out that the BQL limit correlates closely with VETH_BQL_COAL_TX_USECS. It essentially reflects the latency we are targeting. I raised the default to 100 µs to allow DQL to converge to a higher limit (for reaching 255 in the testing below).

With the patched pktgen (respecting __QUEUE_STATE_STACK_XOFF), testing shows:
- --nrules 0: DQL limit reaches (up to) ~255
- --nrules 10000: DQL limit converges to ~0 (with --gro-disable)

Quoted 4 lines

These results are what I would expect from a BQL algorithm, but more testing is needed of course.

What do you think?

Hi,

This is exactly what I had in mind for implementing the BQL algorithm in this case. I did some testing with pktgen of this patch and also compared it to the v5 version.

Quoted 8 lines
Quoted 4 lines

You can find an extension of the benchmark script with pktgen here [1], as well as a wrapper script (veth_bql_bench.sh) to run the test script with and without --bql-disable to report the difference. I also configured pktgen to use the qdisc as suggested by Jesper.

Great, I will use your pktgen solution from now on.

Didn't know about the qdisc option, is there a performance difference with/without it? Or is it to have ping working next to pktgen?

Did not see any big difference performance-wise, but as you say, ping works better with pktgen then.

Quoted 46 lines

Consider to do a pull request :)

Quoted 3 lines

Note: bpftrace needs to be disabled, otherwise it becomes the bottleneck (at least on my machine) and pktgen throughput is halved when enabled.

Good to know.

Quoted 34 lines

Here are the results:

v5 (not time-based):
--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 1980871 2169898
Ping RTT avg (ms) 0.065 0.162
Throughput diff -8.7% // BQL 8.7% lower throughput
RTT diff -59.9% // BQL 60% lower latency
========================================

Simon's time-based version:

Test args: --nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------
Throughput (pps) 2166335 2153398
Ping RTT avg (ms) 0.165 0.165
Throughput diff 0.6%
RTT diff 0.0%

--pktgen --no-bpftrace --nrules 3500
========================================
Results (average over 10 runs):
========================================
BQL on BQL off
--- ------ -------

Quoted 5 lines
Throughput (pps)            28569        28696
Ping RTT avg (ms)           1.327        8.409
Throughput diff             -0.4%

RTT diff -84.2%

Quoted 2 lines

I think we should run benchmarks against the stock net-next to be safe.

--nrules 0 --pktgen --no-bpftrace
========================================
Results (average over 10 runs):
========================================
net-next

Quoted 9 lines
---                  -------
Throughput (pps)     2285421
Ping RTT avg (ms)    0.161

(slightly adjusted the output to better communicate the results)

So in my case this means BQL implementation has ~5% lower throughput compared to net-next. But please double check.

Yes, I will benchmark myself.

There are probably some places where we can gain performance. For example, I see ptr_ring_empty() which could be swapped for __ptr_ring_empty() which would save a spinlock and unlock.

Quoted 10 lines
Quoted 10 lines
Quoted 1 line

Seems to work now as expected.

Yes, but I think we have to keep these points in mind:

1. Limit/Inflight can be bigger than VETH_RING_SIZE, because packets can be enqueued in the same time as they are read out, so netdev_tx_completed_queue() can theoretically be called with many number of packets.
I do not think it is deal-breaking though.
I could see such high limits/inflights when looking at the /sys BQL statistics..

Quoted 3 lines

For me this makes sense, that inflight just means the number of packets not yet 'completed' or the number of packets that you can send between two completion calls. I think this is not specific

Quoted 6 lines

From my understanding, I do think that this behavior is
pretty specific.

Typically BQL-enabled NIC drivers clear packets out of some internal buffer in their completion interrupt (or something similar). And after that they call netdev_tx_completed_queue().

You are right, because descriptors are freed at the same time when they are accounted for.

Quoted 5 lines
Quoted 3 lines

to this implementation. But for long intervals this might result in some problems because you can just fill the veth_ring to its capacity quickly, and increasing latency if the receiver is slow.

Yes, but I think the latency can only be approx. as big as the interval (can be higher with GRO enabled).

I think it's more like ~2.x times added latency in the worst case, which is normal BQL behavior, but for large intervals 2x is a lot. Of course this value is also capped by the time it takes to process the 256 packets that fit in the ring buffer.

For example, for a test run with fq_codel and 20K rules, and a ping baseline of 1.7 ms without pktgen load, the results are:
tx-usecs p99_ping_rtt (ms)
-------- -----------------

      0           5.223
    100           6.910
    500           7.197
   1000           6.967
   5000          16.233
  10000          25.033
  20000          44.133

Also, the interval is sometimes dominated by the processing time of a packet batch (and other effects). The default is 8 packets/batch (see gro_normal_batch). This means that for 7 packets you see processing times on the order of a few hundred nanoseconds, but for the 8th you see the processing time of the whole batch (see table below). With a ruleset > 1000 entries, this exceeds the default tx-usecs value, which is why the p99 RTT increase is even larger than 2x tx-usecs -- as you can see in the table for tx-usecs values 0-5000.

tx-usecs 0 nrules 0
Percentile per-packet processing time (µs)
---------- -------------

     p5             0.316
    p25             0.382
    p50             0.442
    p75             0.508 -- fast enqueue to skb_list

p95 68.304 -- batch processing time

Quoted 8 lines

Setting gro_normal_batch to 1 makes the interval times more accurate and improves p99 RTT slightly, though it's still > 2x the tx-usecs latency increase.

However, using smaller values > 0 means that you can benefit from bql, but the actual interval might be larger than the configured value. I do not think this is a deal breaker, but something worth noting where the latencies come from.

I understand and agree with your points.
I also do not see anything deal-breaking. Yes, there is no guarantee for the latency, but BQL is best-effort anyway.

Quoted 5 lines
Quoted 2 lines

I will wait for your new measurements, but there is no argument against a default tx-usecs of ~100us for now, right?

Yes, I think 100us is perfectly fine. I guess most of it was just my curiosity why the latency values are as they are :)

Which is great, because I was wondering the same :)

Quoted 7 lines

But it feels like this will need some documentation, because as we have seen, some values are a little different from what you expect from bql. Inflight > veth_ring_size, tx-usecs not necessarily achieving the configured value, inflight can get stuck > 0. Wdyt?
But I think it works nicely overall.

Exactly, we should get ready for a v6 soon.

And I think we should move the BQL logic into a seperate .h file as a library. Then it is also usable for TUN/TAP in the future.

Let's amend the commits. Should we do this on Github?

Quoted 139 lines
Quoted 139 lines
Quoted 4 lines

Also worth noting: using fq_codel is better to use in this case because it can fast track sparse flows. using sfq the ping packet can get head of line blocked by the current quantum of sfq, if my understanding is correct.

Why don't we use a prio qdisc where ping gets prioritized? Then we do not have to worry about head of line blocking on the qdisc layer and instead can concentrate on veth.

Quoted 28 lines
Quoted 6 lines
Quoted 5 lines

To illustrate this have a look at [1]. There are some plots that show the rtt vs. tx-usec config depending on nrules.

[1] https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark/results/tx-usecs

Nice plots! Great for finding a sane default value.

I did some more measurements to better understand whats happening. I will upload the results latest by tomorrow to the github repository mentioned above.

Quoted 18 lines
Quoted 18 lines
Quoted 4 lines

2. sched_clock() is only valid on the same CPU. When a different CPU starts executing its sched_clock() can be in the past compared to the sched_clock() value saved by the previous CPU. My trick...

Quoted 8 lines

min(s->time, sched_clock())
... avoids potentially extremely long intervals between netdev_tx_completed_queue() calls but is not perfect of course. I think CPU hopping happens rarely enough for this to matter.. And also we have to keep this in mind [1]:
"An architecture may or may not provide an implementation of sched_clock() on its own. If a local implementation is not provided, the system jiffy counter will be used as sched_clock()."

So the problem with this is that with jiffies you have like millisecond interval granularity, which might be too long in order to work properly. Given the receiver completes 128 packet in 1ms (queue_completed interval), the bql will set the limit at 256. Then the tx thread can quickly fill the ring, and it then basically stopped until the 1ms interval is over.

Quoted 7 lines
Quoted 1 line

Linux Mint only has 100Hz jiffies :^)

ok so 10ms granularity. Effectively disables BQL for the most part. However, you can still benefit if your receive side is very slow.

Quoted 3 lines

I think you misunderstand something.

+ if (peer_txq && state->n_bql && ptr_ring_empty(&rq->xdp_ring)) {

             // Pairs with smp_wmb() in __ptr_ring_produce()
Quoted 10 lines
Quoted 10 lines
(quoted diff) +4 -0
+        smp_rmb();
+        if (test_bit(__QUEUE_STATE_STACK_XOFF, &peer_txq->state))
+            veth_bql_complete(state, peer_txq);
+    }

This snippet completes whenever the queue is empty and the netdev queue is stopped due to BQL.
So then the interval is smaller than 1ms.
I think, that is the reason why the pps is fine for 1ms in your benchmarks.

Quoted 9 lines

Without that snippet the queue stalls during ramp-up and throughput never gets off the ground. But once the BQL limit has stabilized, the snippet is no longer invoked -- with --nrules 0 the queue is never stopped, and with a slow receiver the queue is not empty. So the snippet is needed for correct ramp-up, but it doesn't affect steady-state throughput in these tests. This is also confirmed by a test where I measure the time between intervals. For 1000us across all nrules configurations the time ranges between 1000-1400us.

Yes, I agree. It only acts in the case of starvation (see [1]) and I think it is an advantage that we react faster compared to other BQL implementations.

Additionally the snippet is *required* because if the ring is empty and the txq stopped due to BQL, there are no more veth_xdp_recv() calls, permanently stalling everything. That is the reason for the memory barrier pairing in the snippet.

[1] Link: https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 11 lines

The variance of the time between intervals is also quite good in general, but not always exact the configured value. For most cases +-5% from p50 value of the real interval.
Migrations between CPUs can also shorten an interval: if the new CPU's sched_clock() is ahead of the previous one (say by 15 ms), the apparent elapsed time within the current interval is inflated, so only ~5 ms of real time remains before the deadline fires (with the configured tx-usecs at 20 ms). Without migrations the interval stays consistently close to the configured value. I don't think this is a problem in practice -- it just adds some variance to the p5 of the measured interval.

Yes, and then we mistakenly decrease the BQL limit once. But we react fast on that starvation. So its fine.

Quoted 50 lines
Quoted 50 lines
Quoted 1 line

I don't know how much this matters in practice. Not sure which architectures still hit the jiffies fallback.When in doubt we could disable BQL for those, or pin it to a "virtual queue size" via limit_min / limit_max similar to the v5 behavior and call queue_completed for every packet. Wdyt?

Yes, probably not relevant, I think I would just disable it for those architectures, avoiding possible regressions.

Quoted 8 lines
Quoted 5 lines

3. Inflight can be stuck at a value>0 for a long time when packet enqueueing stops. Only when packets are enqueued again, (on the next veth_xdp_rcv() call,) netdev_tx_completed_queue() is executed and inflight is set to 0 again.
Can also be seen when looking at the /sys BQL statistics.

This happens if the ring empties before the next queue_completed fires, right?

Exactly.

Quoted 3 lines

Another thing: Is it counterintuitive to set the tx_usec_coal on the receive device? Because this is a bql related config that is normally configured on the TX side?

Yes, I would say so.

Quoted 31 lines
Quoted 3 lines

BTW: Yesterday, I worked on and refactored the code into its own .h
file as a library and it also works fine for TUN/TAP (+vhost-net) for me :)

Nice! Thank you.
Jonas

Quoted 25 lines

Thanks for your work!
Simon

[1] Link: https://docs.kernel.org/timers/timekeeping.html

Quoted 20 lines

[1]https://github.com/jkoeppeler/veth-backpressure-performance-testing/tree/pktgen-and-benchmark

Thanks,
Jonas

Quoted 15 lines

Thanks!

BTW: I think that this implementation could also work for other
software interfaces.

Quoted 10 lines

[2] Link:https://medium.com/@tom_84912/byte-queue-limits-the-unauthorized-biography-61adc5730b83

Quoted 6 lines

There is an important gotcha. We actually have micro-burst of queuing (likely due to scheduling noise). Reading BQL stats from /sys will show BQL inflight=1, but when using the option --hist is it visible that @inflight have a long tail (see below signature). The "qdisc" output line also shows this happening via requeues increasing (approx 17/sec in a test with 567Kpps). (this was with the time-based BQL impl).

I understand..

#34 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 22 lines · Simon Schippers
Quoted 21 lines · Jonas Köppeler
Quoted 20 lines · Simon Schippers
Quoted 19 lines · Jonas Köppeler
Quoted 18 lines · Simon Schippers
Quoted 17 lines · Jonas Köppeler
Quoted 16 lines · Simon Schippers
Quoted 15 lines · Jonas Köppeler
Quoted 14 lines · Simon Schippers
Quoted 13 lines · Simon Schippers
Quoted 12 lines · Jesper Dangaard Brouer
Quoted 3 lines
Quoted 2 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit
in a BPF hashmap. Reading from /sys will not be accurate.

Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 5 lines
Quoted 5 lines

I moved the selftests into a github repo [1] to allow us to collaborate
and evaluate the changes more easily. I explicitly kept the new BPF
based BQL tracking as a commit[2] for your benefit.

[1] https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[... cut ...]

Quoted 7 lines
Quoted 5 lines
Quoted 3 lines

I will wait for your new measurements, but there is no argument against a default tx-usecs of ~100us for now, right?

Yes, I think 100us is perfectly fine. I guess most of it was just my curiosity why the latency values are as they are 🙂

Which is great, because I was wondering the same 🙂

Thank you Jonas and Simon for testing this via[1] on your systems.

One performance concern from my side is if/when BQL limit goes below 8 packets. This will cause cache-line bouncing and many qdisc requeues between the two CPUs. Notice that 8 packets for the ptr_ring is one cache-line. This is why I suggested defaulting BQL min_limit to be 8. This would work in combination with the tx-usecs coalesce tuning as a lower bound.

Quoted 9 lines
Quoted 7 lines

But it feels like this will need some documentation, because as we have seen, some values are a little different from what you expect from bql. Inflight > veth_ring_size, tx-usecs not necessarily achieving the configured value, inflight can get stuck > 0. Wdyt?
But I think it works nicely overall.

Exactly, we should get ready for a v6 soon.

I want to send out a V6 today (and I'm busy rest of week). I'm simply going to include Simon's patch[2]. Then we can iterate on that email thread, making it easier for people to reply inline to the code changes.

[2] https://lore.kernel.org/all/[email protected]/

Quoted 3 lines

And I think we should move the BQL logic into a seperate .h file as a library. Then it is also usable for TUN/TAP in the future.

I like the idea, but refactoring into a library seems out-of-scope for this patchset. As a first step, let us see if this works for veth driver use-case. I do encourage Simon to refactor when doing the work for TUN/TAP in the future.

Alternatively netdev maintainers can choose to apply V5 patchset and we can iterate on finding the right ethtool coalesce parameters for veth and create a .h "library" file for this.

Quoted 1 line

Let's amend the commits. Should we do this on Github?

Kernel development for netdev [3] happens on the mailing list with patches via email.

--Jesper

[3] https://kernel.org/doc/html/latest/process/maintainer-netdev.html

#35 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

Quoted 56 lines · Jesper Dangaard Brouer
Quoted 22 lines · Simon Schippers
Quoted 21 lines · Jonas Köppeler
Quoted 20 lines · Simon Schippers
Quoted 19 lines · Jonas Köppeler
Quoted 18 lines · Simon Schippers
Quoted 17 lines · Jonas Köppeler
Quoted 16 lines · Simon Schippers
Quoted 15 lines · Jonas Köppeler
Quoted 14 lines · Simon Schippers
Quoted 13 lines · Simon Schippers
Quoted 12 lines · Jesper Dangaard Brouer
Quoted 3 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit
in a BPF hashmap. Reading from /sys will not be accurate.
Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 5 lines

I moved the selftests into a github repo [1] to allow us to collaborate
and evaluate the changes more easily. I explicitly kept the new BPF
based BQL tracking as a commit[2] for your benefit.

[1] https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[... cut ...]

Quoted 7 lines
Quoted 5 lines
Quoted 3 lines

I will wait for your new measurements, but there is no argument against a default tx-usecs of ~100us for now, right?

Yes, I think 100us is perfectly fine. I guess most of it was just my curiosity why the latency values are as they are 🙂

Which is great, because I was wondering the same 🙂

Thank you Jonas and Simon for testing this via[1] on your systems.

One performance concern from my side is if/when BQL limit goes below 8 packets. This will cause cache-line bouncing and many qdisc requeues between the two CPUs. Notice that 8 packets for the ptr_ring is one cache-line. This is why I suggested defaulting BQL min_limit to be 8. This would work in combination with the tx-usecs coalesce tuning as a lower bound.

Quoted 9 lines
Quoted 7 lines

But it feels like this will need some documentation, because as we have seen, some values are a little different from what you expect from bql. Inflight > veth_ring_size, tx-usecs not necessarily achieving the configured value, inflight can get stuck > 0. Wdyt?
But I think it works nicely overall.

Exactly, we should get ready for a v6 soon.

Quoted 7 lines

I want to send out a V6 today (and I'm busy rest of week). I'm simply going to include Simon's patch[2]. Then we can iterate on that email thread, making it easier for people to reply inline to the code changes.

[2] https://lore.kernel.org/all/[email protected]/

Sure, but please address those 2 nits:
- replace unlikely(!ring->size) || __ptr_ring_full(ring) with unlikely(__ptr_ring_check_produce(ring))
- replace ptr_ring_empty() with __ptr_ring_empty()

Apart from that I think the code is fine.
Just the commit messages need changes but we can adapt them later.

Quoted 8 lines
Quoted 3 lines

And I think we should move the BQL logic into a seperate .h file as a library. Then it is also usable for TUN/TAP in the future.

I like the idea, but refactoring into a library seems out-of-scope for this patchset. As a first step, let us see if this works for veth driver use-case. I do encourage Simon to refactor when doing the work for TUN/TAP in the future.

Okay, fair point. I do not have a problem with that.

I did this split in [1] but we can ignore that for now.

[1] Link: https://github.com/jkoeppeler/veth-backpressure-performance-testing/pull/1

Quoted 9 lines

Alternatively netdev maintainers can choose to apply V5 patchset and we can iterate on finding the right ethtool coalesce parameters for veth and create a .h "library" file for this.

Quoted 1 line

Let's amend the commits. Should we do this on Github?

Kernel development for netdev [3] happens on the mailing list with patches via email.

Okay :)

Thanks!

Quoted 5 lines

--Jesper

[3] https://kernel.org/doc/html/latest/process/maintainer-netdev.html

#36 lore raw

Re: [PATCH net-next v5 3/5] veth: implement Byte Queue Limits (BQL) for latency reduction

In reply to Jesper Dangaard Brouer
Quoted 33 lines · Jesper Dangaard Brouer
Quoted 22 lines · Simon Schippers
Quoted 21 lines · Jonas Köppeler
Quoted 20 lines · Simon Schippers
Quoted 19 lines · Jonas Köppeler
Quoted 18 lines · Simon Schippers
Quoted 17 lines · Jonas Köppeler
Quoted 16 lines · Simon Schippers
Quoted 15 lines · Jonas Köppeler
Quoted 14 lines · Simon Schippers
Quoted 13 lines · Simon Schippers
Quoted 12 lines · Jesper Dangaard Brouer
Quoted 3 lines

Nope, I'm using a bpftrace program to keep track of the inflight/limit
in a BPF hashmap. Reading from /sys will not be accurate.
Ah nice.

Add the option --hist to have both NAPI and BQL histograms printed when script ends. This will give you an accurate pattern of how inflight and limit evolves.

Quoted 5 lines

I moved the selftests into a github repo [1] to allow us to collaborate
and evaluate the changes more easily. I explicitly kept the new BPF
based BQL tracking as a commit[2] for your benefit.

[1] https://github.com/netoptimizer/veth-backpressure-performance-testing/tree/main/selftests

[... cut ...]

Quoted 5 lines
Quoted 5 lines
Quoted 3 lines

I will wait for your new measurements, but there is no argument against a default tx-usecs of ~100us for now, right?

Yes, I think 100us is perfectly fine. I guess most of it was just my curiosity why the latency values are as they are 🙂

Quoted 8 lines
Quoted 2 lines

Which is great, because I was wondering the same 🙂

Thank you Jonas and Simon for testing this via[1] on your systems.

One performance concern from my side is if/when BQL limit goes below 8 packets. This will cause cache-line bouncing and many qdisc requeues between the two CPUs. Notice that 8 packets for the ptr_ring is one

I think the qdisc requeues are an issue of dc82a33297fc ("veth: apply qdisc backpressure on full ptr_ring to reduce TX drops").

It could be solved by stopping the netdev queue after inserting the last element like I did in my tun tap implementation [1]. I know, you also did this before in a previous implementation, but the required re-check used __ptr_ring_empty() which is not allowed as producer.
That is the reason why I introduced the new __ptr_ring_check_produce() in [1] which is safe for the producer to call.

[1] Link: https://lore.kernel.org/netdev/[email protected]/

Quoted 3 lines

cache-line. This is why I suggested defaulting BQL min_limit to be 8. This would work in combination with the tx-usecs coalesce tuning as a lower bound.

Yes, it would work but I think it is fine as-is.
BQL will only choose such a small limit if the consumer is really slow anyway.

#37 lore raw

PATCH v5 4/5 veth: add tx_timeout watchdog as BQL safety net

In reply to [email protected]

From: Jesper Dangaard Brouer <[email protected]>

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.

---
 drivers/net/veth.c | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
drivers/net/veth.c +18 -0
index 86b78900c48e..4103d298aa9b 100644
@@ -1442,6 +1442,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);
@@ -1780,6 +1796,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 = {
@@ -1819,6 +1836,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(16000);
 
 	dev->hw_features = VETH_FEATURES;
 	dev->hw_enc_features = VETH_FEATURES;
Signature · 1 line
2.43.0
#38 lore raw

PATCH v5 5/5 net: sched: add timeout count to NETDEV WATCHDOG message

In reply to [email protected]

From: Jesper Dangaard Brouer <[email protected]>

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.

---
 net/sched/sch_generic.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
net/sched/sch_generic.c +4 -4
index a93321db8fd7..3e2e2e887a86 100644
@@ -533,13 +533,12 @@ static void dev_watchdog(struct timer_list *t)
 		    netif_running(dev) &&
 		    netif_carrier_ok(dev)) {
 			unsigned int timedout_ms = 0;
+			struct netdev_queue *txq;
 			unsigned int i;
 			unsigned long trans_start;
 			unsigned long oldest_start = jiffies;
 
 			for (i = 0; i < dev->num_tx_queues; i++) {
-				struct netdev_queue *txq;
-
 				txq = netdev_get_tx_queue(dev, i);
 				if (!netif_xmit_stopped(txq))
 					continue;
@@ -561,9 +560,10 @@ 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,
+					    atomic_long_read(&txq->trans_timeout));
 				netif_freeze_queues(dev);
 				dev->netdev_ops->ndo_tx_timeout(dev, i);
 				netif_unfreeze_queues(dev);
Signature · 1 line
2.43.0
#39 lore raw
In reply to [email protected]
This series was applied to netdev/net.git (main) by Paolo Abeni <[email protected]>:

Hello:

This series was applied to netdev/net.git (main)
by Paolo Abeni <[email protected]>:

Quoted 6 lines · On Tue, 5 May 2026 15:21:52 +0200 you wrote:

From: Jesper Dangaard Brouer <[email protected]>

This series adds BQL (Byte Queue Limits) to the veth driver, reducing latency by dynamically limiting in-flight packets in the ptr_ring and moving buffering into the qdisc where AQM algorithms can act on it.

Quoted 7 lines

Problem:
veth's 256-entry ptr_ring acts as a "dark buffer" -- packets queued there are invisible to the qdisc's AQM. Under load, the ring fills completely (DRV_XOFF backpressure), adding up to 256 packets of unmanaged latency before the qdisc even sees congestion.

[...]

Here is the summary with links:
- [net-next,v5,1/5] veth: fix OOB txq access in veth_poll() with asymmetric queue counts

    https://git.kernel.org/netdev/net/c/08f566e8f83b

- [net-next,v5,2/5] net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices (no matching commit)
- [net-next,v5,3/5] veth: implement Byte Queue Limits (BQL) for latency reduction (no matching commit)
- [net-next,v5,4/5] veth: add tx_timeout watchdog as BQL safety net
(no matching commit)
- [net-next,v5,5/5] net: sched: add timeout count to NETDEV WATCHDOG message (no matching commit)

You are awesome, thank you!

Signature · 2 lines
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html