mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Paolo Abeni <pabeni@redhat.com>
To: rkannoth@marvell.com
Cc: bpf@vger.kernel.org, linux-kernel@vger.kernel.org,
	netdev@vger.kernel.org, andrew+netdev@lunn.ch, ast@kernel.org,
	daniel@iogearbox.net, davem@davemloft.net, edumazet@google.com,
	hawk@kernel.org, john.fastabend@gmail.com, kuba@kernel.org,
	pabeni@redhat.com, sdf@fomichev.me, sgoutham@marvell.com
Subject: Re: [PATCH v15 net-next 2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers
Date: Thu, 17 Sep 2026 13:31:50 +0200	[thread overview]
Message-ID: <20260917113150.171042-1-pabeni@redhat.com> (raw)
In-Reply-To: <20260911105521.689565-3-rkannoth@marvell.com>

This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://netdev-ai.bots.linux.dev/sashiko/
---
octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers

This adds TC_SETUP_QDISC_MQPRIO channel-mode offload with
TC_MQPRIO_SHAPER_BW_RATE, programming per-queue MDQ CIR/PIR through the
NIX TX scheduler mailbox. Since the scheduler hierarchy cannot be
reprogrammed live, every mqprio add, replace, delete and rollback bounces
the netdev through ndo_stop()/ndo_open(), with the TC layout and rates
tracked in mq_offload_snap snapshots for rollback.

A few questions on the commit message first.

> Use atomic bit operations when updating OTX2_FLAG_PORT_UP and
> OTX2_FLAG_INTF_DOWN on asynchronous and netdev-restart paths.
>
> Add a ratelimited AF debug message when validating TX scheduler queue
> ownership to aid mqprio hierarchy setup failures.

[Severity: Low]
The first paragraph appears to describe the immediately preceding commit
("octeontx2: use atomic bitops for PF/VF and rep flags"), which is what
converted the flags field to bit numbers and introduced otx2_set_flag().
The only new flag write added here is the
otx2_set_flag(pfvf, OTX2_FLAG_INTF_DOWN) call in
otx2_mqprio_restart_netdev(), which simply inherits that conversion.
Should this paragraph be dropped?

For the second paragraph, the AF change adds dev_err_ratelimited() in
is_valid_txschq(), which is an error-level print rather than a debug
message, and it is reachable from VF mailbox requests. Could the wording
match the log level used, or should the print be pr_debug/dev_dbg level?

> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> index d80d2c00bd843..2d1ab8c761c40 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> @@ -331,8 +331,12 @@ static bool is_valid_txschq(struct rvu *rvu, int blkaddr,
>  			return true;
>  	}
>  
> -	if (map_func != pcifunc)
> +	if (map_func != pcifunc) {
> +		dev_err_ratelimited(rvu->dev,
> +				    "pcifunc %x map pcifunc %x not equal, lvl=%u schq=%u\n",
> +				    pcifunc, map_func, lvl, schq);
>  		return false;
> +	}
>  
>  	return true;
>  }

[ ... ]

> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.h b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.h
> index 7e09c1444a6d5..6e7132f27b621 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.h
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_common.h
> @@ -483,6 +484,23 @@ struct pf_irq_data {
>  	int mdevs;
>  };
>  
> +struct mq_offload_snap {
> +	u64 min_rate[TC_QOPT_MAX_QUEUE];
> +	u64 max_rate[TC_QOPT_MAX_QUEUE];
> +	__u8 num_tc;
> +	__u16 count[TC_QOPT_MAX_QUEUE];
> +	__u16 offset[TC_QOPT_MAX_QUEUE];
> +};

[Severity: Medium]
Should the snapshot also record prio_tc_map[]?

sch_mqprio installs three pieces of netdev state for an offloaded mqprio,
and the priority map is applied from the new qopt after the driver
callback returns success:

net/sched/sch_mqprio.c:mqprio_init() {
	...
	/* Always use supplied priority mappings */
	for (i = 0; i < TC_BITMASK + 1; i++)
		netdev_set_prio_tc_map(dev, i, qopt->prio_tc_map[i]);
	...
}

otx2_mqprio_restore_old() only replays num_tc and the per-TC
count/offset pair:

	netdev_set_num_tc(netdev, snap->num_tc);
	for (tc = 0; tc < snap->num_tc; tc++)
		netdev_set_tc_queue(netdev, tc, snap->count[tc],
				    snap->offset[tc]);

After a rollback of a replacement whose setup succeeded but which was
never grafted (for example qdisc_create() failing at err_out4 with
TCQ_F_MQROOT and then calling ops->destroy()), dev->prio_tc_map[] still
describes the abandoned layout while num_tc/tc_to_txq describe the old
one, so skb_tx_hash() steers traffic to queues whose MDQ shapers belong
to a different configuration.

[ ... ]

> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
> index 32582b6347ea4..1e17f8d49fd44 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
> @@ -2007,6 +2007,14 @@ int otx2_open(struct net_device *netdev)
>  	if (err)
>  		goto err_free_mem;
>  
> +	err = otx2_mqprio_up(pf);
> +	if (err) {
> +		netdev_err(pf->netdev,
> +			   "mqprio: failed to restore shapers during open: %d\n",
> +			   err);
> +		goto err_free_hw;
> +	}
> +
>  	/* Register NAPI handler */
>  	for (qidx = 0; qidx < pf->hw.cint_cnt; qidx++) {
>  		cq_poll = &qset->napi[qidx];
> @@ -2205,6 +2213,7 @@ int otx2_open(struct net_device *netdev)
>  	free_irq(vec, pf);
>  err_disable_napi:
>  	otx2_disable_napi(pf);
> +err_free_hw:
>  	otx2_free_hw_resources(pf);
>  err_free_mem:
>  	otx2_free_queue_mem(qset);

[Severity: Medium]
This isn't a bug introduced by this patch, but does this new error label
leak pf->refill_wrk?

pf->refill_wrk is allocated (and its delayed works initialised) inside
otx2_init_hw_resources() -> otx2_config_nix_queues():

	pfvf->refill_wrk = devm_kcalloc(pfvf->dev, pfvf->qset.cq_cnt,
					sizeof(struct refill_work), GFP_KERNEL);

The only place it is cancelled and freed is otx2_stop():

	for (wrk = 0; wrk < pf->qset.cq_cnt; wrk++)
		cancel_delayed_work_sync(&pf->refill_wrk[wrk].pool_refill_work);
	devm_kfree(pf->dev, pf->refill_wrk);

None of the otx2_open() error labels, including the new err_free_hw,
cancel the works or free the array, so each failed open leaks one array
until driver detach. This unbalanced lifecycle is shared by all the
pre-existing labels, but this patch adds a new one and makes ndo_open()
failure routinely reachable from userspace through
otx2_mqprio_restart_netdev() on tc qdisc add/replace/del.

[ ... ]

> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
> index ddb46b580c3b6..70d99ae3defb5 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_tc.c
> @@ -1600,14 +1609,750 @@ static int otx2_setup_tc_block(struct net_device *netdev,
>  					  nic, nic, ingress);
>  }
>  
> +/* Free the per-queue min/max rate caches. */
> +static void otx2_mqprio_free_cache(struct otx2_nic *pfvf)
> +{
> +	devm_kfree(pfvf->dev, pfvf->mqprio.min_rate);
> +	devm_kfree(pfvf->dev, pfvf->mqprio.max_rate);
> +	pfvf->mqprio.min_rate = NULL;
> +	pfvf->mqprio.max_rate = NULL;
> +	pfvf->mqprio.flags = 0;
> +}
> +
> +static int otx2_mqprio_alloc_cache(struct otx2_nic *pfvf, bool replacing)
> +{
> +	u16 num_txq = pfvf->hw.non_qos_queues;
> +
> +	if (replacing && pfvf->mqprio.min_rate && pfvf->mqprio.max_rate) {
> +		memset(pfvf->mqprio.min_rate, 0,
> +		       num_txq * sizeof(*pfvf->mqprio.min_rate));
> +		memset(pfvf->mqprio.max_rate, 0,
> +		       num_txq * sizeof(*pfvf->mqprio.max_rate));
> +		pfvf->mqprio.flags = 0;
> +		return 0;
> +	}
> +
> +	otx2_mqprio_free_cache(pfvf);
> +
> +	pfvf->mqprio.min_rate = devm_kcalloc(pfvf->dev, num_txq,
> +					     sizeof(*pfvf->mqprio.min_rate),
> +					     GFP_KERNEL);
> +	pfvf->mqprio.max_rate = devm_kcalloc(pfvf->dev, num_txq,
> +					     sizeof(*pfvf->mqprio.max_rate),
> +					     GFP_KERNEL);
> +	if (!pfvf->mqprio.min_rate || !pfvf->mqprio.max_rate) {
> +		otx2_mqprio_free_cache(pfvf);
> +		return -ENOMEM;
> +	}
> +
> +	return 0;
> +}

[Severity: Medium]
Can a failed replace lose the active configuration permanently here?

otx2_mqprio_free_cache() releases the live caches before the two
devm_kcalloc() calls, so on -ENOMEM both min_rate and max_rate are NULL.
otx2_mqprio_restore_old() bails out before rebuilding anything:

	err = otx2_mqprio_alloc_cache(pfvf, false);
	if (err)
		return err;

At that point the old qdisc is still installed and mqprio.rate_limit is
still true, but the rates are gone. The next open cannot recover them
either, because otx2_mqprio_up() treats missing caches as success:

	if (!pfvf->mqprio.min_rate || !pfvf->mqprio.max_rate)
		return 0;

Would allocating into temporary pointers and only swapping them in on
success avoid destroying the live state?

[ ... ]

> +static int otx2_mqprio_restart_netdev(struct net_device *netdev, bool rate_limit);
> +
> +static int otx2_mqprio_restore_old(struct otx2_nic *pfvf)
> +{
> +	struct mq_offload_snap *snap = pfvf->old_mq_snap;
> +	struct net_device *netdev = pfvf->netdev;
> +	u16 num_txq = pfvf->hw.non_qos_queues;
> +	int tc, txq, err;
> +
> +	if (!snap)
> +		return 0;
> +
> +	err = otx2_mqprio_alloc_cache(pfvf, false);
> +	if (err)
> +		return err;

[ ... ]

> +	netdev_set_num_tc(netdev, snap->num_tc);
> +	for (tc = 0; tc < snap->num_tc; tc++)
> +		netdev_set_tc_queue(netdev, tc, snap->count[tc],
> +				    snap->offset[tc]);
> +
> +	if (otx2_mqprio_mdq_allocated(pfvf)) {
> +		err = otx2_nix_tm_clear_queue_shaper(pfvf);
> +		if (err)
> +			return err;
> +	}

[Severity: Medium]
Does this early return leave hardware and software state out of sync?

By the time this runs, the software cache has already been rewritten from
old_mq_snap, and the failed replace has already programmed some MDQ
CIR/PIR registers with the new rates in the
otx2_nix_tm_set_queue_shaper() loop of otx2_setup_tc_mqprio(). Returning
here skips both

	pfvf->mqprio.rate_limit = true;

and the netif_running() restart, so the interface keeps running with
per-queue shapers holding a partially applied configuration that the
driver's bookkeeping no longer describes, until some unrelated stop/open
cycle happens. Should a failure here force the netdev restart so the AF
resets shaping on freshly allocated MDQs?

> +
> +	/* Rebuild the TX scheduler via netdev restart when running; otx2_mqprio_up()
> +	 * alone is insufficient after a failed replace that already bounced the
> +	 * interface. If open failed, TX schedulers were freed; defer shaper restore
> +	 * to the next successful ndo_open() via otx2_mqprio_up().
> +	 */
> +	pfvf->mqprio.rate_limit = true;
> +
> +	if (netif_running(netdev)) {
> +		err = otx2_mqprio_restart_netdev(netdev, true);
> +		if (err)
> +			return err;
> +	} else if (pfvf->hw.txschq_cnt[NIX_TXSCH_LVL_SMQ]) {
> +		err = otx2_mqprio_up(pfvf);
> +		if (err)
> +			return err;
> +	}
> +
> +	otx2_mqprio_snap_free(pfvf, &pfvf->cur_mq_snap);
> +
> +	return 0;
> +}

[ ... ]

> +static void otx2_mqprio_clear_sw(struct otx2_nic *pfvf)
> +{
> +	struct net_device *netdev = pfvf->netdev;
> +
> +	pfvf->mqprio.rate_limit = false;
> +	otx2_mqprio_clear_replace_state(pfvf);
> +	netdev_set_num_tc(netdev, 0);
> +	otx2_mqprio_free_cache(pfvf);
> +}

[ ... ]

> +int otx2_mqprio_up(struct otx2_nic *pfvf)
> +{
> +	struct net_device *netdev = pfvf->netdev;
> +	int txq, err;
> +
> +	if (!pfvf->mqprio.rate_limit)
> +		return 0;
> +
> +	if (!pfvf->mqprio.min_rate || !pfvf->mqprio.max_rate)
> +		return 0;

[ ... ]

> +static int otx2_mqprio_restart_netdev(struct net_device *netdev, bool rate_limit)
> +{
> +	struct otx2_nic *pfvf = netdev_priv(netdev);
> +	const struct net_device_ops *ops = netdev->netdev_ops;
> +	bool running = netif_running(netdev);
> +	int err;
> +
> +	/* TODO: Explore live TX scheduler reprogramming to avoid a full
> +	 * ndo_stop()/ndo_open() bounce on every mqprio change.
> +	 */
> +	netdev_info(netdev,
> +		    "mqprio: restarting interface to reprogram TX scheduler; in-flight traffic will be dropped\n");
> +
> +	if (running) {
> +		clear_bit(__LINK_STATE_START, &netdev->state);
> +		smp_mb__after_atomic(); /* Commit netif_running(). */
> +	}
> +	dev_deactivate(netdev, true);
> +
> +	err = ops->ndo_stop(netdev);
> +	if (err) {
> +		if (running) {
> +			set_bit(__LINK_STATE_START, &netdev->state);
> +			dev_activate(netdev);
> +		}
> +		return err;
> +	}

[Severity: High]
Is it safe for a driver to drive dev_deactivate()/dev_activate() from
inside an ndo_setup_tc(TC_SETUP_QDISC_MQPRIO) callback?

On the teardown/replace path this callback runs from
mqprio_destroy() -> mqprio_disable_offload(), which the core invokes from
the middle of qdisc_graft():

net/sched/sch_api.c:qdisc_graft() {
	if (dev->flags & IFF_UP)
		dev_deactivate(dev, false);
	qdisc_offload_graft_root(dev, new, old, extack);
	...
		notify_and_destroy(net, skb, n, classid, old, new, extack);
		if (new && new->ops->attach)
			new->ops->attach(new);
	}
	if (dev->flags & IFF_UP)
		dev_activate(dev);
}

dev_deactivate() does not clear __LINK_STATE_START, so
otx2_teardown_tc_mqprio() sees netif_running() == true and calls
otx2_mqprio_restart_netdev(netdev, false), whose

	set_bit(__LINK_STATE_START, &netdev->state);
	dev_activate(netdev);

clears __QDISC_STATE_DEACTIVATED on the outgoing per-queue child qdiscs
and republishes them into dev_queue->qdisc, since new->ops->attach() has
not run yet. mq_attach()/mqprio_attach() then dev_graft_qdisc() and
qdisc_put() exactly those qdiscs without any further
some_qdisc_is_busy() wait, so a concurrent softirq __qdisc_run() can be
running inside a qdisc that is being reset and freed.

Reproducer would be "tc qdisc replace dev X root mq" on a running
interface carrying traffic with the offload active. The same reentrancy
also purges an unrelated live qdisc on the add path, where
dev_deactivate(netdev, true) is called from within mqprio_init().

Can the offload be restructured so the driver never touches the core's
activate/deactivate state machine from a qdisc callback?

[ ... ]

> +static int otx2_teardown_tc_mqprio(struct otx2_nic *pfvf,
> +				   struct tc_mqprio_qopt_offload *mqprio)
> +{
> +	struct tc_mqprio_qopt *qopt = &mqprio->qopt;
> +	bool had_mqprio = pfvf->mqprio.rate_limit;
> +	struct net_device *netdev = pfvf->netdev;
> +	bool if_up = netif_running(netdev);
> +	int err;
> +
> +	qopt->hw = 0;
> +
> +	/* tc qdisc replace runs setup on the new mqprio before destroying the
> +	 * old one. replace_setup_done and TC_ROOT_GRAFT distinguish stale
> +	 * old-instance teardown from graft failure after setup.
> +	 */
> +	if (pfvf->mqprio.replace_setup_done && pfvf->cur_mq_snap) {
> +		err = 0;
> +		if (pfvf->mqprio.replace_graft_done)
> +			otx2_mqprio_snap_commit(pfvf);
> +		else
> +			err = otx2_mqprio_restore_old(pfvf);
> +		otx2_mqprio_clear_replace_state(pfvf);
> +		return err;
> +	}
> +
> +	/* Skip the netdev restart when mqprio offload was not active. */
> +	if (!had_mqprio)
> +		return 0;
> +
> +	if (if_up) {
> +		int down_err, err;
> +
> +		down_err = otx2_mqprio_down(pfvf);
> +		err = otx2_mqprio_restart_netdev(netdev, false);
> +		if (err)
> +			return err;
> +		return down_err;
> +	}

[Severity: Medium]
Does replacing an offloaded mqprio with a non-offloaded configuration
wipe the TC layout of the qdisc that is already live?

For "tc qdisc replace dev X root mqprio ... hw 0" (or "... root mq"),
mqprio_init() installs the new layout without ever calling
ndo_setup_tc():

net/sched/sch_mqprio.c:mqprio_init() {
	...
	} else {
		netdev_set_num_tc(dev, qopt->num_tc);
		for (i = 0; i < qopt->num_tc; i++)
			netdev_set_tc_queue(dev, i, qopt->count[i], qopt->offset[i]);
	}
	...
}

So replace_setup_done stays false, and the later destruction of the old
offloaded instance reaches this function and takes the full-teardown
branch above, whose otx2_mqprio_clear_sw() runs
netdev_set_num_tc(netdev, 0) on the qdisc the core has just grafted.

The result is dev->num_tc == 0 while a software mqprio expects N traffic
classes, so skb_tx_hash() no longer honours the prio/TC/txq ranges and
mqprio_dump() reports 0 TCs. The netdev is also bounced in that case.

[ ... ]

> +cleanup:
> +	qopt->hw = 0;
> +	if (replacing) {
> +		int restore_err = otx2_mqprio_restore_old(pfvf);
> +
> +		otx2_mqprio_clear_replace_state(pfvf);
> +		if (restore_err) {

[ ... ]

> +		return err ? err : -EIO;
> +	}

[Severity: Medium]
Does the core immediately undo the netdev part of this rollback?

Because otx2_setup_tc_mqprio() returns an error here,
mqprio_enable_offload() propagates it before setting priv->hw_offload,
so when qdisc_create() unwinds via ops->destroy() the core takes the
non-offload branch:

net/sched/sch_mqprio.c:mqprio_destroy() {
	...
	if (priv->hw_offload && dev->netdev_ops->ndo_setup_tc)
		mqprio_disable_offload(sch);
	else
		netdev_set_num_tc(dev, 0);
	...
}

That leaves the old mqprio qdisc installed and the driver offload active
(mqprio.rate_limit true, MDQ CIR/PIR programmed from old_mq_snap) while
dev->num_tc is 0 and tc_to_txq[] is cleared, so traffic spreads over the
whole queue range and tc qdisc show reports 0 TCs for an offloaded
qdisc. This is reachable from any failure after the bounce, for example
an -ENOMEM from otx2_mqprio_alloc_cache() or a mailbox error from
otx2_nix_tm_set_queue_shaper().

[ ... ]
-- 
This is an AI-generated review.


      reply	other threads:[~2026-09-17 11:32 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-11 10:55 [PATCH v15 net-next 0/2] octeontx2-pf: " Ratheesh Kannoth
2026-09-11 10:55 ` [PATCH v15 net-next 1/2] octeontx2: use atomic bitops for PF/VF and rep flags Ratheesh Kannoth
2026-09-17 11:31   ` Paolo Abeni
2026-09-11 10:55 ` [PATCH v15 net-next 2/2] octeontx2-pf: add mqprio bandwidth offload for NIX TX schedulers Ratheesh Kannoth
2026-09-17 11:31   ` Paolo Abeni [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260917113150.171042-1-pabeni@redhat.com \
    --to=pabeni@redhat.com \
    --cc=andrew+netdev@lunn.ch \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=hawk@kernel.org \
    --cc=john.fastabend@gmail.com \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=rkannoth@marvell.com \
    --cc=sdf@fomichev.me \
    --cc=sgoutham@marvell.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®