On Sat, 7 Nov 2026 12:59:18 +0530
Jerin Jacob <[email protected]> wrote:

> > Design Overview
> > ---------------
> > `event_slb` is a distributed (`RTE_EVENT_DEV_CAP_DISTRIBUTED_SCHED`),
> > maintenance-free (`RTE_EVENT_DEV_CAP_MAINTENANCE_FREE`) software eventdev
> > PMD that supports Atomic, Ordered, Parallel, and Single-Link queues:  
> 
> @[email protected]  - drivers/event/dsw/
> @[email protected] - drivers/event/opdl/
> NO MAINTAINER for drivers/event/sw/
> 
> Hi Pravin,
> Creating new flavours. of evendev SW drivers is a maintenance burden,
> If this is close to DSW then please introduce mode in DSW driver and
> have function pointer switches for fastpath,
> Also claim should be backed by performance test values. Use
> testeventdev test cases and share the numbers to prove one way or
> another.

I ran AI review with Fable and the current AGENTS guidelines.
Short answer: it agrees with Jerin

Review: [PATCH v1 0/8] event/slb: introducing software load balancer
eventdev PMD

Applied cleanly on 26.11-rc2 (a13c105). Builds per-commit with
-Dwerror=true; bisect is clean. Line numbers below are in the
post-apply tree.

Cover / series-level
--------------------

Jerin's question (new PMD vs. a DSW mode) is the right one, and the
code supports it. The credit pool (slb_port_acquire_credits,
slb_port_return_credits, credits_on_loan, the max_inflight formula
in slb_configure, the RTE_VERIFY on pending_releases), the log
macros, and all of slb_xstats.c are DSW code with the prefix
changed; the comment at slb_evdev.h:30 is verbatim dsw_evdev.h:62.
What is new is the queue-side scheduling: atomic queues use a main
MP/SC ring plus per-port SP/SC rings filled by whichever port finds
its ring empty, under a per-queue spinlock; ordered queues use the
ring slot index as the sequence number into a per-queue reorder
buffer, drained under a second per-queue spinlock. Both are
serialised per queue, so "distributed" means "whichever core takes
the lock", and the atomic refill does an O(SLB_ATOMIC_FLOWS)+O(ring)
scan on every migration trigger. The scaling claim needs
testeventdev numbers against sw and dsw, as Jerin asked.

Patches 7 and 8 are fix-ups to patches 1-6 (patch 8 also deletes
code added by patch 6). Squash them; the bullet-list commit messages
describe the diff, not the change.

The cover letter says the self-test covers priority, backpressure,
and multi-core atomic migration. slb_selftest.c is single-threaded,
4 ports, no priorities, no backpressure, and never triggers a
migration. None of the concurrency paths below are exercised.

No release notes. Patch 6 says it adds a 26.11 entry and patch 7
says it edits it; neither touches doc/guides/rel_notes/. A new
driver after rc2 is 27.03 material anyway.

Patch 1: introduce software load balancer PMD
---------------------------------------------

Error:

- slb_evdev.c:781, slb_dump(): NULL dereference.
      uint32_t count = rte_event_ring_count(q->q_ring);
  The loop runs i < slb->num_queues, which is set by configure, but
  q_ring is NULL for any queue not yet set up. rte_event_dev_dump()
  after configure and before queue_setup segfaults. Guard with
  if (q->q_ring == NULL) continue;  (same loop at :782).

- slb_evdev.c:197, slb_queue_setup(): double free. q_ring is
  assigned before the atomic/ordered allocations:
      queue->q_ring = ring;
  and the failure paths at :212 and :239 do
      rte_event_ring_free(ring);
      return -ENOMEM;
  leaving queue->q_ring dangling. The next slb_queue_release()
  (close, or re-setup) frees it again. Set queue->q_ring = NULL on
  those paths, or assign q_ring only after all allocations succeed.

Warning:

- slb_evdev.c:167, slb_queue_setup() on an already-linked queue
  frees q_ring and any per_port_rings via slb_queue_release(), but
  the linked ports' q_entries[][].dq_ring (set at :374) and
  single_linked_ring (:319) still point at the freed rings. The
  API allows queue_setup on a stopped device without unlinking
  first, so the next dequeue is a use-after-free. Either return
  -EBUSY when serving_ports_mask != 0, or re-resolve the entries.

- slb_event.c:661, new_event_threshold is compared against
  credits_on_loan, which includes every port's idle cache:
      if (unlikely(rte_atomic_load_explicit(&slb->credits_on_loan,
                                            rte_memory_order_relaxed) >
                   source_port->new_event_threshold))
              return 0;
  This is the DSW check, but patch 7 raises SLB_MIN_CREDIT_LOAN to
  256 (DSW: 64), so a port that only dequeues and releases holds
  256..511 credits forever (slb_port_return_credits keeps
  SLB_PORT_MIN_CREDITS). With K terminating ports the floor on
  credits_on_loan is 256*K regardless of events in flight. The
  default threshold from slb_port_def_conf is min(4096,
  nb_events_limit): nb_events_limit=1024 with 4 consumer ports, or
  the default with 16 workers, rejects every NEW enqueue once the
  caches fill, and nothing ever lowers them. Either go back to the
  DSW quantum, or let the port use its own cache before applying
  the global check (acquire first, check the pool only when
  borrowing).

Info:

- slb_configure() logs its -EINVAL causes with SLB_LOG_DP_LINE(INFO,
  ...). Control path; use SLB_LOG_LINE(ERR, ...).

- drivers/event/meson.build:18: 'slb' added after 'sw'; list is
  alphabetical.

Patch 2: add support for ordered queues
---------------------------------------

Error:

- slb_event.c:993 / :1021, reorder slot reuse. The sequence number
  is the ring slot:
      *start_slot = cons_head & r->r.mask;
      h->seq_slot = (start_slot + j) & ring_mask;
  but the ring slot is freed at dequeue (__rte_ring_update_tail,
  :998) while the reorder slot is freed only when slb_reorder_drain
  passes it. The two are only in step if the producer can never
  advance r.size positions while a slot is outstanding. Credits do
  not guarantee that: RELEASE returns the credit immediately
  (slb_event.c:673, and :1139 for implicit release) even though the
  slot stays RELEASED until drained. So: worker A dequeues slot S
  and is slow; drain stops at S; other workers dequeue and release
  (or implicit-release) r.size more events, all returning credits;
  the producer wraps to slot S; worker B dequeues the new event with
  seq_slot S. B's forward writes reorder_buffer[S] and drain
  delivers it past A's event; when A then forwards, its event lands
  in a slot drain has already passed and is delivered r.size events
  late, and if A releases while B's event is still READY the state
  is overwritten and B's event is lost with its credit. For
  max_inflight=4096 r.size is 8192, i.e. one stalled worker plus a
  few ms of traffic. Fix: hold the credit for an ordered event
  until its slot is drained (return it in slb_reorder_drain when a
  RELEASED slot is reclaimed, and skip ordered releases in the
  :673/:1139 returns). Then occupied slots + ring entries <=
  max_inflight < r.size and the ring cannot wrap onto a live slot.

- slb_event.c:114, lost wakeup in slb_reorder_drain():
      if (!rte_spinlock_trylock(&queue->reorder_lock))
              return;
  The holder exits when it reads EMPTY at drain_idx (:125). A second
  port that marks that very slot READY and then fails the trylock
  returns too. The slot stays READY until some later forward or
  release on the same ordered queue happens to call drain; nothing
  in the dequeue path does. At the end of a burst the last events
  are stranded in the reorder buffer. Re-check after unlock:
      rte_spinlock_unlock(&queue->reorder_lock);
      rte_atomic_thread_fence(rte_memory_order_seq_cst);
      if (rte_atomic_load_explicit(&buf[drain_idx].state,
                                   rte_memory_order_acquire) !=
          SLB_REORDER_EMPTY)
              goto retry;   /* back to the trylock */

Warning:

- slb_event.c:987-998, slb_ordered_ring_dequeue_burst() calls
  __rte_ring_move_cons_head(), __rte_ring_dequeue_elems() and
  __rte_ring_update_tail() directly. No other driver uses the ring
  private API; it is not a stable contract. If the slot index is
  the design, add an exported helper to lib/ring (or
  rte_event_ring) that returns the consumer head, in its own patch.
  Same for slb_event.c:198, which assumes the data array follows
  struct rte_ring at (r + 1).

Patch 3: add support for atomic queues
--------------------------------------

Warning:

- slb_event.c:826, :845, :1172 write q_returned from the consuming
  port with a plain increment:
      orig_q->atomic_state->port_stats[ep].q_returned++;
  and slb_event.c:406 (patch 4) reads it from whichever core holds
  the refill lock, with a plain load, to decide the flow mapping
  can be switched. Single writer, but cross-core and load-bearing
  (the enq==ret test is what makes migration order-preserving).
  Make q_returned RTE_ATOMIC(uint64_t), store with
  rte_memory_order_release, load with rte_memory_order_acquire so
  the application's last access to the flow on the old port is
  ordered before the new port can see events for it.

Info:

- slb_event.c:166:
      return (flow_id * 2654435761U) & SLB_ATOMIC_FLOWS_MASK;
  The multiplier is odd, so the low 11 bits of the product are a
  permutation of the low 11 bits of flow_id; flow_id and
  flow_id+2048 always collide. Fibonacci hashing takes the high
  bits: (flow_id * 2654435761U) >> (32 - 11).

- slb_history_entry.flow_id is written at slb_event.c:1024 and
  never read.

Patch 4: add dynamic flow migration for atomic queues
-----------------------------------------------------

Warning:

- struct slb_port fields is_loaded (slb_evdev.h:146),
  last_migrated_time, flow_migrations, migrations_in,
  migrations_out (:164-167) are per port but written under the
  refill_lock of whichever atomic queue is migrating
  (slb_event.c:269, :327-328, :337-339, :343, :420) and read by
  every other atomic queue's refill (:249, :290, :364). Two atomic
  queues sharing ports race on them: lost increments, and both
  queues can pick the same port as migration source. Either make
  them per (queue, port) inside slb_atomic_state (port_stats is
  already there), or make them atomic. is_loaded also sits in the
  port's hot cacheline next to inflight_credits and history_head,
  so a remote refill write bounces that line under the owning
  core.

Info:

- The hold buffer is per queue, not per port. Once one linked port
  stops draining, SLB_HOLD_BUFFER_SIZE - 3*128 held events later
  slb_atomic_refill returns at :501 without touching the main ring
  and every other port on that queue starves. Worth stating in the
  Limitations section of slb.rst.

Patch 5: add extended statistics and diagnostics
------------------------------------------------

Warning:

- slb_xstats.c:105 exposes port_%u_queue_%u_dequeued from
  slb_port.queue_dequeued[], which nothing increments. Either count
  it in slb_port_dequeue() or drop the stat.

Info:

- Leftovers: slb_port.total_dequeues, enqueue_calls (printed by
  slb_dump, never incremented), credits_drained (only ever cleared),
  flow_migrations (incremented, never read); SLB_MAX_FLOWS_BITS and
  SLB_MAX_FLOWS_MASK unused.

Patch 6: add self-test and documentation
----------------------------------------

Warning:

- doc/guides/eventdevs/features/slb.ini:14 has
  runtime_port_link = Y but slb_info_get() does not set
  RTE_EVENT_DEV_CAP_RUNTIME_PORT_LINK. Patch 7 says it removed this
  from the feature matrix; it removed it from the .rst only.

- slb.rst (as of patch 7) says "link up to eight queues per port at
  each priority"; SLB_PORT_MAX_Q_MAP_PER_PRIO is SLB_MAX_QUEUES
  (64).

- Self-test does not use TEST_ASSERT / unit_test_suite_runner and
  never runs more than one lcore. At minimum add a multi-core
  ordered test with one deliberately slow worker and an atomic test
  that forces a migration (SLB_MIGRATION_LOAD_THRESHOLD is 16 full
  per-port rings); both of the Errors above would show up.

Reply via email to