rte_eth_tx_burst() descriptor/mbuf management guarantees vs. free thresholds

Viewed 792

The rte_eth_tx_burst() function is documented as:

 * It is the responsibility of the rte_eth_tx_burst() function to
 * transparently free the memory buffers of packets previously sent.
 * This feature is driven by the *tx_free_thresh* value supplied to the
 * rte_eth_dev_configure() function at device configuration time.
 * When the number of free TX descriptors drops below this threshold, the
 * rte_eth_tx_burst() function must [attempt to] free the *rte_mbuf*  buffers
 * of those packets whose transmission was effectively completed.

I have a small test program where this doesn't seem to hold true (when using the ixgbe driver on a vfio X553 1GbE NIC).

So my program sets up one transmit queue like this:

uint16_t tx_ring_size = 1024-32;
rte_eth_dev_configure(port_id, 0, 1, &port_conf);
r = rte_eth_dev_adjust_nb_rx_tx_desc(port_id, &rx_ring_size, &tx_ring_size);
struct rte_eth_txconf txconf = dev_info.default_txconf;
r = rte_eth_tx_queue_setup(port_id, 0, tx_ring_size,
        rte_eth_dev_socket_id(port_id), &txconf);

The transmit mbuf packet pool is created like this:

struct rte_mempool *pkt_pool = rte_pktmbuf_pool_create("pkt_pool", 1023, 341, 0,
        RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());

In that way, when sending packets I rather run out of TX descriptors before I run out of packet buffers. (the program generates packets with just one segment)

My expectation is that when I call rte_eth_tx_burst() in a loop (to send one packet after another) that it never fails since it transparently frees mbufs of already sent packets.

However, this doesn't happen.

I basically have a transmit loop like this:

for (unsigned i = 0; i < 2048; ++i) {
    struct rte_mbuf *pkt = rte_pktmbuf_alloc(args.pkt_pool);
    // error check, prepare packet etc.

    uint16_t l = rte_eth_tx_burst(args.port_id, 0, &pkt, 1);
    // error check etc.
}

After 1086 transmitted packets (of ~ 300 bytes each), rte_eth_tx_burst() returns 0.

I use the default threshold values, i.e. the queried values are (from dev_info.default_txconf):

tx thresh   : 32
tx rs thresh: 32
wthresh     : 0

So the main question now is: How hard is rte_eth_tx_burst() supposed to try to free mbuf buffers (and thus descriptors)?

I mean, it could busy loop until the transmission of previously supplied mbufs is completed.

Or it could just quickly check if some descriptors are free again. But if not, just give up.

Related question: Are the default threshold values appropriate for this use case?


So I work around this like that:

for (;;) {
    uint16_t l = rte_eth_tx_burst(args.port_id, 0, &pkt, 1);
    if (l == 1) {
        break;
    } else {
        RTE_LOG(ERR, USER1, "cannot send packet\n");
        int r = rte_eth_tx_done_cleanup(args.port_id, 0, 256);
        if (r < 0) {
             rte_panic("%u. cannot cleanup tx descs: %s\n", i, rte_strerror(-r));
        }
        RTE_LOG(WARNING, USER1, "%u. cleaned up %d descriptors ...\n", i, r);
    }
}

With that I get output like this:

USER1: cannot send packet
USER1: 1086. cleaned up 32 descriptors ...
USER1: cannot send packet
USER1: 1118. cleaned up 32 descriptors ...
USER1: cannot send packet
USER1: 1150. cleaned up 0 descriptors ...
USER1: cannot send packet
USER1: 1182. cleaned up 0 descriptors ...
[..]

USER1: cannot send packet
USER1: 1950. cleaned up 32 descriptors ...
USER1: cannot send packet
USER1: 1982. cleaned up 0 descriptors ...
USER1: cannot send packet
USER1: 2014. cleaned up 0 descriptors ...
USER1: cannot send packet
USER1: 2014. cleaned up 32 descriptors ...
USER1: cannot send packet
USER1: 2046. cleaned up 32 descriptors ...

Meaning that it frees at most 32 descriptors like this. And that it doesn't always succeed, but then the next rte_eth_tx_burst() succeeds freeing some.

Side question: Is there a better more dpdk-idiomatic way to handle the recycling of mbufs?


When I change the code such that I run out of mbufs before I run out of transmit descriptors (i.e. tx ring created with 1024 descriptors, mbuf pool still has 1023 elements), I have to change the alloc part like this:

struct rte_mbuf *pkt;
do {
    pkt = rte_pktmbuf_alloc(args.pkt_pool);
    if (!pkt) {
        r = rte_eth_tx_done_cleanup(args.port_id, 0, 256);
        if (r < 0) {
             rte_panic("%u. cannot cleanup tx descs: %s\n", i, rte_strerror(-r));
        }
        RTE_LOG(WARNING, USER1, "%u. cleaned up %d descriptors ...\n", i, r);
    }
} while (!pkt);

The output is similar, e.g.:

USER1: 1023. cleaned up 95 descriptors ...
USER1: 1118. cleaned up 32 descriptors ...
USER1: 1150. cleaned up 32 descriptors ...
USER1: 1182. cleaned up 32 descriptors ...
USER1: 1214. cleaned up 0 descriptors ...
USER1: 1214. cleaned up 0 descriptors ...
USER1: 1214. cleaned up 32 descriptors ...
[..]

That means the freeing of descriptors/mbufs is so 'slow' that it has to busy loop up to 3 times.

Again, is this a valid approach, or are there better dpdk ways to solve this?


Since rte_eth_tx_done_cleanup() might return -ENOTSUP, this may point to the direction that my usage of it might not be the best solution.

Incidentally, even with the ixgbe driver it fails for me when I disable checksum offloads!

Apparently, ixgbe_dev_tx_done_cleanup() then invokes ixgbe_tx_done_cleanup_vec() instead of ixgbe_tx_done_cleanup_full() which unconditionally returns -ENOTSUP:

static int
ixgbe_tx_done_cleanup_vec(struct ixgbe_tx_queue *txq __rte_unused,
                        uint32_t free_cnt __rte_unused)
{
        return -ENOTSUP;
}

Does this make sense?

So then perhaps the better strategy is then to make sure that there are less descriptors than pool elements (e.g. 1024-32 < 1023) and just re-call rte_eth_tx_burst() until it returns one?

That means like this:

for (;;) {
    uint16_t l = rte_eth_tx_burst(args.port_id, 0, &pkt, 1);
    if (l == 1) {
        break;
    } else {
        RTE_LOG(ERR, USER1, "%u. cannot send packet - retry\n", i);
    }
}

This works, and the output shows again that the descriptors are freed 32 at a time, e.g.:

USER1: 1951. cannot send packet - retry
USER1: 1951. cannot send packet - retry
USER1: 1983. cannot send packet - retry
USER1: 1983. cannot send packet - retry
USER1: 2015. cannot send packet - retry
USER1: 2015. cannot send packet - retry
USER1: 2047. cannot send packet - retry
USER1: 2047. cannot send packet - retry

I know that I also can use rte_eth_tx_burst() to submit bigger bursts. But I want to get the simple/edge cases right and understand the dpdk semantics, first.

I'm on Fedora 33 and DPDK 20.11.2.

3 Answers

Recommendation/Solution: after analyzing the cause of the issue is indeed with TX descriptor with either rte_mempool_list_dump or dpdk-procinfo, please use rte_eth_tx_buffer_flush or change the settings for TX thresholds.

Explanation:

The behaviour mbuf_free is varied across PMD, and within the same NIC PF and VF also varies. Follow are some points to understand this propely

  1. rte_mempool can be created with or without cache elements.
  2. when created with cached elements, depending upon the available lcores (eal_options) and number of cache elements per core parameter, the configured mbufs are added per core cache.
  3. When HW offload DEV_TX_OFFLOAD_MBUF_FAST_FREE is available and enabled, the agreement is the mbuf will have ref_cnt as 1.
  4. So when ever tx_burst (success or failure is invoked) threshold levels are checked if free mbuf/mbuf-segments can be pushed back to pool.
  5. With DEV_TX_OFFLOAD_MBUF_FAST_FREE enabled the driver blindly puts the elements into lcore cache.
  6. while in case of no DEV_TX_OFFLOAD_MBUF_FAST_FREE, generic approach of validating the MBUF ensuring the nb_segments and ref_cnt are checked, then pushed to mempool.

But always the either fixed (32 I believe is the default set for all PMD) or available free mbuf is pushed to cache or pool always.

Facts:

  1. In the case of the IXGBE VF driver the option DEV_TX_OFFLOAD_MBUF_FAST_FREE is not available. Which means each time whenever thresholds are met, each individual mbuf are checked and pushed to the mempool.
  2. as per the code snippet rte_eth_dev_configure is configured only for TX, and rte_pktmbuf_pool_create is created to have 341 elements as cache.
  3. Assumption has to be made, that there is only 1 Lcore based (which runs the loop of alloc and tx).

Code Snippet-1:

for (unsigned i = 0; i < 2048; ++i) {
    struct rte_mbuf *pkt = rte_pktmbuf_alloc(args.pkt_pool);
    // error check, prepare packet etc.

    uint16_t l = rte_eth_tx_burst(args.port_id, 0, &pkt, 1);
    // error check etc.
}

After 1086 transmitted packets (of ~ 300 bytes each), rte_eth_tx_burst() returns 0.

[Observation] If indeed the mbuf were running, the rte_pktmbuf_alloc should be failing before rte_eth_tx_burst. But failing at 1086, creates an interesting phenomenon because total mbuf created is 1023, and failure happens are 2 iteration of 32 mbuf_release to mempool. Analyzing the driver code for ixgbe, it can be found that (only place return as 0) in tx_xmit_pkts is

        /* Only use descriptors that are available */
        nb_pkts = (uint16_t)RTE_MIN(txq->nb_tx_free, nb_pkts);
        if (unlikely(nb_pkts == 0))
                return 0;

Even though in config tx_ring_size is set to 992, internally rte_eth_dev_adjust_nb_desc sets to max of *nb_desc, desc_lim->nb_min. Based on the code it is not because there are no free mbuf, but it due to TX descriptor is low or not availble.

while in all other cases, whenever rte_eth_tx_done_cleanup or rte_eth_tx_buffer_flush these actually pushes any pending descriptors to be DMA immediately out of SW PMD. This internally frees up more descriptors which makes the tx_burst much smoother.

To identify the root cause, whenever DPDK API tx_burst return either

  1. invoke rte_mempool_list_dump or
  2. make use of mempool dump via dpdk-procinfo

Note: most PMD operates on amortizing the cost of the descriptor (PCIe payload) write by batching and bunching for at least 4 (in case of SSE). Hence a single packet even if DPDK tx_burst returning 1 will not be pushing the packet out of NIC. Hence to ensure use rte_eth_tx_buffer_flush.

Say, you invoke rte_eth_tx_burst() to send one small packet (single mbuf, no offloads). Suppose, the driver indeed pushes the packet to the HW. Doing so eats up one descriptor in the ring: the driver "remembers" that this packet mbuf is associated with that descriptor. But the packet is not sent instantly. The HW typically has some means to notify the driver of completions. Just imagine: if the driver checked for completions on every rte_eth_tx_burst() invocation (thus ignoring any thresholds), then calling rte_eth_tx_burst() one more time in a tight loop manner for another packet would likely consume one more descriptor rather than recycle the first one. So, given this fact, I'd not use tight loop when investigating tx_free_thresh semantics. And it shouldn't matter whether you invoke rte_eth_tx_burst() once per a packet or once per a batch of them.

Now. Say, you have a Tx ring of size N. Suppose, tx_free_thresh is M. And you have a mempool of size Z. What you do is allocate a burst of N - M - 1 small packets and invoke rte_eth_tx_burst() to send this burst (no offloads; each packet is assumed to eat up one Tx descriptor). Then you wait for some wittingly sufficient (for completions) amount of time and check the number of free objects in the mempool. This figure should read Z - (N - M - 1). Then you allocate and send one extra packet. Then wait again. This time, the number of spare objects in the mempool should read Z - (N - M). Finally, you allocate and send one more packet (again!) thus crossing the threshold (the number of spare Tx descriptors becomes less than M). During this invocation of rte_eth_tx_burst(), the driver should detect crossing the threshold and start checking for completions. This should make the driver free (N - M) descriptors (consumed by two previous rte_eth_tx_burst() invocations) thus clearing up the whole ring. Then the driver proceeds to push the new packet in question to the HW thus spending one descriptor. You then check the mempool: this should report Z - 1 free objects.

So, the short of it: no loop, just three rte_eth_tx_burst() invocations with sufficient waiting time between them. And you check the spare object count in the mempool after each send operation. Theoretically, this way, you'll be able to understand the corner case semantics. That's the gist of it. However, please keep in mind that the actual behaviour may vary across different vendors / PMDs.

Relying on rte_eth_tx_done_cleanup() really isn't an option since many PMDs don't implement it. Mostly Intel PMD's provide it, but e.g. SFC, MLX* and af_packet ones don't.

However, it's still unclear why the ixgbe PMD doesn't support cleanup when no offloads are enabled.

The requirements on rte_eth_tx_burst() with respect to freeing are really light - from the API docs:

 * It is the responsibility of the rte_eth_tx_burst() function to
 * transparently free the memory buffers of packets previously sent.
 * This feature is driven by the *tx_free_thresh* value supplied to the
 * rte_eth_dev_configure() function at device configuration time.
 * When the number of free TX descriptors drops below this threshold, the
 * rte_eth_tx_burst() function must [attempt to] free the *rte_mbuf*  buffers
 * of those packets whose transmission was effectively completed.
[..]
 * @return
 *   The number of output packets actually stored in transmit descriptors of
 *   the transmit ring. The return value can be less than the value of the
 *   *tx_pkts* parameter when the transmit ring is full or has been filled up.

So just attempting to free (but not waiting on the results of that attempt) and returning 0 (since 0 is less than tx_pkts) is covered by that 'contract'.

FWIW, no example distributed with dpdk loops around rte_eth_tx_burst() to re-submit not-yet-sent packages. There are some examples that use rte_eth_tx_burst() and discard unsent packages, though.

AFAICS, besides rte_eth_tx_done_cleanup() and rte_eth_tx_burst() there is no other function for requesting the release of mbufs previously submitted for transmission.

Thus, it's advisable to size the mbuf packet pool larger than the configured ring size in order to survive situations where all mbufs are inflight and can't be recovered because there is no mbuf left for calling rte_eth_tx_burst() again.

Related