MPTCP Linux Development
 help / color / mirror / Atom feed
* [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab)
@ 2026-07-26  5:55 Shardul Bankar
  2026-07-26  5:55 ` [PATCH mptcp-next 1/3] mptcp: sched: penalise a slow subflow by halving its cwnd Shardul Bankar
                   ` (4 more replies)
  0 siblings, 5 replies; 9+ messages in thread
From: Shardul Bankar @ 2026-07-26  5:55 UTC (permalink / raw)
  To: Matthieu Baerts (NGI0), MPTCP Linux; +Cc: shardulsb08, Shardul Bankar

Hi Matt,

Following up on my note about aligning #345 to mptcp_rcv_buf_optimization():
here is a first cut, as a 3-patch series. A few things came out differently
from what I described there, so I have called each one out below rather than
leave it for you to spot.

  1/3 penalise a slow subflow by halving its cwnd
  2/3 do not penalise when receive-window-limited
  3/3 DO-NOT-MERGE counters (for testing only)

1/3 is the simple version. In the scheduler, once a subflow is picked, it is
flagged for a cwnd halving that is applied in the push path under the subflow
socket lock (so it is safe with the per-subflow locks, as I mentioned). The
flag is set when the subflow is clearly slower than the fastest path, the
fastest path is cwnd-limited, and the subflow is in TCP_CA_Open. The reduction
halves cwnd (and ssthresh if cwnd is past it), at most once per RTT, and the
congestion control grows it back.

What differs from what I described, and why:

- Trigger on delivery rate, not RTT. I had said "slower by RTT". In testing
  that over-penalised a path that is only higher latency but still carries its
  share of the traffic (equal bandwidth, unequal delay): it fires on the
  slower-by-latency path even though shrinking its window loses real goodput.
  Keying on the pacing rate instead (penalise only a path whose rate is below
  half the fastest) targets a genuinely low-throughput path, and reuses the
  avg_pacing_rate the scheduler already maintains. On the threshold I raised
  with you (the fork's "any slower" versus a small factor): I had said I would
  default to "any slower", but with a rate trigger that fires on almost every
  non-fastest path, since rates always vary a little, so I used the factor to
  keep it to genuinely slow paths, as I flagged might be needed. Half is just
  a starting point, easy to tune.

- I did not carry over the fork's "meta is send-buffer-limited" gate. In
  mainline the msk send buffer is the sum of the subflow send buffers, and it
  is effectively never full when the scheduler samples it (the scheduler runs
  on the push path, just after an ACK has opened room), so that gate never
  fires and the penalty stays dormant. That is why 1/3 has no send-buffer
  condition.

- 2/3 is a guard that is in neither the fork nor what I described. Without it,
  1/3 regresses badly (about 2x slower in my runs) when the connection is
  receive-window-limited. In that case the fastest path is capped by the same
  shared window, so it cannot absorb what the slow path gives up, and halving
  just sheds the slow path's throughput. 2/3 skips the penalty while the
  application has queued past the send-window edge (write_seq > wnd_end), which
  is the sign that the receiver, not our congestion window, is the bottleneck.
  I kept it a separate patch so you can test 1/3 on its own, or drop or retune
  2/3 independently. The exact condition is the piece I would most value your
  lab checking.

Testing was local (network namespaces plus netem, patched against a clean
mptcp/export), starting from the existing simult_flows selftest. It is a debug
kernel and mostly single runs, so please read the numbers as directional; I am
happy to share the full logs and the scenario script.

- No regression on the simult_flows suite.
- To see the intended effect I looked at MPTcpExtOFOQueue, the number of
  segments the receiver had to hold out of order over the transfer, since that
  is what the change is meant to reduce and a plain throughput number cannot
  show it. On the asymmetric-bandwidth pair (10 vs 3 mbit) with a small
  SO_SNDBUF, that count fell by roughly a fifth (about 18 to 23% in my runs)
  with no change in throughput. So this is a reduction in reordering, i.e. a
  latency and smoothness effect, not more bytes per second; whether that is
  worth it for a real workload is exactly what I hope your lab can judge.
- The receive-window-limited regression above is back to baseline with 2/3.
- Two honest limits. First, with fully autotuned buffers (the common default)
  the guard does not fire, because the connection is not receive-window-
  limited, and the penalty then leaves a small reordering cost: halving trims
  the slow path's delivery rate, so its share of the in-order stream arrives a
  little later and the out-of-order count rises a few percent. I did not find a
  simple way to also suppress
  that without re-opening the gating, and I did not want to over-build v1;
  scoping it more tightly (for example only when send-buffer-limited) may be
  the right call and I would defer to your lab on it. Second, I have only
  exercised two subflows and no backup subflow so far.

3/3 adds two MPTcpExt counters (CwndPenalized, PenalCandidate) so a run can
tell "the guard held the penalty back" from "the trigger never fired". Not for
merge. I left your Co-developed-by on it in case it is useful to you elsewhere.

I drove those regimes with a small simult_flows variant (receive-window-
limited, send-buffer-limited, and autotuned cases). It is a helper, not
selftest quality, so I did not fold it into the series; it is on a branch of
my tree, in case it saves your lab time or you spot a case I missed:

https://github.com/shardulsdk-mpiric/linux/blob/6926c4b7f583/tools/testing/selftests/net/mptcp/mptcp_sched_penalise.sh

Run it on a baseline and a patched kernel and compare (prefix with
MPTCP_LIB_IP_MPTCP=1 if pm_nl_ctl does not work in your setup):

  SCENARIO=suite                              ./mptcp_sched_penalise.sh
  SCENARIO=unbounded                          ./mptcp_sched_penalise.sh
  SCENARIO=rwnd    RCVBUF=262144              ./mptcp_sched_penalise.sh
  SCENARIO=sndbuf  SNDBUF=65536               ./mptcp_sched_penalise.sh
  SCENARIO=both    RCVBUF=262144 SNDBUF=65536 ./mptcp_sched_penalise.sh

For the rwnd/sndbuf/both scenarios the simult_flows pass/fail bound is not
meaningful (it assumes both paths are fully used): read the printed runtime
and out-of-order counts, not OK/FAIL. The "both" case also occasionally fails
to bring up the second subflow with the very small SO_SNDBUF; just rerun it if
you see a single-subflow run.

Thanks,
Shardul

Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com>
---
Shardul Bankar (3):
      mptcp: sched: penalise a slow subflow by halving its cwnd
      mptcp: sched: do not penalise when receive-window-limited
      DO-NOT-MERGE: mptcp: sched: penalise counters

 net/mptcp/mib.c      |  2 ++
 net/mptcp/mib.h      |  2 ++
 net/mptcp/protocol.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 net/mptcp/protocol.h |  2 ++
 4 files changed, 97 insertions(+), 3 deletions(-)
---
base-commit: 97ce11d2793f114ca652a565a8d2795c085d8ff1
change-id: 20260726-mptcp_penalise_send-2fbf15329c71

Best regards,
--  
Shardul Bankar <shardul.b@mpiricsoftware.com>


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH mptcp-next 1/3] mptcp: sched: penalise a slow subflow by halving its cwnd
  2026-07-26  5:55 [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab) Shardul Bankar
@ 2026-07-26  5:55 ` Shardul Bankar
  2026-07-29 11:49   ` Matthieu Baerts
  2026-07-26  5:55 ` [PATCH mptcp-next 2/3] mptcp: sched: do not penalise when receive-window-limited Shardul Bankar
                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 9+ messages in thread
From: Shardul Bankar @ 2026-07-26  5:55 UTC (permalink / raw)
  To: Matthieu Baerts (NGI0), MPTCP Linux; +Cc: shardulsb08, Shardul Bankar

Issue #345: a poorly-performing but usable subflow (high latency, loss,
bufferbloat) can soak up connection resources and cause head-of-line
blocking of the aggregate stream. Give the default packet scheduler a way
to send less than such a subflow's full congestion window.

Once a subflow has been picked for transmission, flag it for penalisation
when:
- its smoothed delivery rate (avg_pacing_rate) is below half that of the
  fastest path, keying on rate, not RTT, so a slow-but-high-throughput
  path is left alone;
- the fastest path is cwnd-limited (saturated), so shifting load off the
  slow path is worthwhile;
- the subflow is in TCP_CA_Open, so its cwnd is not already being reduced
  by loss recovery;
- it has not been penalised in the last RTT.

The reduction halves tcp_snd_cwnd (floor 2) and ssthresh if cwnd
is past it. It is applied in the push path under the subflow socket lock,
which protects snd_cwnd (the scheduler runs under the msk lock). The
congestion control grows the window back, ACK-clocked; that regrowth is
the built-in probe, so no explicit MPTCP-side probing is needed.

Co-developed-by: Matthieu Baerts (NGI0) <matttbe@kernel.org>
Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com>
---
 net/mptcp/protocol.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++++---
 net/mptcp/protocol.h |  2 ++
 2 files changed, 67 insertions(+), 3 deletions(-)

diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index 954e20bb27de..d31bcb9ad894 100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -1556,6 +1556,44 @@ bool mptcp_subflow_active(struct mptcp_subflow_context *subflow)
 #define SSK_MODE_BACKUP	1
 #define SSK_MODE_MAX	2
 
+/* Penalise a subflow whose delivery (pacing) rate is below the fraction
+ * 1 / MPTCP_PENALISE_RATE_RATIO of the fastest path's rate. Keying on rate,
+ * not RTT, throttles only a path whose throughput contribution is small
+ * relative to the head-of-line cost it imposes, and leaves a merely
+ * higher-latency but high-throughput path alone.
+ */
+#define MPTCP_PENALISE_RATE_RATIO	2
+
+/* Rate-limit the penalty to at most once per subflow RTT, so the congestion
+ * control can grow the window back between reductions.
+ */
+static bool mptcp_penalise_throttle_ok(struct mptcp_subflow_context *subflow)
+{
+	struct sock *ssk = mptcp_subflow_tcp_sock(subflow);
+	u32 rtt = usecs_to_jiffies(tcp_sk(ssk)->srtt_us >> 3);
+
+	return tcp_jiffies32 - subflow->last_penalise >= max_t(u32, rtt, 1);
+}
+
+/* Halve the congestion window (and ssthresh, if cwnd is past it) of a subflow
+ * the scheduler flagged. Runs in the push path under the subflow socket lock,
+ * which protects snd_cwnd. The congestion control grows the window back,
+ * ACK-clocked, and that regrowth is the built-in probe, so no explicit probing
+ * is needed.
+ */
+static void mptcp_penalise_cwnd(struct sock *ssk)
+{
+	struct mptcp_subflow_context *subflow = mptcp_subflow_ctx(ssk);
+	struct tcp_sock *tp = tcp_sk(ssk);
+	u32 cwnd = tcp_snd_cwnd(tp);
+
+	subflow->penalise = false;
+	subflow->last_penalise = tcp_jiffies32;
+	tcp_snd_cwnd_set(tp, max_t(u32, cwnd >> 1, 2));
+	if (cwnd >= tp->snd_ssthresh)
+		tp->snd_ssthresh = max_t(u32, tp->snd_ssthresh >> 1, 2);
+}
+
 /* implement the mptcp packet scheduler;
  * returns the subflow that will transmit the next DSS
  * additionally updates the rtx timeout
@@ -1565,9 +1603,9 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
 	struct subflow_send_info send_info[SSK_MODE_MAX];
 	struct mptcp_subflow_context *subflow;
 	struct sock *sk = (struct sock *)msk;
-	u32 pace, burst, wmem;
+	u32 pace, burst, wmem, max_pace = 0;
 	int i, nr_active = 0;
-	struct sock *ssk;
+	struct sock *ssk, *fastest = NULL;
 	u64 linger_time;
 	long tout = 0;
 
@@ -1596,6 +1634,14 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
 				continue;
 		}
 
+		/* track the fastest path by delivery rate; the penalty below
+		 * throttles paths that are slow relative to it.
+		 */
+		if (pace > max_pace) {
+			max_pace = pace;
+			fastest = ssk;
+		}
+
 		linger_time = div_u64((u64)READ_ONCE(ssk->sk_wmem_queued) << 32, pace);
 		if (linger_time < send_info[backup].linger_time) {
 			send_info[backup].ssk = ssk;
@@ -1623,12 +1669,25 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
 	if (!ssk || !sk_stream_memory_free(ssk))
 		return NULL;
 
+	/* Flag the chosen subflow for cwnd halving (applied in the push path)
+	 * when its delivery rate is a small fraction of the fastest path's and
+	 * that fast path is saturated (cwnd-limited), so moving load off the
+	 * slow path is worthwhile. Only penalise a path in TCP_CA_Open, one
+	 * whose cwnd is not already being shrunk by loss recovery, and at most
+	 * once per RTT.
+	 */
+	subflow = mptcp_subflow_ctx(ssk);
+	subflow->penalise = fastest && ssk != fastest &&
+			    (u64)subflow->avg_pacing_rate * MPTCP_PENALISE_RATE_RATIO < max_pace &&
+			    inet_csk(ssk)->icsk_ca_state == TCP_CA_Open &&
+			    tcp_is_cwnd_limited(fastest) &&
+			    mptcp_penalise_throttle_ok(subflow);
+
 	burst = min(MPTCP_SEND_BURST_SIZE, mptcp_wnd_end(msk) - msk->snd_nxt);
 	wmem = READ_ONCE(ssk->sk_wmem_queued);
 	if (!burst)
 		return ssk;
 
-	subflow = mptcp_subflow_ctx(ssk);
 	subflow->avg_pacing_rate = div_u64((u64)subflow->avg_pacing_rate * wmem +
 					   READ_ONCE(ssk->sk_pacing_rate) * burst,
 					   burst + wmem);
@@ -1685,6 +1744,9 @@ static int __subflow_push_pending(struct sock *sk, struct sock *ssk,
 	struct mptcp_data_frag *dfrag;
 	int len, copied = 0, err = 0;
 
+	if (mptcp_subflow_ctx(ssk)->penalise)
+		mptcp_penalise_cwnd(ssk);
+
 	while ((dfrag = mptcp_send_head(sk))) {
 		info->sent = dfrag->already_sent;
 		info->limit = dfrag->data_len;
diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h
index da40c6f3705f..2bf801292563 100644
--- a/net/mptcp/protocol.h
+++ b/net/mptcp/protocol.h
@@ -587,6 +587,7 @@ struct mptcp_subflow_context {
 		__unused : 8;
 	bool	data_avail;
 	bool	scheduled;
+	bool	penalise;	    /* scheduler flagged this subflow for cwnd halving */
 	bool	pm_listener;	    /* a listener managed by the kernel PM? */
 	bool	fully_established;  /* path validated */
 	u32	lent_mem_frag;
@@ -606,6 +607,7 @@ struct mptcp_subflow_context {
 	u8	stale_count;
 
 	u32	subflow_id;
+	u32	last_penalise;	    /* tcp_jiffies32 of the last cwnd penalty */
 
 	long	delegated_status;
 	unsigned long	fail_tout;

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH mptcp-next 2/3] mptcp: sched: do not penalise when receive-window-limited
  2026-07-26  5:55 [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab) Shardul Bankar
  2026-07-26  5:55 ` [PATCH mptcp-next 1/3] mptcp: sched: penalise a slow subflow by halving its cwnd Shardul Bankar
@ 2026-07-26  5:55 ` Shardul Bankar
  2026-07-29 11:49   ` Matthieu Baerts
  2026-07-26  5:55 ` [PATCH mptcp-next 3/3] DO-NOT-MERGE: mptcp: sched: penalise counters Shardul Bankar
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 9+ messages in thread
From: Shardul Bankar @ 2026-07-26  5:55 UTC (permalink / raw)
  To: Matthieu Baerts (NGI0), MPTCP Linux; +Cc: shardulsb08, Shardul Bankar

The penalty in the previous patch shifts load off a slow subflow onto the
fastest one, which only helps if the fastest path can absorb it. When the
connection is receive-window-limited (the receiver's advertised window,
not our congestion window, is the bottleneck), the fastest path is capped
by that shared window too and cannot send more, so halving the slow path's
cwnd just sheds its throughput. In a receive-window-limited transfer this
was measured roughly 2x slower than baseline.

Gate on the application's queued data fitting within the send window:
penalise only while write_seq <= wnd_end. If the application has queued
past the window edge the receive window is the binding constraint, so skip
the penalty. Both write_seq (application demand) and wnd_end (peer window)
are standing values and neither is derived from cwnd, so the test is not
biased by the scheduler sampling just after an ACK opened the window, nor
made circular by the window itself suppressing cwnd.

Co-developed-by: Matthieu Baerts (NGI0) <matttbe@kernel.org>
Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com>
---
 net/mptcp/protocol.c | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index d31bcb9ad894..7cbc5aa17e22 100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -1575,6 +1575,23 @@ static bool mptcp_penalise_throttle_ok(struct mptcp_subflow_context *subflow)
 	return tcp_jiffies32 - subflow->last_penalise >= max_t(u32, rtt, 1);
 }
 
+/* Only penalise when the connection is not receive-window-limited: all the
+ * data the application has queued fits within the current send window
+ * (write_seq <= wnd_end). If it has queued past the window edge, the peer's
+ * receive window (not our congestion window) is the bottleneck: the fast
+ * path is capped by that shared window too and cannot use capacity freed from
+ * the slow path, so penalising would only shed the slow path's throughput.
+ *
+ * write_seq (application demand) and wnd_end (peer-advertised window) are both
+ * standing values and neither is derived from cwnd, so unlike the instantaneous
+ * window headroom this is not biased by the scheduler sampling just after an
+ * ACK opened the window, nor circular when the window is what suppresses cwnd.
+ */
+static bool mptcp_penalise_send_window_ok(const struct mptcp_sock *msk)
+{
+	return msk->write_seq <= mptcp_wnd_end(msk);
+}
+
 /* Halve the congestion window (and ssthresh, if cwnd is past it) of a subflow
  * the scheduler flagged. Runs in the push path under the subflow socket lock,
  * which protects snd_cwnd. The congestion control grows the window back,
@@ -1681,6 +1698,7 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
 			    (u64)subflow->avg_pacing_rate * MPTCP_PENALISE_RATE_RATIO < max_pace &&
 			    inet_csk(ssk)->icsk_ca_state == TCP_CA_Open &&
 			    tcp_is_cwnd_limited(fastest) &&
+			    mptcp_penalise_send_window_ok(msk) &&
 			    mptcp_penalise_throttle_ok(subflow);
 
 	burst = min(MPTCP_SEND_BURST_SIZE, mptcp_wnd_end(msk) - msk->snd_nxt);

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH mptcp-next 3/3] DO-NOT-MERGE: mptcp: sched: penalise counters
  2026-07-26  5:55 [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab) Shardul Bankar
  2026-07-26  5:55 ` [PATCH mptcp-next 1/3] mptcp: sched: penalise a slow subflow by halving its cwnd Shardul Bankar
  2026-07-26  5:55 ` [PATCH mptcp-next 2/3] mptcp: sched: do not penalise when receive-window-limited Shardul Bankar
@ 2026-07-26  5:55 ` Shardul Bankar
  2026-07-29 11:50   ` Matthieu Baerts
  2026-07-26  7:14 ` [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab) MPTCP CI
  2026-07-29 11:48 ` Matthieu Baerts
  4 siblings, 1 reply; 9+ messages in thread
From: Shardul Bankar @ 2026-07-26  5:55 UTC (permalink / raw)
  To: Matthieu Baerts (NGI0), MPTCP Linux; +Cc: shardulsb08, Shardul Bankar

Instrumentation for validating the two preceding patches; not for merge.

Adds two MPTcpExt SNMP counters:
- CwndPenalized: times a subflow cwnd was actually halved;
- PenalCandidate: times the rate trigger picked a slow subflow.

Together they separate "the guard held the penalty back" (PenalCandidate
high, CwndPenalized ~0) from "the trigger never fired" (both ~0), which is
what the receive-window-limited case needs to be read correctly.

Co-developed-by: Matthieu Baerts (NGI0) <matttbe@kernel.org>
Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com>
---
 net/mptcp/mib.c      | 2 ++
 net/mptcp/mib.h      | 2 ++
 net/mptcp/protocol.c | 8 ++++++++
 3 files changed, 12 insertions(+)

diff --git a/net/mptcp/mib.c b/net/mptcp/mib.c
index d9bd4f4afcc0..1299613e183b 100644
--- a/net/mptcp/mib.c
+++ b/net/mptcp/mib.c
@@ -88,6 +88,8 @@ static const struct snmp_mib mptcp_snmp_list[] = {
 	SNMP_MIB_ITEM("BacklogDrop", MPTCP_MIB_BACKLOGDROP),
 	SNMP_MIB_ITEM("RcvPruned", MPTCP_MIB_RCVPRUNED),
 	SNMP_MIB_ITEM("OfoPruned", MPTCP_MIB_OFO_PRUNED),
+	SNMP_MIB_ITEM("CwndPenalized", MPTCP_MIB_CWNDPENALIZED),
+	SNMP_MIB_ITEM("PenalCandidate", MPTCP_MIB_PENALCAND),
 };
 
 /* mptcp_mib_alloc - allocate percpu mib counters
diff --git a/net/mptcp/mib.h b/net/mptcp/mib.h
index 18f35f7e0a2d..93b3a7e9584f 100644
--- a/net/mptcp/mib.h
+++ b/net/mptcp/mib.h
@@ -91,6 +91,8 @@ enum linux_mptcp_mib_field {
 	MPTCP_MIB_BACKLOGDROP,		/* Backlog over memory limit */
 	MPTCP_MIB_RCVPRUNED,		/* Dropped due to memory constrains */
 	MPTCP_MIB_OFO_PRUNED,		/* MPTCP-level OoO queue pruned */
+	MPTCP_MIB_CWNDPENALIZED,	/* DEBUG: subflow cwnd halved by the scheduler (#345) */
+	MPTCP_MIB_PENALCAND,		/* DEBUG: picker chose a slow (low-rate) subflow */
 	__MPTCP_MIB_MAX
 };
 
diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index 7cbc5aa17e22..80866f09831a 100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -1607,6 +1607,7 @@ static void mptcp_penalise_cwnd(struct sock *ssk)
 	subflow->penalise = false;
 	subflow->last_penalise = tcp_jiffies32;
 	tcp_snd_cwnd_set(tp, max_t(u32, cwnd >> 1, 2));
+	MPTCP_INC_STATS(sock_net(ssk), MPTCP_MIB_CWNDPENALIZED);
 	if (cwnd >= tp->snd_ssthresh)
 		tp->snd_ssthresh = max_t(u32, tp->snd_ssthresh >> 1, 2);
 }
@@ -1694,6 +1695,13 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
 	 * once per RTT.
 	 */
 	subflow = mptcp_subflow_ctx(ssk);
+	/* DEBUG: count how often the trigger picks a slow path, so a gated-off
+	 * run (PenalCandidate high, CwndPenalized 0) is distinguishable from one
+	 * where the trigger never fired.
+	 */
+	if (fastest && ssk != fastest &&
+	    (u64)subflow->avg_pacing_rate * MPTCP_PENALISE_RATE_RATIO < max_pace)
+		MPTCP_INC_STATS(sock_net(ssk), MPTCP_MIB_PENALCAND);
 	subflow->penalise = fastest && ssk != fastest &&
 			    (u64)subflow->avg_pacing_rate * MPTCP_PENALISE_RATE_RATIO < max_pace &&
 			    inet_csk(ssk)->icsk_ca_state == TCP_CA_Open &&

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab)
  2026-07-26  5:55 [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab) Shardul Bankar
                   ` (2 preceding siblings ...)
  2026-07-26  5:55 ` [PATCH mptcp-next 3/3] DO-NOT-MERGE: mptcp: sched: penalise counters Shardul Bankar
@ 2026-07-26  7:14 ` MPTCP CI
  2026-07-29 11:48 ` Matthieu Baerts
  4 siblings, 0 replies; 9+ messages in thread
From: MPTCP CI @ 2026-07-26  7:14 UTC (permalink / raw)
  To: Shardul Bankar; +Cc: mptcp

Hi Shardul,

Thank you for your modifications, that's great!

Our CI did some validations and here is its report:

- KVM Validation: normal (except selftest_mptcp_join): Success! ✅
- KVM Validation: normal (only selftest_mptcp_join): Success! ✅
- KVM Validation: debug (except selftest_mptcp_join): Success! ✅
- KVM Validation: debug (only selftest_mptcp_join): Success! ✅
- KVM Validation: btf-normal (only bpftest_all): Success! ✅
- KVM Validation: btf-debug (only bpftest_all): Success! ✅
- Task: https://github.com/multipath-tcp/mptcp_net-next/actions/runs/30190751415

Initiator: Patchew Applier
Commits: https://github.com/multipath-tcp/mptcp_net-next/commits/ccb68814458b
Patchwork: https://patchwork.kernel.org/project/mptcp/list/?series=1134545


If there are some issues, you can reproduce them using the same environment as
the one used by the CI thanks to a docker image, e.g.:

    $ cd [kernel source code]
    $ docker run -v "${PWD}:${PWD}:rw" -w "${PWD}" --privileged --rm -it \
        --pull always mptcp/mptcp-upstream-virtme-docker:latest \
        auto-normal

For more details:

    https://github.com/multipath-tcp/mptcp-upstream-virtme-docker


Please note that despite all the efforts that have been already done to have a
stable tests suite when executed on a public CI like here, it is possible some
reported issues are not due to your modifications. Still, do not hesitate to
help us improve that ;-)

Cheers,
MPTCP GH Action bot
Bot operated by Matthieu Baerts (NGI0 Core)

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab)
  2026-07-26  5:55 [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab) Shardul Bankar
                   ` (3 preceding siblings ...)
  2026-07-26  7:14 ` [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab) MPTCP CI
@ 2026-07-29 11:48 ` Matthieu Baerts
  4 siblings, 0 replies; 9+ messages in thread
From: Matthieu Baerts @ 2026-07-29 11:48 UTC (permalink / raw)
  To: Shardul Bankar; +Cc: shardulsb08, MPTCP Linux

Hi Shardul,

On 26/07/2026 07:55, Shardul Bankar wrote:
> Hi Matt,
> 
> Following up on my note about aligning #345 to mptcp_rcv_buf_optimization():
> here is a first cut, as a 3-patch series. A few things came out differently
> from what I described there, so I have called each one out below rather than
> leave it for you to spot.
> 
>   1/3 penalise a slow subflow by halving its cwnd
>   2/3 do not penalise when receive-window-limited
>   3/3 DO-NOT-MERGE counters (for testing only)
> 
> 1/3 is the simple version. In the scheduler, once a subflow is picked, it is
> flagged for a cwnd halving that is applied in the push path under the subflow
> socket lock (so it is safe with the per-subflow locks, as I mentioned). The
> flag is set when the subflow is clearly slower than the fastest path, the
> fastest path is cwnd-limited, and the subflow is in TCP_CA_Open. The reduction
> halves cwnd (and ssthresh if cwnd is past it), at most once per RTT, and the
> congestion control grows it back.

Thank you for having sent these patches!

Note for others: these were supposed to be offlist RFC patches, as an
iteration for the development we started off list, but they were
accidentally shared here. I think that's fine, sorry for the noise, but
please consider this series as an RFC.

I have some questions and small comments.

> What differs from what I described, and why:
> 
> - Trigger on delivery rate, not RTT. I had said "slower by RTT". In testing
>   that over-penalised a path that is only higher latency but still carries its
>   share of the traffic (equal bandwidth, unequal delay): it fires on the
>   slower-by-latency path even though shrinking its window loses real goodput.
>   Keying on the pacing rate instead (penalise only a path whose rate is below
>   half the fastest) targets a genuinely low-throughput path, and reuses the
>   avg_pacing_rate the scheduler already maintains. On the threshold I raised
>   with you (the fork's "any slower" versus a small factor): I had said I would
>   default to "any slower", but with a rate trigger that fires on almost every
>   non-fastest path, since rates always vary a little, so I used the factor to
>   keep it to genuinely slow paths, as I flagged might be needed. Half is just
>   a starting point, easy to tune.

Sounds good to me!

> - I did not carry over the fork's "meta is send-buffer-limited" gate. In
>   mainline the msk send buffer is the sum of the subflow send buffers, and it
>   is effectively never full when the scheduler samples it (the scheduler runs
>   on the push path, just after an ACK has opened room), so that gate never
>   fires and the penalty stays dormant. That is why 1/3 has no send-buffer
>   condition.
> 
> - 2/3 is a guard that is in neither the fork nor what I described. Without it,
>   1/3 regresses badly (about 2x slower in my runs) when the connection is
>   receive-window-limited. In that case the fastest path is capped by the same
>   shared window, so it cannot absorb what the slow path gives up, and halving
>   just sheds the slow path's throughput. 2/3 skips the penalty while the
>   application has queued past the send-window edge (write_seq > wnd_end), which
>   is the sign that the receiver, not our congestion window, is the bottleneck.
>   I kept it a separate patch so you can test 1/3 on its own, or drop or retune
>   2/3 independently. The exact condition is the piece I would most value your
>   lab checking.

It feels to me that you require this because patch 1/3 doesn't check if
the MPTCP connection was "send-buffer-limited", no? But you are doing
something very similar, no? Without testing, it feels like this is
required not to limit the penalisation to when it is really needed.

> Testing was local (network namespaces plus netem, patched against a clean
> mptcp/export), starting from the existing simult_flows selftest. It is a debug
> kernel and mostly single runs, so please read the numbers as directional; I am
> happy to share the full logs and the scenario script.
> 
> - No regression on the simult_flows suite.
> - To see the intended effect I looked at MPTcpExtOFOQueue, the number of
>   segments the receiver had to hold out of order over the transfer, since that
>   is what the change is meant to reduce and a plain throughput number cannot
>   show it. On the asymmetric-bandwidth pair (10 vs 3 mbit) with a small
>   SO_SNDBUF, that count fell by roughly a fifth (about 18 to 23% in my runs)
>   with no change in throughput. So this is a reduction in reordering, i.e. a
>   latency and smoothness effect, not more bytes per second; whether that is
>   worth it for a real workload is exactly what I hope your lab can judge.
> - The receive-window-limited regression above is back to baseline with 2/3.
> - Two honest limits. First, with fully autotuned buffers (the common default)
>   the guard does not fire, because the connection is not receive-window-
>   limited, and the penalty then leaves a small reordering cost: halving trims
>   the slow path's delivery rate, so its share of the in-order stream arrives a
>   little later and the out-of-order count rises a few percent. I did not find a
>   simple way to also suppress
>   that without re-opening the gating, and I did not want to over-build v1;
>   scoping it more tightly (for example only when send-buffer-limited) may be
>   the right call and I would defer to your lab on it. Second, I have only
>   exercised two subflows and no backup subflow so far.
> 
> 3/3 adds two MPTcpExt counters (CwndPenalized, PenalCandidate) so a run can
> tell "the guard held the penalty back" from "the trigger never fired". Not for
> merge. I left your Co-developed-by on it in case it is useful to you elsewhere.
> 
> I drove those regimes with a small simult_flows variant (receive-window-
> limited, send-buffer-limited, and autotuned cases). It is a helper, not
> selftest quality, so I did not fold it into the series; it is on a branch of
> my tree, in case it saves your lab time or you spot a case I missed:
> 
> https://github.com/shardulsdk-mpiric/linux/blob/6926c4b7f583/tools/testing/selftests/net/mptcp/mptcp_sched_penalise.sh
> 
> Run it on a baseline and a patched kernel and compare (prefix with
> MPTCP_LIB_IP_MPTCP=1 if pm_nl_ctl does not work in your setup):
> 
>   SCENARIO=suite                              ./mptcp_sched_penalise.sh
>   SCENARIO=unbounded                          ./mptcp_sched_penalise.sh
>   SCENARIO=rwnd    RCVBUF=262144              ./mptcp_sched_penalise.sh
>   SCENARIO=sndbuf  SNDBUF=65536               ./mptcp_sched_penalise.sh
>   SCENARIO=both    RCVBUF=262144 SNDBUF=65536 ./mptcp_sched_penalise.sh

Sounds good! Did you check with a fixed sndbuf higher than the rcv one?

Also, be careful that with netem, the limits you give to run_test() can
influence a lot the bufferbloat. Did you monitor the RTTs during these
transfers?

On the other hand, it would be good to validate this with one path
having bufferbloat. These patches should also help to improve the
situation. (And issue #332 should help even more)

> For the rwnd/sndbuf/both scenarios the simult_flows pass/fail bound is not
> meaningful (it assumes both paths are fully used): read the printed runtime
> and out-of-order counts, not OK/FAIL. The "both" case also occasionally fails
> to bring up the second subflow with the very small SO_SNDBUF; just rerun it if
> you see a single-subflow run.

I see, yes. I think what is important here for #345, is that when the
transfer is buffer limited, the slow subflow impact should be reduced.
At least not to cause the transfer to be worse than without this slow
subflow.

Cheers,
Matt
-- 
Sponsored by the NGI0 Core fund.


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH mptcp-next 1/3] mptcp: sched: penalise a slow subflow by halving its cwnd
  2026-07-26  5:55 ` [PATCH mptcp-next 1/3] mptcp: sched: penalise a slow subflow by halving its cwnd Shardul Bankar
@ 2026-07-29 11:49   ` Matthieu Baerts
  0 siblings, 0 replies; 9+ messages in thread
From: Matthieu Baerts @ 2026-07-29 11:49 UTC (permalink / raw)
  To: Shardul Bankar; +Cc: shardulsb08, MPTCP Linux

Hi Shardul,

On 26/07/2026 07:55, Shardul Bankar wrote:
> Issue #345: a poorly-performing but usable subflow (high latency, loss,
> bufferbloat) can soak up connection resources and cause head-of-line
> blocking of the aggregate stream. Give the default packet scheduler a way
> to send less than such a subflow's full congestion window.
> 
> Once a subflow has been picked for transmission, flag it for penalisation
> when:
> - its smoothed delivery rate (avg_pacing_rate) is below half that of the
>   fastest path, keying on rate, not RTT, so a slow-but-high-throughput
>   path is left alone;
> - the fastest path is cwnd-limited (saturated), so shifting load off the
>   slow path is worthwhile;
> - the subflow is in TCP_CA_Open, so its cwnd is not already being reduced
>   by loss recovery;
> - it has not been penalised in the last RTT.
> 
> The reduction halves tcp_snd_cwnd (floor 2) and ssthresh if cwnd
> is past it. It is applied in the push path under the subflow socket lock,
> which protects snd_cwnd (the scheduler runs under the msk lock). The
> congestion control grows the window back, ACK-clocked; that regrowth is
> the built-in probe, so no explicit MPTCP-side probing is needed.
> 
> Co-developed-by: Matthieu Baerts (NGI0) <matttbe@kernel.org>
> Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com>
> ---
>  net/mptcp/protocol.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++++---
>  net/mptcp/protocol.h |  2 ++
>  2 files changed, 67 insertions(+), 3 deletions(-)
> 
> diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
> index 954e20bb27de..d31bcb9ad894 100644
> --- a/net/mptcp/protocol.c
> +++ b/net/mptcp/protocol.c
> @@ -1556,6 +1556,44 @@ bool mptcp_subflow_active(struct mptcp_subflow_context *subflow)
>  #define SSK_MODE_BACKUP	1
>  #define SSK_MODE_MAX	2
>  
> +/* Penalise a subflow whose delivery (pacing) rate is below the fraction
> + * 1 / MPTCP_PENALISE_RATE_RATIO of the fastest path's rate. Keying on rate,
> + * not RTT, throttles only a path whose throughput contribution is small
> + * relative to the head-of-line cost it imposes, and leaves a merely
> + * higher-latency but high-throughput path alone.

Could you reduce the size of the comments, please? I understood that LLM
tends to leave long comments, but too long is not always good:
explanations can go in the commit message, or be understood by reading
the conditions (or that's a sign the code should be improved). Ideally,
comments should not be needed when reading the code, or limited to one,
with some exceptions for complex cases.

Here for example, I think you could limit the comment to one line:

/* Penalise subflows with pacing rate < this fraction of the fastest path */

> + */
> +#define MPTCP_PENALISE_RATE_RATIO	2
> +
> +/* Rate-limit the penalty to at most once per subflow RTT, so the congestion
> + * control can grow the window back between reductions.
> + */
> +static bool mptcp_penalise_throttle_ok(struct mptcp_subflow_context *subflow)
> +{
> +	struct sock *ssk = mptcp_subflow_tcp_sock(subflow);
> +	u32 rtt = usecs_to_jiffies(tcp_sk(ssk)->srtt_us >> 3);
> +
> +	return tcp_jiffies32 - subflow->last_penalise >= max_t(u32, rtt, 1);
> +}
> +
> +/* Halve the congestion window (and ssthresh, if cwnd is past it) of a subflow
> + * the scheduler flagged. Runs in the push path under the subflow socket lock,
> + * which protects snd_cwnd. The congestion control grows the window back,
> + * ACK-clocked, and that regrowth is the built-in probe, so no explicit probing
> + * is needed.
> + */

Same here: I think what is important to mention is that it is under
subflow socket lock, and you can move the comment about the CC growing
the window back below: no explicit probing required then.

> +static void mptcp_penalise_cwnd(struct sock *ssk)
> +{
> +	struct mptcp_subflow_context *subflow = mptcp_subflow_ctx(ssk);
> +	struct tcp_sock *tp = tcp_sk(ssk);
> +	u32 cwnd = tcp_snd_cwnd(tp);
> +
> +	subflow->penalise = false;

You probably need to check inet_csk(ssk)->icsk_ca_state == TCP_CA_Open
again here, under the subflow socket lock, just in case it has been
modified in between.

> +	subflow->last_penalise = tcp_jiffies32;
> +	tcp_snd_cwnd_set(tp, max_t(u32, cwnd >> 1, 2));
> +	if (cwnd >= tp->snd_ssthresh)
> +		tp->snd_ssthresh = max_t(u32, tp->snd_ssthresh >> 1, 2);
> +}
> +
>  /* implement the mptcp packet scheduler;
>   * returns the subflow that will transmit the next DSS
>   * additionally updates the rtx timeout
> @@ -1565,9 +1603,9 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
>  	struct subflow_send_info send_info[SSK_MODE_MAX];
>  	struct mptcp_subflow_context *subflow;
>  	struct sock *sk = (struct sock *)msk;
> -	u32 pace, burst, wmem;
> +	u32 pace, burst, wmem, max_pace = 0;

Sashiko is (rightly I think) mentioning that the size of these variables
are wrong. I guess a separate fix is needed to change "pace" to
"unsigned long" is required. "max_pace" will get this type too then.

>  	int i, nr_active = 0;
> -	struct sock *ssk;
> +	struct sock *ssk, *fastest = NULL;
>  	u64 linger_time;
>  	long tout = 0;
>  
> @@ -1596,6 +1634,14 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
>  				continue;
>  		}
>  
> +		/* track the fastest path by delivery rate; the penalty below
> +		 * throttles paths that are slow relative to it.
> +		 */
> +		if (pace > max_pace) {
> +			max_pace = pace;
> +			fastest = ssk;
> +		}
> +
>  		linger_time = div_u64((u64)READ_ONCE(ssk->sk_wmem_queued) << 32, pace);
>  		if (linger_time < send_info[backup].linger_time) {
>  			send_info[backup].ssk = ssk;
> @@ -1623,12 +1669,25 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
>  	if (!ssk || !sk_stream_memory_free(ssk))
>  		return NULL;
>  
> +	/* Flag the chosen subflow for cwnd halving (applied in the push path)
> +	 * when its delivery rate is a small fraction of the fastest path's and
> +	 * that fast path is saturated (cwnd-limited), so moving load off the
> +	 * slow path is worthwhile. Only penalise a path in TCP_CA_Open, one
> +	 * whose cwnd is not already being shrunk by loss recovery, and at most
> +	 * once per RTT.
> +	 */

Same here. Maybe a comment is not needed, or limited?

> +	subflow = mptcp_subflow_ctx(ssk);
> +	subflow->penalise = fastest && ssk != fastest &&
> +			    (u64)subflow->avg_pacing_rate * MPTCP_PENALISE_RATE_RATIO < max_pace &&

(or divide max_pace once before?)

> +			    inet_csk(ssk)->icsk_ca_state == TCP_CA_Open &&
> +			    tcp_is_cwnd_limited(fastest) &&
> +			    mptcp_penalise_throttle_ok(subflow);
> +
>  	burst = min(MPTCP_SEND_BURST_SIZE, mptcp_wnd_end(msk) - msk->snd_nxt);
>  	wmem = READ_ONCE(ssk->sk_wmem_queued);
>  	if (!burst)
>  		return ssk;
>  
> -	subflow = mptcp_subflow_ctx(ssk);
>  	subflow->avg_pacing_rate = div_u64((u64)subflow->avg_pacing_rate * wmem +
>  					   READ_ONCE(ssk->sk_pacing_rate) * burst,

(here as well, it looks like there is an existing bug, and a cast to u64
is probably needed for 32-bit system, as pointed by Sashiko)

>  					   burst + wmem);
> @@ -1685,6 +1744,9 @@ static int __subflow_push_pending(struct sock *sk, struct sock *ssk,
>  	struct mptcp_data_frag *dfrag;
>  	int len, copied = 0, err = 0;
>  
> +	if (mptcp_subflow_ctx(ssk)->penalise)
> +		mptcp_penalise_cwnd(ssk);
> +
>  	while ((dfrag = mptcp_send_head(sk))) {
>  		info->sent = dfrag->already_sent;
>  		info->limit = dfrag->data_len;
> diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h
> index da40c6f3705f..2bf801292563 100644
> --- a/net/mptcp/protocol.h
> +++ b/net/mptcp/protocol.h
> @@ -587,6 +587,7 @@ struct mptcp_subflow_context {
>  		__unused : 8;
>  	bool	data_avail;
>  	bool	scheduled;
> +	bool	penalise;	    /* scheduler flagged this subflow for cwnd halving */

(If you don't need to read this locklessly, then you can probably use
one unused bit.)

>  	bool	pm_listener;	    /* a listener managed by the kernel PM? */
>  	bool	fully_established;  /* path validated */
>  	u32	lent_mem_frag;
> @@ -606,6 +607,7 @@ struct mptcp_subflow_context {
>  	u8	stale_count;
>  
>  	u32	subflow_id;
> +	u32	last_penalise;	    /* tcp_jiffies32 of the last cwnd penalty */
>  
>  	long	delegated_status;
>  	unsigned long	fail_tout;
> 

Cheers,
Matt
-- 
Sponsored by the NGI0 Core fund.


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH mptcp-next 2/3] mptcp: sched: do not penalise when receive-window-limited
  2026-07-26  5:55 ` [PATCH mptcp-next 2/3] mptcp: sched: do not penalise when receive-window-limited Shardul Bankar
@ 2026-07-29 11:49   ` Matthieu Baerts
  0 siblings, 0 replies; 9+ messages in thread
From: Matthieu Baerts @ 2026-07-29 11:49 UTC (permalink / raw)
  To: Shardul Bankar; +Cc: shardulsb08, MPTCP Linux

Hi Shardul,

On 26/07/2026 07:55, Shardul Bankar wrote:
> The penalty in the previous patch shifts load off a slow subflow onto the
> fastest one, which only helps if the fastest path can absorb it. When the
> connection is receive-window-limited (the receiver's advertised window,
> not our congestion window, is the bottleneck), the fastest path is capped
> by that shared window too and cannot send more, so halving the slow path's
> cwnd just sheds its throughput. In a receive-window-limited transfer this
> was measured roughly 2x slower than baseline.
> 
> Gate on the application's queued data fitting within the send window:
> penalise only while write_seq <= wnd_end. If the application has queued
> past the window edge the receive window is the binding constraint, so skip
> the penalty. Both write_seq (application demand) and wnd_end (peer window)
> are standing values and neither is derived from cwnd, so the test is not
> biased by the scheduler sampling just after an ACK opened the window, nor
> made circular by the window itself suppressing cwnd.
> 
> Co-developed-by: Matthieu Baerts (NGI0) <matttbe@kernel.org>
> Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com>
> ---
>  net/mptcp/protocol.c | 18 ++++++++++++++++++
>  1 file changed, 18 insertions(+)
> 
> diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
> index d31bcb9ad894..7cbc5aa17e22 100644
> --- a/net/mptcp/protocol.c
> +++ b/net/mptcp/protocol.c
> @@ -1575,6 +1575,23 @@ static bool mptcp_penalise_throttle_ok(struct mptcp_subflow_context *subflow)
>  	return tcp_jiffies32 - subflow->last_penalise >= max_t(u32, rtt, 1);
>  }
>  
> +/* Only penalise when the connection is not receive-window-limited: all the
> + * data the application has queued fits within the current send window
> + * (write_seq <= wnd_end). If it has queued past the window edge, the peer's
> + * receive window (not our congestion window) is the bottleneck: the fast
> + * path is capped by that shared window too and cannot use capacity freed from
> + * the slow path, so penalising would only shed the slow path's throughput.
> + *
> + * write_seq (application demand) and wnd_end (peer-advertised window) are both
> + * standing values and neither is derived from cwnd, so unlike the instantaneous
> + * window headroom this is not biased by the scheduler sampling just after an
> + * ACK opened the window, nor circular when the window is what suppresses cwnd.

(a bit too long, some text can probably be moved to the commit message
if not there already → but also, I guess this commit will be squashed in
the previous one at the end)

> + */
> +static bool mptcp_penalise_send_window_ok(const struct mptcp_sock *msk)
> +{
> +	return msk->write_seq <= mptcp_wnd_end(msk);

It looks like you are doing something similar to tcp_snd_wnd_test(), no?

I guess you should at least use after64/before64. Here we don't have the
skb, that might change later if the MPTCP scheduler API is modified, but
that can be an optimisation for later.

You could name the helper mptcp_snd_wnd_test(), and mention it is
inspired by the TCP version, but without checking the packet len (for
the moment).

Other than that, this Sashiko's comment is interesting:

> Does this heuristic correctly identify receive-window bottlenecks without
> unintentionally disabling the penalty for congestion-limited bulk transfers?
> Because write_seq is limited by the socket send buffer (sk_sndbuf) rather
> than the congestion window, an application performing a bulk transfer with a
> large send buffer can easily queue data past the peer's advertised receive
> window.
> If the connection is heavily congestion-limited, the fast path is saturated
> and the slow path should still be penalized to reduce head-of-line blocking.
> However, since write_seq > mptcp_wnd_end(msk) in this scenario, it seems
> this check will incorrectly assume the connection is receive-window limited
> and skip the penalty.

By chance, did you already validate this case?

> +}
> +
>  /* Halve the congestion window (and ssthresh, if cwnd is past it) of a subflow
>   * the scheduler flagged. Runs in the push path under the subflow socket lock,
>   * which protects snd_cwnd. The congestion control grows the window back,
> @@ -1681,6 +1698,7 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
>  			    (u64)subflow->avg_pacing_rate * MPTCP_PENALISE_RATE_RATIO < max_pace &&
>  			    inet_csk(ssk)->icsk_ca_state == TCP_CA_Open &&
>  			    tcp_is_cwnd_limited(fastest) &&
> +			    mptcp_penalise_send_window_ok(msk) &&
>  			    mptcp_penalise_throttle_ok(subflow);
>  
>  	burst = min(MPTCP_SEND_BURST_SIZE, mptcp_wnd_end(msk) - msk->snd_nxt);
> 

Cheers,
Matt
-- 
Sponsored by the NGI0 Core fund.


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH mptcp-next 3/3] DO-NOT-MERGE: mptcp: sched: penalise counters
  2026-07-26  5:55 ` [PATCH mptcp-next 3/3] DO-NOT-MERGE: mptcp: sched: penalise counters Shardul Bankar
@ 2026-07-29 11:50   ` Matthieu Baerts
  0 siblings, 0 replies; 9+ messages in thread
From: Matthieu Baerts @ 2026-07-29 11:50 UTC (permalink / raw)
  To: Shardul Bankar; +Cc: shardulsb08, MPTCP Linux

Hi Shardul,

On 26/07/2026 07:55, Shardul Bankar wrote:
> Instrumentation for validating the two preceding patches; not for merge.
> 
> Adds two MPTcpExt SNMP counters:
> - CwndPenalized: times a subflow cwnd was actually halved;
> - PenalCandidate: times the rate trigger picked a slow subflow.

I think at least the first counter is interesting, probably the second
one as well, no?

Generally, if you need counters during the development, they might be
needed to debug issues. But here with the scheduler, maybe the tracing
are better. Did you use "trace_mptcp_subflow_get_send"?

> Together they separate "the guard held the penalty back" (PenalCandidate
> high, CwndPenalized ~0) from "the trigger never fired" (both ~0), which is
> what the receive-window-limited case needs to be read correctly.
> 
> Co-developed-by: Matthieu Baerts (NGI0) <matttbe@kernel.org>
> Signed-off-by: Shardul Bankar <shardul.b@mpiricsoftware.com>
> ---
>  net/mptcp/mib.c      | 2 ++
>  net/mptcp/mib.h      | 2 ++
>  net/mptcp/protocol.c | 8 ++++++++
>  3 files changed, 12 insertions(+)
> 
> diff --git a/net/mptcp/mib.c b/net/mptcp/mib.c
> index d9bd4f4afcc0..1299613e183b 100644
> --- a/net/mptcp/mib.c
> +++ b/net/mptcp/mib.c
> @@ -88,6 +88,8 @@ static const struct snmp_mib mptcp_snmp_list[] = {
>  	SNMP_MIB_ITEM("BacklogDrop", MPTCP_MIB_BACKLOGDROP),
>  	SNMP_MIB_ITEM("RcvPruned", MPTCP_MIB_RCVPRUNED),
>  	SNMP_MIB_ITEM("OfoPruned", MPTCP_MIB_OFO_PRUNED),
> +	SNMP_MIB_ITEM("CwndPenalized", MPTCP_MIB_CWNDPENALIZED),
> +	SNMP_MIB_ITEM("PenalCandidate", MPTCP_MIB_PENALCAND),
>  };
>  
>  /* mptcp_mib_alloc - allocate percpu mib counters
> diff --git a/net/mptcp/mib.h b/net/mptcp/mib.h
> index 18f35f7e0a2d..93b3a7e9584f 100644
> --- a/net/mptcp/mib.h
> +++ b/net/mptcp/mib.h
> @@ -91,6 +91,8 @@ enum linux_mptcp_mib_field {
>  	MPTCP_MIB_BACKLOGDROP,		/* Backlog over memory limit */
>  	MPTCP_MIB_RCVPRUNED,		/* Dropped due to memory constrains */
>  	MPTCP_MIB_OFO_PRUNED,		/* MPTCP-level OoO queue pruned */
> +	MPTCP_MIB_CWNDPENALIZED,	/* DEBUG: subflow cwnd halved by the scheduler (#345) */
> +	MPTCP_MIB_PENALCAND,		/* DEBUG: picker chose a slow (low-rate) subflow */
>  	__MPTCP_MIB_MAX
>  };
>  
> diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
> index 7cbc5aa17e22..80866f09831a 100644
> --- a/net/mptcp/protocol.c
> +++ b/net/mptcp/protocol.c
> @@ -1607,6 +1607,7 @@ static void mptcp_penalise_cwnd(struct sock *ssk)
>  	subflow->penalise = false;
>  	subflow->last_penalise = tcp_jiffies32;
>  	tcp_snd_cwnd_set(tp, max_t(u32, cwnd >> 1, 2));
> +	MPTCP_INC_STATS(sock_net(ssk), MPTCP_MIB_CWNDPENALIZED);
>  	if (cwnd >= tp->snd_ssthresh)
>  		tp->snd_ssthresh = max_t(u32, tp->snd_ssthresh >> 1, 2);
>  }
> @@ -1694,6 +1695,13 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk)
>  	 * once per RTT.
>  	 */
>  	subflow = mptcp_subflow_ctx(ssk);
> +	/* DEBUG: count how often the trigger picks a slow path, so a gated-off
> +	 * run (PenalCandidate high, CwndPenalized 0) is distinguishable from one
> +	 * where the trigger never fired.
> +	 */
> +	if (fastest && ssk != fastest &&
> +	    (u64)subflow->avg_pacing_rate * MPTCP_PENALISE_RATE_RATIO < max_pace)

If this counter is interesting, you could have a variable for penal_cand
and use it below.

> +		MPTCP_INC_STATS(sock_net(ssk), MPTCP_MIB_PENALCAND);
>  	subflow->penalise = fastest && ssk != fastest &&
>  			    (u64)subflow->avg_pacing_rate * MPTCP_PENALISE_RATE_RATIO < max_pace &&
>  			    inet_csk(ssk)->icsk_ca_state == TCP_CA_Open &&
> 

Cheers,
Matt
-- 
Sponsored by the NGI0 Core fund.


^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-07-29 11:50 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-26  5:55 [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab) Shardul Bankar
2026-07-26  5:55 ` [PATCH mptcp-next 1/3] mptcp: sched: penalise a slow subflow by halving its cwnd Shardul Bankar
2026-07-29 11:49   ` Matthieu Baerts
2026-07-26  5:55 ` [PATCH mptcp-next 2/3] mptcp: sched: do not penalise when receive-window-limited Shardul Bankar
2026-07-29 11:49   ` Matthieu Baerts
2026-07-26  5:55 ` [PATCH mptcp-next 3/3] DO-NOT-MERGE: mptcp: sched: penalise counters Shardul Bankar
2026-07-29 11:50   ` Matthieu Baerts
2026-07-26  7:14 ` [PATCH mptcp-next 0/3] mptcp: sched: penalise a slow subflow (#345, first cut for your lab) MPTCP CI
2026-07-29 11:48 ` Matthieu Baerts

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox