Re: [PATCH net-next v4 08/14] ibmveth: Add queue-aware RX buffer submit helper for MQ
From: mingming cao <hidden>
Date: 2026-08-10 22:52:18
Also in:
netdev
On 8/6/26 11:37 AM, Jakub Kicinski wrote:
This is an AI-generated review of your patch. The human sending this email has considered the AI review valid, or at least plausible. --- ibmveth: Add queue-aware RX buffer submit helper for MQ This adds ibmveth_add_logical_lan_buffers() to choose between the legacy single/multi-buffer hcalls and the new per-queue buffer hcall, threads a queue_index argument through the RX accessors, harvest and replenish helpers, and adds a per-queue replenish_lock plus reworked H_FUNCTION handling. A few questions below about the new failure handling, the new lock and the logging changes.
Hi Jakub, Thanks for the review. This patch introduces more than helper reshaping. It establishes the replenish locking and error-handling model that later MQ RX behavior depends on.
quoted
diff --git a/drivers/net/ethernet/ibm/ibmveth.c b/drivers/net/ethernet/ibm/ibmveth.c index 9d1242dbcb19d..cb93659fc0576 100644 --- a/drivers/net/ethernet/ibm/ibmveth.c +++ b/drivers/net/ethernet/ibm/ibmveth.c[ ... ]quoted
@@ -588,11 +598,75 @@ static inline void ibmveth_flush_buffer(void *addr, unsigned long length) asm("dcbf %0,%1,1" :: "b" (addr), "r" (offset)); } +/** + * ibmveth_add_logical_lan_buffers - Add receive buffers to hypervisor + * @adapter: ibmveth adapter structure + * @descs: array of buffer descriptors to add + * @filled: number of valid descriptors in the array + * @buff_size: size of each buffer (multi-queue mode only) + * @queue_index: RX queue index + * + * Return: hypervisor return code + */ +static long ibmveth_add_logical_lan_buffers(struct ibmveth_adapter *adapter, + union ibmveth_buf_desc *descs, + int filled, + unsigned long buff_size, + int queue_index) +{ + struct vio_dev *vdev = adapter->vdev; + unsigned long rc; + + if (adapter->multi_queue) {[ ... ]quoted
+ rc = h_add_logical_lan_buffers_queue(vdev->unit_address, + handle, + buffersznum, + ioba[0], ioba[1], ioba[2], + ioba[3], ioba[4], ioba[5]); + adapter->hcall_stats.add_bufs_queue++; + } else if (filled == 1) { + rc = h_add_logical_lan_buffer(vdev->unit_address, + descs[0].desc); + adapter->hcall_stats.add_buf++; + } else { + rc = h_add_logical_lan_buffers(vdev->unit_address, + descs[0].desc, descs[1].desc, + descs[2].desc, descs[3].desc, + descs[4].desc, descs[5].desc, + descs[6].desc, descs[7].desc); + adapter->hcall_stats.add_bufs++; + }[Severity: Medium] These hcall_stats fields are plain non-atomic u64 counters on the adapter, but the only serialization this patch adds is the per-queue rx_queue[i].replenish_lock taken in ibmveth_replenish_task(). Two NAPI instances replenishing different queues hold different locks, so can these increments lose counts once MQ is enabled later in the series and num_rx_queues becomes larger than 1? The same question applies to the other adapter-wide counters reached from this path: adapter->replenish_task_cycles++; adapter->replenish_add_buff_success += filled; adapter->replenish_add_buff_failure += filled; and to ibmveth_update_rx_no_buffer(), which ibmveth_replenish_task() calls while holding queue N's lock only, although it rewrites adapter-global state for every queue: adapter->rx_no_buffer = 0; for (i = 0; i < adapter->num_rx_queues; i++) { ... adapter->rx_qstats[i].no_buffer_drops = drops; adapter->rx_no_buffer += drops; } Would rx_no_buffer and rx_qstats[].no_buffer_drops be clobbered when two queues replenish concurrently? ibmveth_close() calls the same helper with no lock held at all.
Yes, once MQ is live those adapter-global counters can lose updates across queues. I agree they need a better concurrency model, but I see that as broader accounting follow-up work rather than one of the main correctness blockers in this series. The pool-availability path is already using `atomic_t` for correctness; these adapter-global counters are diagnostic/accounting state and should either move to a per-queue model or use a safer aggregation strategy.
quoted
/* replenish the buffers for a pool. note that we don't need to * skb_reserve these since they are used for incoming... */ static void ibmveth_replenish_buffer_pool(struct ibmveth_adapter *adapter, - struct ibmveth_buff_pool *pool) + struct ibmveth_buff_pool *pool, + int queue_index) { union ibmveth_buf_desc descs[IBMVETH_MAX_RX_PER_HCALL] = {0}; u32 remaining = pool->size - atomic_read(&pool->available);@@ -678,24 +752,16 @@ static void ibmveth_replenish_buffer_pool(struct ibmveth_adapter *adapter,[ ... ]quoted
+ lpar_rc = ibmveth_add_logical_lan_buffers(adapter, descs, + filled, + pool->buff_size, + queue_index); + if (lpar_rc != H_SUCCESS) { dev_warn_ratelimited(dev, - "RX h_add_logical_lan failed: filled=%u, rc=%lu, batch=%u\n", + "RX h_add_logical_lan %s failed: filled=%u, rc=%lu, batch=%u\n", + adapter->multi_queue ? + "_queue" : "", filled, lpar_rc, batch); goto hcall_failure; }[Severity: Low] The interpolated hcall name never matches an actual symbol. In MQ mode this prints RX h_add_logical_lan _queue failed: ... with a stray space, and in legacy mode RX h_add_logical_lan failed: ... with a double space. Would a full literal per branch read better? The two new H_FUNCTION messages added below also print batch with %d, although batch is declared u32 in this function ("u32 i, filled, batch;"), while this warning uses %u for the same variable.
Yes. Full literal hcall names would read better here, and `batch` should use `%u`. I'll fold that cleanup back into the owning patch rather than leave it as a tail fixup.
quoted
@@ -736,24 +802,23 @@ static void ibmveth_replenish_buffer_pool(struct ibmveth_adapter *adapter, } adapter->replenish_add_buff_failure += filled; - /* - * If multi rx buffers hcall is no longer supported by FW - * e.g. in the case of Live Partition Migration - */ - if (batch > 1 && lpar_rc == H_FUNCTION) { - /* - * Instead of retry submit single buffer individually - * here just set the max rx buffer per hcall to 1 - * buffers will be respleshed next time - * when ibmveth_replenish_buffer_pool() is called again - * with single-buffer case - */ - netdev_info(adapter->netdev, - "RX Multi buffers not supported by FW, rc=%lu\n", - lpar_rc); - adapter->rx_buffers_per_hcall = 1; - netdev_info(adapter->netdev, - "Next rx replesh will fall back to single-buffer hcall\n"); + if (lpar_rc == H_FUNCTION) { + if (adapter->multi_queue) { + netdev_err(adapter->netdev, + "MQ buffer add H_FUNCTION (q=%d, batch=%d)\n", + queue_index, batch); + } else if (batch > 1) {[Severity: High] The multi_queue arm logs and breaks without changing any state: adapter->multi_queue stays set, rx_buffers_per_hcall is untouched, there is no fallback to h_add_logical_lan_buffer()/h_add_logical_lan_buffers(), and no schedule_work(&adapter->work). ibmveth_add_logical_lan_buffers() picks the hcall purely on adapter->multi_queue, and that field is only written in ibmveth_probe(), which is not re-run on resume (ibmveth_resume() only calls ibmveth_schedule_rx_queue()). So once the hypervisor stops implementing H_ADD_LOGICAL_LAN_BUFFERS_QUEUE - exactly the Live Partition Migration case the legacy arm below exists for - can this queue ever post another RX buffer again? pool->available stays at 0, PHYP drops every inbound frame, and the interface still reports the link up, so recovery would need an administrative down/up. Every other unrecoverable condition in this driver schedules the reset work (ibmveth_remove_buffer_from_pool(), ibmveth_rxq_get_buffer(), and the new enable_irq failure path in ibmveth_poll()). Should this arm do the same, or fall back to the legacy hcalls the way the legacy arm downgrades batch? Separately, netdev_err() here is not rate limited, while the warning for the same failure a few lines above uses dev_warn_ratelimited(), so a dried-out pool can emit several console lines per poll cycle per pool. This arm appears unchanged through the end of the series.
Yes. As written, the MQ `H_FUNCTION` path can strand the queue in a permanent dry-pool state. I'm planning to add real recovery here rather than paper over it by shrinking the batch size, because that still leaves the driver calling the dead queue-buffer hcall. The safer direction is to treat this as an unexpected MQ `H_FUNCTION` event, mark fallback, and recover through the normal reset path rather than trying to half-switch modes on the live datapath. I'll also make that path rate-limited. With the clarified project assumption that HMC blocks LPM into LPARs without MQ-capable veth, I do not think this should be framed as a supported in-place MQ-to-SQ downgrade path.
quoted
+ /* + * Live Partition Migration may drop multi- + * buffer support. Fall back to single-buffer + * on the next replenish; do not continue with + * a stale local batch size (infinite loop). + */ + netdev_warn(adapter->netdev, + "Legacy batch add H_FUNCTION (batch=%d), fallback\n", + batch); + adapter->rx_buffers_per_hcall = 1; + } } break; }@@ -775,21 +840,33 @@ static void ibmveth_update_rx_no_buffer(struct ibmveth_adapter *adapter) } /* replenish routine */ -static void ibmveth_replenish_task(struct ibmveth_adapter *adapter) +static void ibmveth_replenish_task(struct ibmveth_adapter *adapter, + int queue_index) { + struct ibmveth_rx_q *rxq = &adapter->rx_queue[queue_index]; + unsigned long flags; int i; + if (queue_index >= adapter->num_rx_queues) + return; + adapter->replenish_task_cycles++; + spin_lock_irqsave(&rxq->replenish_lock, flags); + for (i = (IBMVETH_NUM_BUFF_POOLS - 1); i >= 0; i--) { - struct ibmveth_buff_pool *pool = &adapter->rx_buff_pool[0][i]; + struct ibmveth_buff_pool *pool = + &adapter->rx_buff_pool[queue_index][i]; if (pool->active && (atomic_read(&pool->available) < pool->threshold)) - ibmveth_replenish_buffer_pool(adapter, pool); + ibmveth_replenish_buffer_pool(adapter, pool, + queue_index); } ibmveth_update_rx_no_buffer(adapter); + + spin_unlock_irqrestore(&rxq->replenish_lock, flags); }[Severity: High] Can this lock deadlock against itself when netconsole runs over this ibmveth device? ibmveth_replenish_buffer_pool() printk()s from inside the locked region: netdev_info() on an invalid map index, dev_warn_ratelimited() on hcall failure, and the two new H_FUNCTION messages. With netconsole on this interface a printk from there re-enters the driver: netconsole write_msg() netpoll_send_udp() netpoll_send_skb() __netpoll_send_skb() netpoll_poll_dev() ops->ndo_poll_controller() -> ibmveth_poll_controller() ibmveth_replenish_task(adapter, i) __netpoll_send_skb() calls netpoll_poll_dev() whenever the xmit does not go through on the first try, and netpoll_poll_dev() only declines when down_trylock(&ni->dev_lock) fails, when !netif_running(dev), or when netif_local_xmit_active(dev): if (!ni || down_trylock(&ni->dev_lock)) return; if (!netif_running(dev) || netif_local_xmit_active(dev)) { up(&ni->dev_lock); return; } ... if (ops->ndo_poll_controller) ops->ndo_poll_controller(dev); None of those covers the replenish path, so ibmveth_poll_controller() loops ibmveth_replenish_task() over every queue and re-takes the same replenish_lock on the same CPU, with interrupts already disabled. Before this patch ibmveth_poll_controller() also called ibmveth_replenish_task(), but with no lock the recursion was harmless. This code appears unchanged through the end of the series.
Yes. That deadlock looks plausible with netconsole on this device. I'm planning to move the logging and recovery actions out from under `replenish_lock`, because that is the real issue here. The helper should return outcome/state while the actual logging and reset scheduling happen after unlock, so netconsole cannot recurse back into the same queue's replenish path while the lock is held.
[Severity: Medium]
Does this need to be spin_lock_irqsave()? The lock now brackets the whole
multi-pool loop, so every netdev_alloc_skb(), every dma_map_single() and
every H_ADD_LOGICAL_LAN_BUFFER[S][_QUEUE] hypervisor call for all five
pools runs with local interrupts hard-disabled.
On a cold fill from ibmveth_open() or the first NAPI poll that is roughly
768 allocations and DMA maps and around 96 hcalls per queue at the default
MTU (pool_count[] = { 256, 512, 256, 256, 256 }), and more when the large
pools are active.
None of the protected state is written from hardirq context, and the commit
message says the lock exists only "so later concurrent NAPI/resize paths
can serialize buffer posting", so would spin_lock_bh(), dropping and
re-acquiring per batch, or preparing buffers outside the critical section
be enough?Fair point. That `spin_lock_irqsave()` critical section is heavier than I would like, but I still see it as a secondary issue compared to the main correctness problems in this patch. I do not want to rewrite that lock protocol in this series while the higher-priority fixes here are the MQ `H_FUNCTION` recovery, no logging under `replenish_lock`, and making the producer/consumer locking model complete.
quoted
/* empty and free ana buffer pool - also used to do cleanup in error paths */[ ... ]quoted
@@ -1004,50 +1083,55 @@ static int ibmveth_remove_buffer_from_pool(struct ibmveth_adapter *adapter,[ ... ]quoted
- free_index = adapter->rx_buff_pool[0][pool].producer_index; - adapter->rx_buff_pool[0][pool].producer_index++; - if (adapter->rx_buff_pool[0][pool].producer_index >= - adapter->rx_buff_pool[0][pool].size) - adapter->rx_buff_pool[0][pool].producer_index = 0; - adapter->rx_buff_pool[0][pool].free_map[free_index] = index; + free_index = adapter->rx_buff_pool[queue_index][pool].producer_index; + adapter->rx_buff_pool[queue_index][pool].producer_index++; + if (adapter->rx_buff_pool[queue_index][pool].producer_index >= + adapter->rx_buff_pool[queue_index][pool].size) + adapter->rx_buff_pool[queue_index][pool].producer_index = 0; + adapter->rx_buff_pool[queue_index][pool].free_map[free_index] = index; mb(); - atomic_dec(&adapter->rx_buff_pool[0][pool].available); + atomic_dec(&adapter->rx_buff_pool[queue_index][pool].available);[Severity: High] This isn't a bug introduced by this patch, but the new replenish_lock only covers the producer half of this bookkeeping, so it is worth asking whether the consumer half is still unprotected. ibmveth_remove_buffer_from_pool() mutates free_map[], producer_index, skbuff[], dma_addr[] and available for the same pool that ibmveth_replenish_buffer_pool() mutates under replenish_lock, but takes no lock: producer: ibmveth_poll_controller() ibmveth_replenish_task() /* holds replenish_lock */ ibmveth_replenish_buffer_pool() consumer: ibmveth_poll() /* holds nothing */ ibmveth_rxq_harvest_buffer() ibmveth_remove_buffer_from_pool() netpoll_poll_dev() serializes netpoll users against each other via ni->dev_lock, and __napi_poll()'s netpoll_poll_lock() only stops netpoll from re-polling a NAPI instance that is already running; neither prevents ndo_poll_controller from running on one CPU while the same queue's NAPI poll runs on another. Can free_map[]/producer_index/consumer_index then hand the same pool index out twice, so a buffer is posted to the hypervisor while its skb has already gone up the stack, or lose a free index permanently? Since the future work described in the commit message ("so later concurrent NAPI/resize paths can serialize buffer posting") builds on this lock, should the consumer side take it as well? [ ... ]
Yes. That race looks real, and the consumer side should participate in the same per-queue locking protocol. I'm planning to serialize harvest-side mutation with the same lock so the producer/consumer model is actually complete. The important point here is that the race is not "two NAPIs on one queue"; it is harvest on one CPU versus netpoll-driven replenish on the same queue.
quoted
@@ -1993,34 +2076,45 @@ static void ibmveth_rx_csum_helper(struct sk_buff *skb, static int ibmveth_poll(struct napi_struct *napi, int budget) { - struct ibmveth_adapter *adapter = - container_of(napi, struct ibmveth_adapter, napi[0]); - struct net_device *netdev = adapter->netdev; + struct net_device *netdev = napi->dev; + struct ibmveth_adapter *adapter = netdev_priv(netdev); int frames_processed = 0; unsigned long lpar_rc; + int queue_index, rc; u16 mss = 0; + queue_index = napi - adapter->napi; + + if (WARN_ON(queue_index < 0 || queue_index >= adapter->num_rx_queues)) + return 0; +[ ... ]quoted
@@ -2097,14 +2197,19 @@ static int ibmveth_poll(struct napi_struct *napi, int budget) /* We think we are done - reenable interrupts, * then check once more to make sure we are done. */ - lpar_rc = ibmveth_enable_irq(adapter, 0); - if (WARN_ON(lpar_rc != H_SUCCESS)) { + lpar_rc = ibmveth_enable_irq(adapter, queue_index); + if (lpar_rc != H_SUCCESS) { + netdev_err(netdev, + "Failed to enable IRQ for queue %d (rc=0x%lx), scheduling reset\n", + queue_index, lpar_rc); schedule_work(&adapter->work); goto out; } - if (ibmveth_rxq_pending_buffer(adapter) && napi_schedule(napi)) { - lpar_rc = ibmveth_disable_irq(adapter, 0); + if (ibmveth_rxq_pending_buffer(adapter, queue_index) && + napi_schedule(napi)) { + lpar_rc = ibmveth_disable_irq(adapter, queue_index); + WARN_ON(lpar_rc != H_SUCCESS); goto restart_poll; }[Severity: Low] Two diagnostic changes here are not described in the commit message, which only says callers were updated "so arity stays consistent": the WARN_ON() around the ibmveth_enable_irq() failure is replaced by an unconditional netdev_err(), which drops the taint and backtrace that test harnesses key on, and a new WARN_ON() is added on the ibmveth_disable_irq() result. Would it help to mention or split out that change? Also, the new guard added at the top of ibmveth_poll() if (WARN_ON(queue_index < 0 || queue_index >= adapter->num_rx_queues)) return 0; returns less than budget from a NAPI poll callback without calling napi_complete_done(), which leaves the instance scheduled with the hypervisor IRQ still masked. The guard is unreachable at this commit, since IBMVETH_MAX_RX_QUEUES is 1 and queue_index is always 0, but the same early return is still there at the end of the series. Should it complete NAPI before returning? [ ... ]
Yes. The bad-queue-index guard should complete NAPI before returning, otherwise that defensive path can leave the instance scheduled with the IRQ still masked. The diagnostic reshaping around the enable/disable IRQ paths belongs with the owning poll work rather than being overclaimed here. Thanks, Mingming
quoted
@@ -2229,9 +2334,13 @@ static int ibmveth_change_mtu(struct net_device *dev, int new_mtu) static void ibmveth_poll_controller(struct net_device *dev) { struct ibmveth_adapter *adapter = netdev_priv(dev); + int i; - ibmveth_replenish_task(adapter); - ibmveth_schedule_rx_queue(adapter, 0); + for (i = 0; i < adapter->num_rx_queues; i++) + ibmveth_replenish_task(adapter, i); + + for (i = 0; i < adapter->num_rx_queues; i++) + ibmveth_schedule_rx_queue(adapter, i); } #endif[ ... ]