migration/rdma: producer/consumer send path
cover migration/rdma: producer/consumer send path
This is a rebase with no code changes; the prerequisites (Yanfei Xu's two RDMA series) were merged in migration-20260925-pull-request.
v1: https://lore.kernel.org/qemu-devel/[email protected]/
This series splits the RDMA live-migration send path into two threads:
- a producer thread (the migration thread) that scans dirty pages and records which chunks must be migrated, pushing chunk descriptors (block + chunk index + dirty byte span) onto a lock-free SPSC ring; and
- a consumer thread that owns the RDMA queue pair: it drains the ring, posts the RDMA writes and control messages, handles chunk registration and barriers, and polls the completion queue.
Chunk-granular descriptors on the ring replace the previous per-page merge logic on the migration thread.
Patches 1-12 are the producer/consumer split. Patch 13 (Jack Wang) negotiates the batched chunk registration capability so a peer that lacks it falls back to one chunk per request.
Results
-------
64 GiB guest, single-path RDMA, memload workload (one-time RAM fill, fixed dirty set). "before" is the single-thread send path with only the nb_sent-stats patch (patch 1) applied; "after" is the full producer/consumer series, so the delta isolates the split itself.
pin_all=off:
- total migration time: -16.2% (24 GiB fill), -14.2% (60 GiB fill)
- transfer-phase time: -16.3% (24 GiB fill), -14.2% (60 GiB fill) - RAM throughput: +17.2% (24 GiB fill), +16.6% (60 GiB fill) - guest downtime: -55.3% (24 GiB fill), -23.1% (60 GiB fill)
pin_all=on (whole-RAM registration at setup is unchanged and dominates total time, so the split shows mainly in the transfer phase):
- total migration time: -5.0%
- transfer-phase time: -10.9% - transfer speed: +12.9% - guest downtime: -1.5%
QP saturation was measured with the new nb_sent send-queue counter:
send-queue "drain to zero" events (queue starved empty) fell by ~99% in the transfer-bound pin_all=off cases (24 GiB: 26609 -> 276; 60 GiB:
63771 -> 641), while "full-queue" hits rose.
This is the first step toward RDMA multipath migration.
Dmitrii Omelchenko (12):
migration/rdma: Add nb_sent send-queue-depth stats migration/rdma: Add producer/consumer send-ring types migration/rdma: Add send-ring lifecycle (no consumer thread yet) migration/rdma: Add the send consumer thread (drain only) migration/rdma: Handle write/flush/reg-stop on the consumer migration/rdma: Extract qemu_rdma_write_file() and dispatch FILE migration/rdma: Switch the producer onto the send ring migration/rdma: Add producer/consumer tuning instrumentation migration/rdma: drain the send ring in batches migration/rdma: size reg_resp.len by the register request count migration/rdma: batch chunk registration in one round-trip migration/rdma: trace batching and register runs
Jack Wang (1):
migration/rdma: negotiate batched chunk registration
migration/rdma.c | 1275 +++++++++++++++++++++++++++++----------- migration/trace-events | 18 + 2 files changed, 956 insertions(+), 337 deletions(-)
base-commit: 71f25c75417d31c214ba2cd26b273c114fc99987
Signature · 1 line
2.39.5
RFC PATCH 1/13 migration/rdma: Add nb_sent send-queue-depth stats
Track whole-migration counters for the RDMA send queue: peak nb_sent (max outstanding writes), the number of posts, how many posts hit a full send queue, and how many times the queue drained to zero. Reset at RAM_CONTROL_SETUP and dumped at RAM_CONTROL_FINISH; migration-thread only, no locking.
--- migration/rdma.c | 60 ++++++++++++++++++++++++++++++++++++++++++ migration/trace-events | 1 + 2 files changed, 61 insertions(+)
migration/rdma.c +60 -0
@@ -302,6 +302,12 @@ typedef struct RDMAContext {/* number of outstanding writes */ int nb_sent; +/* nb_sent statistics */ +uint64_t nbsent_max;/* peak outstanding writes */ +uint64_t nbsent_posts;/* posts (nb_sent++) */ +uint64_t nbsent_full_hits;/* posts that hit a full SQ */ +uint64_t nbsent_zero_hits;/* drains to zero */ +/* store info about current buffer so that we can merge it with future sends */ uint64_t current_addr;
@@ -1164,6 +1170,11 @@ static uint64_t qemu_rdma_make_wrid(uint64_t wr_id, uint64_t index,return result; } +/* nb_sent statistics counters */ +static void rdma_nbsent_inc (RDMAContext *rdma,uint64_t nb_sent); +static void rdma_nbsent_full (RDMAContext *rdma); +static void rdma_nbsent_zero (RDMAContext *rdma); +/* * Consult the connection manager to see a work request * (of any kind) has completed.
@@ -1213,6 +1224,9 @@ static int qemu_rdma_poll(RDMAContext *rdma, struct ibv_cq *cq,if (rdma->nb_sent >0 ) { rdma->nb_sent--; +if (rdma->nb_sent ==0 ) { +rdma_nbsent_zero (rdma); + } } }else {trace_rdma_poll_other (wr_id, rdma->nb_sent);
@@ -1438,6 +1452,7 @@ static bool qemu_rdma_post_send(RDMAContext *rdma, ret =ibv_post_send (rdma->qp, send_wr, &bad_wr);if (ret ==ENOMEM && rdma->nb_sent) {trace_rdma_post_send_queue_full (send_wr->wr_id, rdma->nb_sent); +rdma_nbsent_full (rdma); ret =qemu_rdma_block_for_wrid (rdma,RDMA_WRID_RDMA_WRITE ,NULL );if (ret <0 ) { error_setg(errp, "rdma migration: failed to make room for %s",
@@ -1953,6 +1968,7 @@ static int qemu_rdma_write_flush(RDMAContext *rdma, Error **errp)if (ret ==0 ) { rdma->nb_sent++;trace_rdma_write_flush (rdma->nb_sent); +rdma_nbsent_inc (rdma, rdma->nb_sent); } rdma->current_length =0 ;
@@ -2998,6 +3014,41 @@ err:return -1 ; } +static void rdma_nbsent_inc (RDMAContext *rdma,uint64_t nb_sent) +{ + rdma->nbsent_posts++; +if (nb_sent > rdma->nbsent_max) { + rdma->nbsent_max = nb_sent; + } +} + +static void rdma_nbsent_full (RDMAContext *rdma) +{ + rdma->nbsent_full_hits++; +} + +static void rdma_nbsent_zero (RDMAContext *rdma) +{ + rdma->nbsent_zero_hits++; +} + +static void rdma_stats_reset (RDMAContext *rdma) +{ + rdma->nbsent_max =0 ; + rdma->nbsent_posts =0 ; + rdma->nbsent_full_hits =0 ; + rdma->nbsent_zero_hits =0 ; +} + +static void rdma_nbsent_stats_dump (RDMAContext *rdma) +{ +if (rdma->nbsent_posts ==0 ) { +return ; + } +trace_rdma_nbsent_stats (rdma->nbsent_max, rdma->nbsent_posts, + rdma->nbsent_full_hits, rdma->nbsent_zero_hits); +} +int rdma_control_save_page (QEMUFile *f,ram_addr_t block_offset,ram_addr_t offset,size_t size) {
@@ -3547,6 +3598,10 @@ int rdma_registration_start(QEMUFile *f, uint64_t flags)return -1 ; } +if (flags ==RAM_CONTROL_SETUP ) { +rdma_stats_reset (rdma); + } +trace_rdma_registration_start (flags);qemu_put_be64 (f,RAM_SAVE_FLAG_HOOK );return qemu_fflush (f);
@@ -3581,6 +3636,11 @@ int rdma_registration_stop(QEMUFile *f, uint64_t flags) }qemu_fflush (f); + +if (flags ==RAM_CONTROL_FINISH ) { +rdma_nbsent_stats_dump (rdma); + } + ret =qemu_rdma_write_flush (rdma, &err);if (ret <0 ) {
migration/trace-events +1 -0
@@ -249,6 +249,7 @@ rdma_resolve_host_trying(const char *host, const char *ip) "Trying %s => %s" rdma_write_flush(int sent) "sent total: %d" rdma_write_one_post(uint64_t chunk, long addr, long remote, uint32_t len) "Posting chunk: %" PRIu64 ", addr: 0x%lx remote: 0x%lx, bytes %" PRIu32 rdma_post_send_queue_full(uint64_t wr_id, int sent) "send queue full, wr_id=%" PRIu64 ", outstanding writes=%d" +rdma_nbsent_stats(uint64_t max, uint64_t posts, uint64_t full_hits, uint64_t zero_hits) "nb_sent max %" PRIu64 " posts %" PRIu64 " full-queue hits %" PRIu64 " drains to zero %" PRIu64 rdma_write_one_recvregres(int mykey, int theirkey, uint64_t chunk) "Received registration result: my key: 0x%x their key 0x%x, chunk %" PRIu64 rdma_write_one_sendreg(uint64_t chunk, int len, int index, int64_t offset) "Sending registration request chunk %" PRIu64 " for %d bytes, index: %d, offset: %" PRId64 rdma_write_one_top(uint64_t chunks, uint64_t size) "Writing %" PRIu64 " chunks, (%" PRIu64 " MB)"
Signature · 1 line
2.39.5
RFC PATCH 2/13 migration/rdma: Add producer/consumer send-ring types
In reply to Dmitrii OmelchenkoDefine the SPSC send-ring descriptor (RDMACmd), the ring itself (RDMASendRing) and the producer/consumer fields in RDMAContext. No functional change.
--- migration/rdma.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+)
migration/rdma.c +82 -0
@@ -29,6 +29,7 @@#include "qemu/main-loop.h" #include "qemu/module.h" #include "qemu/rcu.h" +#include "qemu/thread.h" #include "qemu/sockets.h" #include "qemu/coroutine.h" #include "system/memory.h"
@@ -278,6 +279,33 @@ typedef struct RDMALocalBlocks {RDMALocalBlock *block; } RDMALocalBlocks; +/* +* Producer/consumer split (source side): the migration thread (producer) +* scans dirty pages and enqueues chunk-write and barrier commands into an +* SPSC ring; a dedicated consumer thread owns the QP/CQ and does all RDMA. +*/ +typedef enum { +RDMA_CMD_WRITE ,/* write one dirty span: block/chunk + offset/length */ +RDMA_CMD_FLUSH ,/* drain nb_sent -> 0 (round boundary) */ +RDMA_CMD_REG_STOP ,/* send REGISTER_FINISHED; flags = RAM_CONTROL_* */ +RDMA_CMD_FILE ,/* QEMUFile control bytes: iov/niov */ +RDMA_CMD_FINISH ,/* drain then stop the consumer */ +}RDMACmdType ; + +typedef struct RDMACmd { +RDMACmdType type; +uint32_t block_index; +uint32_t chunk_index; +uint64_t offset;/* WRITE: dirty span start, block-relative */ +uint32_t length;/* WRITE: dirty span length (within a chunk) */ +uint64_t flags;/* REG_STOP */ +const struct iovec *iov;/* FILE (producer blocks -> stays valid) */ +size_t niov;/* FILE */ +unsigned seq;/* barrier completion sequence (0 = none) */ +}RDMACmd ; + +typedef struct RDMASendRing RDMASendRing ; +/* * Main data structure for RDMA state. * While there is only one copy of this structure being allocated right now,
@@ -317,6 +345,17 @@ typedef struct RDMAContext {/* index of the chunk in the current ram block */ int current_chunk; +/* producer/consumer split (source side) */ +RDMASendRing *send_ring;/* SPSC ring; allocated at consumer start */ +QemuThread consumer_thread; +bool consumer_started;/* producer-only: route via ring once set */ + +/* producer-only: pending contiguous WRITE span, not yet enqueued */ +int pend_block;/* block index, -1 = none */ +uint32_t pend_chunk;/* chunk index of the span */ +uint64_t pend_offset;/* span start, block-relative bytes */ +uint64_t pend_length;/* span length in bytes (within one chunk) */ +bool pin_all;/*
@@ -2671,6 +2710,49 @@ static NotifierWithReturn rdma_ram_round_notifier = { .notify = rdma_ram_round_notify, }; +/* +* --------------------------------------------------------------------------- +* Producer/consumer send ring (source side). +* +* Lock-free SPSC ring: the producer (migration thread) owns head, the +* consumer (RDMA thread) owns tail. A mutex+condvars are used only to +* sleep/wake at the empty/full/abort/barrier edges (guarded by the *_waiting +* flags so the steady state stays lock-free). Barriers ride the ring as +* ordered descriptors and publish completion via barrier_done_seq. +* +* head/tail are free-running unsigned counters, never reset. Wrap is +* implicit: occupancy is the modular difference head - tail (always in +* [0, N] since the producer stops at full), and the slot index is +* counter & MASK. Because N is a power of two and 2^32 is an exact multiple +* of N, masking stays continuous across the 2^32 counter wrap. Barrier seqs +* use the same modular scheme with signed distance; seq 0 is reserved as the +* "no barrier" sentinel and skipped on wrap. +* --------------------------------------------------------------------------- +*/ +#define RDMA_SEND_RING_SIZE 2048/* power of two; > SQ depth (512) */ +#define RDMA_SEND_RING_MASK (RDMA_SEND_RING_SIZE - 1) + +struct RDMASendRing { +RDMACmd ring[RDMA_SEND_RING_SIZE ]; +unsigned head;/* producer owns */ +unsigned tail;/* consumer owns */ +unsigned barrier_done_seq;/* consumer owns; producer reads */ +unsigned barrier_next_seq;/* producer owns */ +bool abort;/* sticky */ +QemuMutex wait_lock; +QemuCond not_full;/* producer sleeps */ +QemuCond not_empty;/* consumer sleeps */ +QemuCond barrier_cv;/* producer sleeps for ack */ +bool producer_waiting; +bool consumer_waiting; + +/* statistics, dumped at FINISH */ +uint64_t pushwait_max_ns;/* longest producer wait on a full ring */ +uint64_t pushwait_total_ns;/* total producer wait on a full ring */ +uint64_t occupancy_max;/* peak queued commands */ +uint64_t idle_max_ns;/* longest consumer wait on an empty ring */ +}; +static int qio_channel_rdma_set_blocking (QIOChannel *ioc,bool blocking,Error **errp)
Signature · 1 line
2.39.5
RFC PATCH 3/13 migration/rdma: Add send-ring lifecycle (no consumer thread yet)
In reply to Dmitrii OmelchenkoIntroduce the producer side of the SPSC send ring: rdma_consumer_start allocates the ring at registration setup, rdma_consumer_stop frees it at cleanup, plus ring_push/ring_wake_consumer. No thread runs yet and the existing send path is unchanged.
--- migration/rdma.c | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+)
migration/rdma.c +96 -0
@@ -1214,6 +1214,9 @@ static void rdma_nbsent_inc(RDMAContext *rdma, uint64_t nb_sent);static void rdma_nbsent_full (RDMAContext *rdma);static void rdma_nbsent_zero (RDMAContext *rdma); +static void rdma_consumer_start (RDMAContext *rdma); +static void rdma_consumer_stop (RDMAContext *rdma); +/* * Consult the connection manager to see a work request * (of any kind) has completed.
@@ -2108,6 +2111,9 @@ static void qemu_rdma_cleanup(RDMAContext *rdma) {Error *err =NULL ; +/* Free the send ring before touching the QP. */ +rdma_consumer_stop (rdma); +if (rdma->cm_id && rdma->connected) { if ((rdma->errored || migrate_get_current()->state == MIGRATION_STATUS_CANCELLING) &&
@@ -2753,6 +2759,91 @@ struct RDMASendRing {uint64_t idle_max_ns;/* longest consumer wait on an empty ring */ }; +static bool ring_full (RDMASendRing *r) +{ +return (r->head -qatomic_load_acquire (&r->tail)) >=RDMA_SEND_RING_SIZE ; +} + +/* Producer -> wake the consumer if it is idle on an empty ring. */ +static void ring_wake_consumer (RDMASendRing *r) +{ +smp_mb ();/* order the *_waiting load below vs the sleeper's store */ +if (qatomic_read (&r->consumer_waiting)) { +qemu_mutex_lock (&r->wait_lock); +qemu_cond_signal (&r->not_empty); +qemu_mutex_unlock (&r->wait_lock); + } +} + +static int ring_push (RDMAContext *rdma,const RDMACmd *cmd) +{ +RDMASendRing *r = rdma->send_ring; + +assert (r); + +if (ring_full (r)) { +qemu_mutex_lock (&r->wait_lock); + r->producer_waiting = true; +smp_mb ();/* pair with ring_wake_producer() */ +while (ring_full (r) && !r->abort) { +qemu_cond_wait (&r->not_full, &r->wait_lock); + } + r->producer_waiting = false; +qemu_mutex_unlock (&r->wait_lock); + } +if (qatomic_read (&r->abort)) { +return -1 ; + } + r->ring[r->head &RDMA_SEND_RING_MASK ] = *cmd; +qatomic_store_release (&r->head, r->head +1 );/* publish payload+slot */ +ring_wake_consumer (r); +return 0 ; +} + +/* +* Producer: allocate the send ring once (idempotent). The consumer +* thread that drains it is created in a later change. +*/ +static void rdma_consumer_start (RDMAContext *rdma) +{ +RDMASendRing *r; + +if (rdma->consumer_started) { +return ; + } + r =g_new0 (RDMASendRing,1 ); +qemu_mutex_init (&r->wait_lock); +qemu_cond_init (&r->not_full); +qemu_cond_init (&r->not_empty); +qemu_cond_init (&r->barrier_cv); + rdma->send_ring = r; + rdma->pend_block =-1 ;/* no pending WRITE span yet */ +/* Producer-only flag (migration thread + cleanup, never concurrent). */ + rdma->consumer_started = true; +} + +/* Producer: tear down the send ring. */ +static void rdma_consumer_stop (RDMAContext *rdma) +{ +RDMASendRing *r = rdma->send_ring; +RDMACmd cmd = { .type =RDMA_CMD_FINISH }; + +if (!rdma->consumer_started) { +return ; + } +/* Ask the consumer to drain and exit; if it already aborted, just join. */ +if (!qatomic_read (&r->abort)) { +ring_push (rdma, &cmd); + } +qemu_mutex_destroy (&r->wait_lock); +qemu_cond_destroy (&r->not_full); +qemu_cond_destroy (&r->not_empty); +qemu_cond_destroy (&r->barrier_cv); +g_free (r); + rdma->send_ring =NULL ; + rdma->consumer_started = false; +} +static int qio_channel_rdma_set_blocking (QIOChannel *ioc,bool blocking,Error **errp)
@@ -3810,6 +3901,11 @@ int rdma_registration_stop(QEMUFile *f, uint64_t flags) goto err; } +if (flags ==RAM_CONTROL_SETUP ) { +/* Setup complete: allocate the send ring (consumer added later). */ +rdma_consumer_start (rdma); + } +return 0 ; err: rdma->errored = true;
Signature · 1 line
2.39.5
RFC PATCH 4/13 migration/rdma: Add the send consumer thread (drain only)
In reply to Dmitrii OmelchenkoCreate and join the consumer thread in rdma_consumer_start/stop and add the ring operations it needs to drain and shut down: ring_pop, ring_advance_tail, ring_idle_wait and ring_set_abort. rdma_consumer_dispatch handles only RDMA_CMD_FINISH (drain the CQ and exit). The old send path is still in charge, so the ring carries only the FINISH pushed at teardown.
--- migration/rdma.c | 120 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 6 deletions(-)
migration/rdma.c +114 -6
@@ -2111,7 +2111,7 @@ static void qemu_rdma_cleanup(RDMAContext *rdma) {Error *err =NULL ; -/* Free the send ring before touching the QP. */ +/* Stop and join the consumer before touching the QP it owns. */ rdma_consumer_stop (rdma);if (rdma->cm_id && rdma->connected) {
@@ -2764,6 +2764,37 @@ static bool ring_full(RDMASendRing *r)return (r->head -qatomic_load_acquire (&r->tail)) >=RDMA_SEND_RING_SIZE ; } +static bool ring_empty (RDMASendRing *r) +{ +return (qatomic_load_acquire (&r->head) - r->tail) ==0 ; +} + +static void ring_set_abort (RDMAContext *rdma) +{ +RDMASendRing *r = rdma->send_ring; + +if (!r) { +return ; + } +qemu_mutex_lock (&r->wait_lock); +qatomic_set (&r->abort, true); +qemu_cond_broadcast (&r->not_full); +qemu_cond_broadcast (&r->not_empty); +qemu_cond_broadcast (&r->barrier_cv); +qemu_mutex_unlock (&r->wait_lock); +} + +static void ring_wake_producer (RDMASendRing *r) +{ +smp_mb ();/* order the *_waiting load below vs the sleeper's store */ +if (qatomic_read (&r->producer_waiting)) { +qemu_mutex_lock (&r->wait_lock); +qemu_cond_broadcast (&r->not_full); +qemu_cond_broadcast (&r->barrier_cv); +qemu_mutex_unlock (&r->wait_lock); + } +} +/* Producer -> wake the consumer if it is idle on an empty ring. */ static void ring_wake_consumer (RDMASendRing *r) {
@@ -2800,10 +2831,84 @@ static int ring_push(RDMAContext *rdma, const RDMACmd *cmd)return 0 ; } -/* -* Producer: allocate the send ring once (idempotent). The consumer -* thread that drains it is created in a later change. -*/ +static bool ring_pop (RDMASendRing *r,RDMACmd *cmd) +{ +if (ring_empty (r)) { +return false; + } + *cmd = r->ring[r->tail &RDMA_SEND_RING_MASK ];/* after load_acquire */ +return true; +} + +static void ring_advance_tail (RDMASendRing *r) +{ +qatomic_store_release (&r->tail, r->tail +1 ); +ring_wake_producer (r); +} + +/* Consumer: sleep until the ring is non-empty or aborted. */ +static void ring_idle_wait (RDMASendRing *r) +{ +qemu_mutex_lock (&r->wait_lock); + r->consumer_waiting = true; +smp_mb ();/* pair with ring_wake_consumer() */ +while (ring_empty (r) && !r->abort) { +qemu_cond_wait (&r->not_empty, &r->wait_lock); + } + r->consumer_waiting = false; +qemu_mutex_unlock (&r->wait_lock); +} + +static int rdma_consumer_dispatch (RDMAContext *rdma,const RDMACmd *cmd, +bool *stop) +{ +Error *err =NULL ; +int ret =0 ; + +switch (cmd->type) { +case RDMA_CMD_FINISH : + ret =qemu_rdma_drain_cq (rdma, &err); + *stop = true; +break ; +default : +break ; + } +if (ret <0 && err) { +error_report_err (err); + } +return ret; +} + +static void *rdma_consumer_thread (void *opaque) +{ +RDMAContext *rdma = opaque; +RDMASendRing *r = rdma->send_ring; +bool stop = false; + +rcu_register_thread (); + +while (!qatomic_read (&r->abort) && !stop) { +RDMACmd cmd; + +if (ring_pop (r, &cmd)) { +if (rdma_consumer_dispatch (rdma, &cmd, &stop) <0 ) { +ring_set_abort (rdma); +break ; + } +ring_advance_tail (r); +continue ; + } +ring_idle_wait (r); + } + +if (qatomic_read (&r->abort)) { +/* make the failure visible to the producer's rdma_errored() checks */ + rdma->errored = true; + } +rcu_unregister_thread (); +return NULL ; +} +static void rdma_consumer_start (RDMAContext *rdma) {RDMASendRing *r;
@@ -2820,9 +2925,10 @@ static void rdma_consumer_start(RDMAContext *rdma) rdma->pend_block =-1 ;/* no pending WRITE span yet */ /* Producer-only flag (migration thread + cleanup, never concurrent). */ rdma->consumer_started = true; +qemu_thread_create (&rdma->consumer_thread,"rdma-send" , + rdma_consumer_thread, rdma,QEMU_THREAD_JOINABLE ); } -/* Producer: tear down the send ring. */ static void rdma_consumer_stop (RDMAContext *rdma) {RDMASendRing *r = rdma->send_ring;
@@ -2835,6 +2941,8 @@ static void rdma_consumer_stop(RDMAContext *rdma)if (!qatomic_read (&r->abort)) {ring_push (rdma, &cmd); } +qemu_thread_join (&rdma->consumer_thread); +qemu_mutex_destroy (&r->wait_lock);qemu_cond_destroy (&r->not_full);qemu_cond_destroy (&r->not_empty);
Signature · 1 line
2.39.5
Re: [RFC PATCH RESEND 04/13] migration/rdma: Add the send consumer thread (drain only)
Quoted 21 lines · Dmitrii Omelchenko
migration/rdma.c +21
-0+ +static void *rdma_consumer_thread (void *opaque) +{ +RDMAContext *rdma = opaque; +RDMASendRing *r = rdma->send_ring; +bool stop = false; + +rcu_register_thread (); + +while (!qatomic_read (&r->abort) && !stop) { +RDMACmd cmd; + +if (ring_pop (r, &cmd)) { +if (rdma_consumer_dispatch (rdma, &cmd, &stop) <0 ) { +ring_set_abort (rdma); +break ; + } +ring_advance_tail (r); +continue ; + } +ring_idle_wait (r);
migration/rdma.c static int ring_push(RDMAContext *rdma, const RDMACmd *cmd) · line 2911
Quoted 10 lines
migration/rdma.c +10
-0+ } + +if (qatomic_read (&r->abort)) { +/* make the failure visible to the producer's rdma_errored() checks */ + rdma->errored = true; + } +rcu_unregister_thread (); +return NULL ; +} +
The consumer thread writes rdma->errored here:
if (qatomic_read(&r->abort)) {
rdma->errored = true;
}However, rdma->errored is read by the producer/migration thread through rdma_errored() and directly in the control-channel path. Since this is a plain non-atomic store/read pair, the new consumer thread introduces a data race.
The atomic access to r->abort does not make the subsequent rdma->errored store safe, since the store happens after the abort handling and is not protected by wait_lock. In particular, the producer may race with this store while scanning dirty pages or waiting for a barrier.
Please either make rdma->errored atomic and use atomic accesses consistently, or use the existing atomic ring abort state as the sole cross-thread error indicator and have the producer-side error checks consult that state.
With this synchronization issue fixed, the consumer-thread error path will be much easier to reason about.
RFC PATCH 5/13 migration/rdma: Handle write/flush/reg-stop on the consumer
In reply to Dmitrii OmelchenkoExtend rdma_consumer_dispatch to run the real send work off the ring:RDMA_CMD_WRITE posts one chunk via qemu_rdma_write_chunk, RDMA_CMD_FLUSH drains the CQ, and RDMA_CMD_REG_STOP sends REGISTER_FINISHED (draining and dumping stats first on FINISH). The thread acks barrier commands via ring_barrier_complete. The producer still drives the old inline path, so the ring still carries only FINISH.
--- migration/rdma.c | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+)
migration/rdma.c +65 -0
@@ -1216,6 +1216,7 @@ static void rdma_nbsent_zero(RDMAContext *rdma);static void rdma_consumer_start (RDMAContext *rdma);static void rdma_consumer_stop (RDMAContext *rdma); +static void rdma_nbsent_stats_dump (RDMAContext *rdma);/* * Consult the connection manager to see a work request
@@ -1986,6 +1987,33 @@ static int qemu_rdma_write_one(RDMAContext *rdma,return 0 ; } +/* Consumer: write one whole chunk */ +static int qemu_rdma_write_chunk (RDMAContext *rdma,uint32_t block_index, +uint32_t chunk_index,Error **errp) +{ +RDMALocalBlock *block = &rdma->local_ram_blocks.block[block_index]; +uint64_t chunk_off = (uint64_t )chunk_index *migrate_rdma_chunk_size (); +uint64_t len = block->length - chunk_off; +int ret; + +if (len >migrate_rdma_chunk_size ()) { + len =migrate_rdma_chunk_size (); + } + +/* write_one wants current_addr in ram_addr_t space (block->offset based) */ + ret =qemu_rdma_write_one (rdma, block_index, block->offset + chunk_off, + len, errp); +if (ret <0 ) { +return -1 ; + } +if (ret ==0 ) { + rdma->nb_sent++; +trace_rdma_write_flush (rdma->nb_sent); +rdma_nbsent_inc (rdma, rdma->nb_sent); + } +return 0 ; +} +/* * Push out any unwritten RDMA operations. *
@@ -2846,6 +2874,13 @@ static void ring_advance_tail(RDMASendRing *r)ring_wake_producer (r); } +/* Consumer: publish barrier completion. */ +static void ring_barrier_complete (RDMASendRing *r,unsigned seq) +{ +qatomic_store_release (&r->barrier_done_seq, seq); +ring_wake_producer (r); +} +/* Consumer: sleep until the ring is non-empty or aborted. */ static void ring_idle_wait (RDMASendRing *r) {
@@ -2859,6 +2894,23 @@ static void ring_idle_wait(RDMASendRing *r)qemu_mutex_unlock (&r->wait_lock); } +/* Consumer: send REGISTER_FINISHED; for FINISH drain and dump stats first. */ +static int rdma_consumer_reg_stop (RDMAContext *rdma,uint64_t flags, +Error **errp) +{ +RDMAControlHeader head = { .len =0 , + .type =RDMA_CONTROL_REGISTER_FINISHED , + .repeat =1 }; + +if (flags ==RAM_CONTROL_FINISH ) { +if (qemu_rdma_drain_cq (rdma, errp) <0 ) { +return -1 ; + } +rdma_nbsent_stats_dump (rdma); + } +return qemu_rdma_exchange_send (rdma, &head,NULL ,NULL ,NULL ,NULL , errp); +} +static int rdma_consumer_dispatch (RDMAContext *rdma,const RDMACmd *cmd,bool *stop) {
@@ -2866,6 +2918,16 @@ static int rdma_consumer_dispatch(RDMAContext *rdma, const RDMACmd *cmd,int ret =0 ;switch (cmd->type) { +case RDMA_CMD_WRITE : + ret =qemu_rdma_write_chunk (rdma, cmd->block_index, + cmd->chunk_index, &err); +break ; +case RDMA_CMD_FLUSH : + ret =qemu_rdma_drain_cq (rdma, &err); +break ; +case RDMA_CMD_REG_STOP : + ret =rdma_consumer_reg_stop (rdma, cmd->flags, &err); +break ;case RDMA_CMD_FINISH : ret =qemu_rdma_drain_cq (rdma, &err); *stop = true;
@@ -2895,6 +2957,9 @@ static void *rdma_consumer_thread(void *opaque)ring_set_abort (rdma);break ; } +if (cmd.seq) { +ring_barrier_complete (r, cmd.seq); + }ring_advance_tail (r);continue ; }
Signature · 1 line
2.39.5
RFC PATCH 6/13 migration/rdma: Extract qemu_rdma_write_file() and dispatch FILE
In reply to Dmitrii OmelchenkoMove the per-iov QEMUFile control-byte send loop out of qio_channel_rdma_writev() into a reusable qemu_rdma_write_file() helper, and route the consumer's RDMA_CMD_FILE command through the same helper. No functional change to writev, which still sends inline.
--- migration/rdma.c | 62 ++++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 26 deletions(-)
migration/rdma.c +36 -26
@@ -2523,6 +2523,34 @@ static RDMAContext *qemu_rdma_data_init(InetSocketAddress *saddr, Error **errp)* SEND messages for control only. * VM's ram is handled with regular RDMA messages. */ +static ssize_t qemu_rdma_write_file (RDMAContext *rdma,const struct iovec *iov, +size_t niov,Error **errp) +{ +ssize_t done =0 ; + +for (size_t i =0 ; i < niov; i++) { +size_t remaining = iov[i].iov_len; +uint8_t *data = iov[i].iov_base; + +while (remaining) { +RDMAControlHeader head = {}; +size_t len =MIN (remaining,RDMA_SEND_INCREMENT ); + + remaining -= len; + head.len = len; + head.type =RDMA_CONTROL_QEMU_FILE ; + +if (qemu_rdma_exchange_send (rdma, &head, data, +NULL ,NULL ,NULL , errp) <0 ) { +return -1 ; + } + data += len; + done += len; + } + } +return done; +} + static ssize_t qio_channel_rdma_writev(QIOChannel *ioc, const struct iovec *iov, size_t niov,
@@ -2535,7 +2563,6 @@ static ssize_t qio_channel_rdma_writev(QIOChannel *ioc,RDMAContext *rdma;int ret;ssize_t done =0 ; -size_t len;RCU_READ_LOCK_GUARD (); rdma =qatomic_rcu_read (&rioc->rdmaout);
@@ -2561,29 +2588,10 @@ static ssize_t qio_channel_rdma_writev(QIOChannel *ioc,return -1 ; } -for (int i =0 ; i < niov; i++) { -size_t remaining = iov[i].iov_len; -uint8_t * data = (void *)iov[i].iov_base; -while (remaining) { -RDMAControlHeader head = {}; - - len =MIN (remaining,RDMA_SEND_INCREMENT ); - remaining -= len; - - head.len = len; - head.type =RDMA_CONTROL_QEMU_FILE ; - - ret =qemu_rdma_exchange_send (rdma, &head, - data,NULL ,NULL ,NULL , errp); - -if (ret <0 ) { - rdma->errored = true; -return -1 ; - } - - data += len; - done += len; - } + done =qemu_rdma_write_file (rdma, iov, niov, errp); +if (done <0 ) { + rdma->errored = true; +return -1 ; }return done;
@@ -2928,12 +2936,14 @@ static int rdma_consumer_dispatch(RDMAContext *rdma, const RDMACmd *cmd,case RDMA_CMD_REG_STOP : ret =rdma_consumer_reg_stop (rdma, cmd->flags, &err);break ; +case RDMA_CMD_FILE : + ret =qemu_rdma_write_file (rdma, cmd->iov, cmd->niov, &err) <0 ? +-1 :0 ; +break ;case RDMA_CMD_FINISH : ret =qemu_rdma_drain_cq (rdma, &err); *stop = true;break ; -default : -break ; }if (ret <0 && err) {error_report_err (err);
Signature · 1 line
2.39.5
RFC PATCH 7/13 migration/rdma: Switch the producer onto the send ring
In reply to Dmitrii OmelchenkoMove the migration thread off the QP: qemu_rdma_save_page enqueues chunk writes (ring_push_write), qio_channel_rdma_writev and the round/finish notifiers post FILE/FLUSH/REG_STOP barriers, and the consumer reaps completions. Drop the per-page merge machinery (qemu_rdma_write / buffer_mergeable / write_flush / current_*) now that the consumer owns the transfer, and make errors set the sticky ring abort so both sides unwind.
--- migration/rdma.c | 417 +++++++++++++++++++++-------------------------- 1 file changed, 189 insertions(+), 228 deletions(-)
migration/rdma.c +189 -228
@@ -327,7 +327,7 @@ typedef struct RDMAContext {*/ int control_ready_expected; -/* number of outstanding writes */ +/* number of outstanding writes (consumer thread only) */ int nb_sent;/* nb_sent statistics */
@@ -336,15 +336,6 @@ typedef struct RDMAContext {uint64_t nbsent_full_hits;/* posts that hit a full SQ */ uint64_t nbsent_zero_hits;/* drains to zero */ -/* store info about current buffer so that we can -merge it with future sends */ -uint64_t current_addr; -uint64_t current_length; -/* index of ram block the current buffer belongs to */ -int current_index; -/* index of the chunk in the current ram block */ -int current_chunk; -/* producer/consumer split (source side) */ RDMASendRing *send_ring;/* SPSC ring; allocated at consumer start */ QemuThread consumer_thread;
@@ -1216,6 +1207,12 @@ static void rdma_nbsent_zero(RDMAContext *rdma);static void rdma_consumer_start (RDMAContext *rdma);static void rdma_consumer_stop (RDMAContext *rdma); +static int ring_push_write (RDMAContext *rdma,uint32_t bidx,uint32_t cidx, +uint64_t offset,uint64_t length); +static int producer_flush_pending (RDMAContext *rdma); +static int ring_barrier (RDMAContext *rdma,RDMACmdType type,uint64_t flags, +const struct iovec *iov,size_t niov); +static void ring_set_abort (RDMAContext *rdma);static void rdma_nbsent_stats_dump (RDMAContext *rdma);/*
@@ -1987,151 +1984,24 @@ static int qemu_rdma_write_one(RDMAContext *rdma,return 0 ; } -/* Consumer: write one whole chunk */ +/* Consumer: write one dirty span (offset/length are block-relative bytes) */ static int qemu_rdma_write_chunk (RDMAContext *rdma,uint32_t block_index, -uint32_t chunk_index,Error **errp) +uint64_t offset,uint64_t length,Error **errp) {RDMALocalBlock *block = &rdma->local_ram_blocks.block[block_index]; -uint64_t chunk_off = (uint64_t )chunk_index *migrate_rdma_chunk_size (); -uint64_t len = block->length - chunk_off;int ret; -if (len >migrate_rdma_chunk_size ()) { - len =migrate_rdma_chunk_size (); - } -/* write_one wants current_addr in ram_addr_t space (block->offset based) */ - ret =qemu_rdma_write_one (rdma, block_index, block->offset + chunk_off, - len, errp); -if (ret <0 ) { -return -1 ; - } -if (ret ==0 ) { - rdma->nb_sent++; -trace_rdma_write_flush (rdma->nb_sent); -rdma_nbsent_inc (rdma, rdma->nb_sent); - } -return 0 ; -} - -/* -* Push out any unwritten RDMA operations. -* -* We support sending out multiple chunks at the same time. -* Not all of them need to get signaled in the completion queue. -*/ -static int qemu_rdma_write_flush (RDMAContext *rdma,Error **errp) -{ -int ret; - -if (!rdma->current_length) { -return 0 ; - } - - ret =qemu_rdma_write_one (rdma, rdma->current_index, rdma->current_addr, - rdma->current_length, errp); - + ret =qemu_rdma_write_one (rdma, block_index, block->offset + offset, + length, errp);if (ret <0 ) {return -1 ; } -if (ret ==0 ) { rdma->nb_sent++;trace_rdma_write_flush (rdma->nb_sent);rdma_nbsent_inc (rdma, rdma->nb_sent); } - - rdma->current_length =0 ; - rdma->current_addr =0 ; - -return 0 ; -} - -static inline bool qemu_rdma_buffer_mergeable (RDMAContext *rdma, -uint64_t offset,uint64_t len) -{ -RDMALocalBlock *block; -uint8_t *host_addr; -uint8_t *chunk_end; - -if (rdma->current_index <0 ) { -return false; - } - -if (rdma->current_chunk <0 ) { -return false; - } - - block = &(rdma->local_ram_blocks.block[rdma->current_index]); - host_addr = block->local_host_addr + (offset - block->offset); - chunk_end =ram_chunk_end (block, rdma->current_chunk); - -if (rdma->current_length ==0 ) { -return false; - } - -/* -* Only merge into chunk sequentially. -*/ -if (offset != (rdma->current_addr + rdma->current_length)) { -return false; - } - -if (offset < block->offset) { -return false; - } - -if ((offset + len) > (block->offset + block->length)) { -return false; - } - -if ((host_addr + len) > chunk_end) { -return false; - } - -return true; -} - -/* -* We're not actually writing here, but doing three things: -* -* 1. Identify the chunk the buffer belongs to. -* 2. If the chunk is full or the buffer doesn't belong to the current -* chunk, then start a new chunk and flush() the old chunk. -* 3. To keep the hardware busy, we also group chunks into batches -* and only require that a batch gets acknowledged in the completion -* queue instead of each individual chunk. -*/ -static int qemu_rdma_write (RDMAContext *rdma, -uint64_t block_offset,uint64_t offset, -uint64_t len,Error **errp) -{ -uint64_t current_addr = block_offset + offset; -uint64_t index = rdma->current_index; -uint64_t chunk = rdma->current_chunk; - -/* If we cannot merge it, we flush the current buffer first. */ -if (!qemu_rdma_buffer_mergeable (rdma, current_addr, len)) { -if (qemu_rdma_write_flush (rdma, errp) <0 ) { -return -1 ; - } - rdma->current_length =0 ; - rdma->current_addr = current_addr; - -qemu_rdma_search_ram_block (rdma, block_offset, - offset, len, &index, &chunk); - rdma->current_index = index; - rdma->current_chunk = chunk; - } - -/* merge it */ - rdma->current_length += len; - -/* flush it if buffer is too large */ -if (rdma->current_length >=rdma_merge_max ()) { -return qemu_rdma_write_flush (rdma, errp); - } -return 0 ; }
@@ -2510,8 +2380,7 @@ static RDMAContext *qemu_rdma_data_init(InetSocketAddress *saddr, Error **errp)RDMAContext *rdma =NULL ; rdma =g_new0 (RDMAContext,1 ); - rdma->current_index =-1 ; - rdma->current_chunk =-1 ; + rdma->pend_block =-1 ;/* no pending WRITE span yet */ rdma->host =g_strdup (saddr->host); rdma->port =atoi (saddr->port);
@@ -2561,7 +2430,6 @@ static ssize_t qio_channel_rdma_writev(QIOChannel *ioc, {QIOChannelRDMA *rioc =QIO_CHANNEL_RDMA (ioc);RDMAContext *rdma; -int ret;ssize_t done =0 ;RCU_READ_LOCK_GUARD ();
@@ -2578,22 +2446,28 @@ static ssize_t qio_channel_rdma_writev(QIOChannel *ioc,return -1 ; } +if (!rdma->consumer_started) { +/* Setup phase: no consumer yet, no queued RAM writes -> send inline. */ + done =qemu_rdma_write_file (rdma, iov, niov, errp); +if (done <0 ) { + rdma->errored = true; +return -1 ; + } +return done; + } +/* -* Push out any writes that -* we're queued up for VM's ram. +* Consumer owns the QP. Route through the ring so this control message +* is ordered after all chunk writes queued so far; block until it has +* been sent (iov stays valid for the duration). */ - ret =qemu_rdma_write_flush (rdma, errp); -if (ret <0 ) { - rdma->errored = true; +if (ring_barrier (rdma,RDMA_CMD_FILE ,0 , iov, niov) <0 ) { +error_setg (errp,"RDMA consumer aborted during control write" );return -1 ; } - - done =qemu_rdma_write_file (rdma, iov, niov, errp); -if (done <0 ) { - rdma->errored = true; -return -1 ; +for (size_t i =0 ; i < niov; i++) { + done += iov[i].iov_len; } -return done; }
@@ -2705,10 +2579,6 @@ static ssize_t qio_channel_rdma_readv(QIOChannel *ioc,/* Block until all outstanding writes have been delivered by the hardware. */ static int qemu_rdma_drain_cq (RDMAContext *rdma,Error **errp) { -if (qemu_rdma_write_flush (rdma, errp) <0 ) { -return -1 ; - } -while (rdma->nb_sent) {if (qemu_rdma_block_for_wrid (rdma,RDMA_WRID_RDMA_WRITE ,NULL ) <0 ) {error_setg (errp,"rdma migration: completion polling error" );
@@ -2745,7 +2615,16 @@ static int rdma_ram_round_notify(NotifierWithReturn *n G_GNUC_UNUSED,return -1 ; } -return qemu_rdma_drain_cq (rdma, errp); +/* Complete all chunks in the ring before the next round. */ +if (ring_barrier (rdma,RDMA_CMD_FLUSH ,0 ,NULL ,0 ) <0 ) { +error_setg (errp,"RDMA consumer aborted during round drain" ); +return -1 ; + } +/* +* The FLUSH barrier already flushed any pending span; start the next +* round with a clean accumulator (no cross-round merging). +*/ +return 0 ; }static NotifierWithReturn rdma_ram_round_notifier = {
@@ -2846,6 +2725,7 @@ static int ring_push(RDMAContext *rdma, const RDMACmd *cmd) {RDMASendRing *r = rdma->send_ring; +/* SETUP starts the consumer before any WRITE/FLUSH/REG_STOP/FILE. */ assert (r);if (ring_full (r)) {
@@ -2867,6 +2747,87 @@ static int ring_push(RDMAContext *rdma, const RDMACmd *cmd)return 0 ; } +static int ring_push_write (RDMAContext *rdma,uint32_t bidx,uint32_t cidx, +uint64_t offset,uint64_t length) +{ +RDMACmd cmd = { + .type =RDMA_CMD_WRITE , + .block_index = bidx, + .chunk_index = cidx, + .offset = offset, + .length = length, + }; +return ring_push (rdma, &cmd); +} + +/* Producer: enqueue the accumulated contiguous span (if any) as one WRITE. */ +static int producer_flush_pending (RDMAContext *rdma) +{ +int ret; + +if (rdma->pend_block <0 ) { +return 0 ; + } + ret =ring_push_write (rdma, rdma->pend_block, rdma->pend_chunk, + rdma->pend_offset, rdma->pend_length); + rdma->pend_block =-1 ; +return ret; +} + +/* Producer: block until the consumer has completed barrier seq (or abort). */ +static int ring_wait_barrier (RDMASendRing *r,unsigned seq) +{ +if ((int )(qatomic_load_acquire (&r->barrier_done_seq) - seq) <0 ) { +qemu_mutex_lock (&r->wait_lock); + r->producer_waiting = true; +smp_mb ();/* pair with ring_wake_producer() */ +while ((int )(qatomic_load_acquire (&r->barrier_done_seq) - seq) <0 && + !r->abort) { +qemu_cond_wait (&r->barrier_cv, &r->wait_lock); + } + r->producer_waiting = false; +qemu_mutex_unlock (&r->wait_lock); + } +/* A completed barrier is success even after a late abort. */ +return (int )(qatomic_load_acquire (&r->barrier_done_seq) - seq) >=0 ? +0 :-1 ; +} + +/* Producer: enqueue a barrier command and wait for its completion. */ +static int ring_barrier (RDMAContext *rdma,RDMACmdType type,uint64_t flags, +const struct iovec *iov,size_t niov) +{ +RDMASendRing *r = rdma->send_ring; +unsigned seq; +RDMACmd cmd; + +assert (r);/* consumer started at SETUP; see ring_push() */ + +/* Order any accumulated writes ahead of this barrier. */ +if (producer_flush_pending (rdma) <0 ) { +return -1 ; + } + + seq = ++r->barrier_next_seq; + +if (seq ==0 ) {/* seq 0 is the "no barrier" sentinel */ + seq = ++r->barrier_next_seq; + } + cmd = (RDMACmd ){ + .type = type, + .flags = flags, + .iov = iov, + .niov = niov, + .seq = seq, + }; + +if (ring_push (rdma, &cmd) <0 ) { +return -1 ; + } + +return ring_wait_barrier (r, seq); +} +static bool ring_pop (RDMASendRing *r,RDMACmd *cmd) {if (ring_empty (r)) {
@@ -2928,7 +2889,7 @@ static int rdma_consumer_dispatch(RDMAContext *rdma, const RDMACmd *cmd,switch (cmd->type) {case RDMA_CMD_WRITE : ret =qemu_rdma_write_chunk (rdma, cmd->block_index, - cmd->chunk_index, &err); + cmd->offset, cmd->length, &err);break ;case RDMA_CMD_FLUSH : ret =qemu_rdma_drain_cq (rdma, &err);
@@ -2973,6 +2934,15 @@ static void *rdma_consumer_thread(void *opaque)ring_advance_tail (r);continue ; } +if (rdma->nb_sent >0 ) { +/* Ring empty but writes in flight: reap (bounded, error-aware). */ +if (qemu_rdma_block_for_wrid (rdma,RDMA_WRID_RDMA_WRITE , +NULL ) <0 ) { +ring_set_abort (rdma); +break ; + } +continue ; + }ring_idle_wait (r); }
@@ -3014,6 +2984,7 @@ static void rdma_consumer_stop(RDMAContext *rdma) }/* Ask the consumer to drain and exit; if it already aborted, just join. */ if (!qatomic_read (&r->abort)) { +producer_flush_pending (rdma);/* order the last span before FINISH */ ring_push (rdma, &cmd); }qemu_thread_join (&rdma->consumer_thread);
@@ -3298,7 +3269,8 @@ static int qemu_rdma_save_page(QEMUFile *f, ram_addr_t block_offset,QIOChannelRDMA *rioc =QIO_CHANNEL_RDMA (qemu_file_get_ioc (f));Error *err =NULL ;RDMAContext *rdma; -int ret; +RDMALocalBlock *block; +uint64_t bidx, first_chunk, chunk_size, pos, end;RCU_READ_LOCK_GUARD (); rdma =qatomic_rcu_read (&rioc->rdmaout);
@@ -3313,60 +3285,46 @@ static int qemu_rdma_save_page(QEMUFile *f, ram_addr_t block_offset,qemu_fflush (f); -/* -* Add this page to the current 'chunk'. If the chunk -* is full, or the page doesn't belong to the current chunk, -* an actual RDMA write will occur and a new chunk will be formed. -*/ - ret =qemu_rdma_write (rdma, block_offset, offset, size, &err); -if (ret <0 ) { -error_report_err (err); - goto err; - } +qemu_rdma_search_ram_block (rdma, block_offset, offset, size, + &bidx, &first_chunk); + block = &rdma->local_ram_blocks.block[bidx]; + chunk_size =migrate_rdma_chunk_size ();/* -* Drain the Completion Queue if possible, but do not block, -* just poll. -* -* If nothing to poll, the end of the iteration will do this -* again to make sure we don't overflow the request queue. +* Split the dirty range into per-chunk segments (a WRITE never crosses a +* chunk boundary) and coalesce with the pending span: pages are scanned +* in increasing order, so contiguous dirty pages extend one span, and we +* enqueue only the actually-dirty bytes instead of whole chunks. */ -while (1 ) { -uint64_t wr_id, wr_id_in; - ret =qemu_rdma_poll (rdma, rdma->recv_cq, &wr_id_in,NULL ); - -if (ret <0 ) { -error_report ("rdma migration: polling error" ); - goto err; - } - - wr_id = wr_id_in &RDMA_WRID_TYPE_MASK ; - -if (wr_id ==RDMA_WRID_NONE ) { -break ; - } - } - -while (1 ) { -uint64_t wr_id, wr_id_in; - ret =qemu_rdma_poll (rdma, rdma->send_cq, &wr_id_in,NULL ); - -if (ret <0 ) { -error_report ("rdma migration: polling error" ); - goto err; - } - - wr_id = wr_id_in &RDMA_WRID_TYPE_MASK ; - -if (wr_id ==RDMA_WRID_NONE ) { -break ; + pos = (block_offset + offset) - block->offset; + end = pos + size; +while (pos < end) { +uint64_t c = pos / chunk_size; +uint64_t seg_end =MIN (end, (c +1 ) * chunk_size); +uint64_t seg_len = seg_end - pos; + +if (rdma->pend_block == (int )bidx && rdma->pend_chunk == (uint32_t )c && + rdma->pend_offset + rdma->pend_length == pos) { + rdma->pend_length += seg_len;/* extend the current span */ + }else { +if (producer_flush_pending (rdma) <0 ) { +error_setg (&err,"rdma migration: consumer aborted" ); + goto err; + } + rdma->pend_block = bidx; + rdma->pend_chunk = c; + rdma->pend_offset = pos; + rdma->pend_length = seg_len; } + pos = seg_end; }return RAM_SAVE_CONTROL_DELAYED ; err: +error_report_err (err); rdma->errored = true; +ring_set_abort (rdma);return -1 ; }
@@ -3991,20 +3949,25 @@ int rdma_registration_stop(QEMUFile *f, uint64_t flags)return -1 ; } -qemu_fflush (f); - -if (flags ==RAM_CONTROL_FINISH ) { -rdma_nbsent_stats_dump (rdma); - } +trace_rdma_registration_stop (flags); - ret =qemu_rdma_write_flush (rdma, &err); +qemu_fflush (f); -if (ret <0 ) { -error_report_err (err); - goto err; - } +if (flags !=RAM_CONTROL_SETUP ) { +/* +* ROUND / FINISH: the consumer sends REGISTER_FINISHED, ordered +* after all chunk writes queued so far. For FINISH it also drains +* outstanding writes and dumps the stats first. +*/ +if (ring_barrier (rdma,RDMA_CMD_REG_STOP , flags,NULL ,0 ) <0 ) { +error_report ("rdma migration: consumer aborted at registration" +" stop" ); + goto err; + } +return 0 ; -if (flags ==RAM_CONTROL_SETUP ) { + }else { +/* SETUP runs on the producer, before the consumer exists. */ RDMAControlHeader resp = {.type =RDMA_CONTROL_RAM_BLOCKS_RESULT };RDMALocalBlocks *local = &rdma->local_ram_blocks;int reg_result_idx, nb_dest_blocks;
@@ -4072,26 +4035,24 @@ int rdma_registration_stop(QEMUFile *f, uint64_t flags) rdma->dest_blocks[i].remote_host_addr; local->block[i].remote_rkey = rdma->dest_blocks[i].remote_rkey; } - } - -trace_rdma_registration_stop (flags); - head.type =RDMA_CONTROL_REGISTER_FINISHED ; - ret =qemu_rdma_exchange_send (rdma, &head,NULL ,NULL ,NULL ,NULL , &err); + head.type =RDMA_CONTROL_REGISTER_FINISHED ; + ret =qemu_rdma_exchange_send (rdma, &head,NULL ,NULL ,NULL , +NULL , &err); -if (ret <0 ) { -error_report_err (err); - goto err; - } +if (ret <0 ) { +error_report_err (err); + goto err; + } -if (flags ==RAM_CONTROL_SETUP ) { -/* Setup complete: allocate the send ring (consumer added later). */ +/* Setup complete: start the consumer; it now owns all QP work. */ rdma_consumer_start (rdma); }return 0 ; err: rdma->errored = true; +ring_set_abort (rdma);return -1 ; }
Signature · 1 line
2.39.5
RFC PATCH 8/13 migration/rdma: Add producer/consumer tuning instrumentation
In reply to Dmitrii OmelchenkoAdd producer full-wait, ring occupancy and consumer idle counters, the per-migration prodcons stats dump, and trace events for ring push, barrier, wake, dispatch and consumer lifecycle. Observability only; the transfer path is unchanged.
--- migration/rdma.c | 75 +++++++++++++++++++++++++++++++++++++++++- migration/trace-events | 13 ++++++++ 2 files changed, 87 insertions(+), 1 deletion(-)
migration/rdma.c +74 -1
@@ -32,6 +32,7 @@#include "qemu/thread.h" #include "qemu/sockets.h" #include "qemu/coroutine.h" +#include "qemu/timer.h" #include "system/memory.h" #include <sys/socket.h> #include <netdb.h>
@@ -1213,7 +1214,9 @@ static int producer_flush_pending(RDMAContext *rdma);static int ring_barrier (RDMAContext *rdma,RDMACmdType type,uint64_t flags,const struct iovec *iov,size_t niov);static void ring_set_abort (RDMAContext *rdma); +static unsigned ring_occupancy (RDMAContext *rdma);static void rdma_nbsent_stats_dump (RDMAContext *rdma); +static void rdma_prodcons_stats_dump (RDMASendRing *r);/* * Consult the connection manager to see a work request
@@ -2616,6 +2619,7 @@ static int rdma_ram_round_notify(NotifierWithReturn *n G_GNUC_UNUSED, }/* Complete all chunks in the ring before the next round. */ +trace_rdma_ram_round_notify (ring_occupancy (rdma));if (ring_barrier (rdma,RDMA_CMD_FLUSH ,0 ,NULL ,0 ) <0 ) {error_setg (errp,"RDMA consumer aborted during round drain" );return -1 ;
@@ -2684,6 +2688,14 @@ static bool ring_empty(RDMASendRing *r)return (qatomic_load_acquire (&r->head) - r->tail) ==0 ; } +/* Producer-side snapshot of queued-but-unprocessed commands in the ring. */ +static unsigned ring_occupancy (RDMAContext *rdma) +{ +RDMASendRing *r = rdma->send_ring; + +return r->head -qatomic_read (&r->tail); +} +static void ring_set_abort (RDMAContext *rdma) {RDMASendRing *r = rdma->send_ring;
@@ -2703,6 +2715,7 @@ static void ring_wake_producer(RDMASendRing *r) {smp_mb ();/* order the *_waiting load below vs the sleeper's store */ if (qatomic_read (&r->producer_waiting)) { +trace_rdma_ring_wake_producer ();qemu_mutex_lock (&r->wait_lock);qemu_cond_broadcast (&r->not_full);qemu_cond_broadcast (&r->barrier_cv);
@@ -2715,20 +2728,38 @@ static void ring_wake_consumer(RDMASendRing *r) {smp_mb ();/* order the *_waiting load below vs the sleeper's store */ if (qatomic_read (&r->consumer_waiting)) { +trace_rdma_ring_wake_consumer ();qemu_mutex_lock (&r->wait_lock);qemu_cond_signal (&r->not_empty);qemu_mutex_unlock (&r->wait_lock); } } +static const char *rdma_cmd_type_str (RDMACmdType type) +{ +switch (type) { +case RDMA_CMD_WRITE :return "WRITE" ; +case RDMA_CMD_FLUSH :return "FLUSH" ; +case RDMA_CMD_REG_STOP :return "REG_STOP" ; +case RDMA_CMD_FILE :return "FILE" ; +case RDMA_CMD_FINISH :return "FINISH" ; +default :return "UNKNOWN" ; + } +} +static int ring_push (RDMAContext *rdma,const RDMACmd *cmd) {RDMASendRing *r = rdma->send_ring; +unsigned occupancy;/* SETUP starts the consumer before any WRITE/FLUSH/REG_STOP/FILE. */ assert (r); +trace_rdma_ring_push_begin (rdma_cmd_type_str (cmd->type), + cmd->chunk_index);if (ring_full (r)) { +int64_t wait_ns =qemu_clock_get_ns (QEMU_CLOCK_REALTIME ); +qemu_mutex_lock (&r->wait_lock); r->producer_waiting = true;smp_mb ();/* pair with ring_wake_producer() */
@@ -2737,12 +2768,23 @@ static int ring_push(RDMAContext *rdma, const RDMACmd *cmd) } r->producer_waiting = false;qemu_mutex_unlock (&r->wait_lock); + + wait_ns =qemu_clock_get_ns (QEMU_CLOCK_REALTIME ) - wait_ns; + r->pushwait_total_ns += wait_ns; +if ((uint64_t )wait_ns > r->pushwait_max_ns) { + r->pushwait_max_ns = wait_ns; + } +trace_rdma_ring_push_full_wait (cmd->type, wait_ns); }if (qatomic_read (&r->abort)) {return -1 ; } r->ring[r->head &RDMA_SEND_RING_MASK ] = *cmd;qatomic_store_release (&r->head, r->head +1 );/* publish payload+slot */ + occupancy = r->head -qatomic_read (&r->tail); +if (occupancy > r->occupancy_max) { + r->occupancy_max = occupancy; + }ring_wake_consumer (r);return 0 ; }
@@ -2798,8 +2840,10 @@ static int ring_barrier(RDMAContext *rdma, RDMACmdType type, uint64_t flags, const struct iovec *iov, size_t niov) {RDMASendRing *r = rdma->send_ring; +int64_t wait_ns;unsigned seq;RDMACmd cmd; +int ret;assert (r);/* consumer started at SETUP; see ring_push() */
@@ -2813,6 +2857,7 @@ static int ring_barrier(RDMAContext *rdma, RDMACmdType type, uint64_t flags,if (seq ==0 ) {/* seq 0 is the "no barrier" sentinel */ seq = ++r->barrier_next_seq; } +trace_rdma_ring_barrier_begin (type, seq); cmd = (RDMACmd ){ .type = type, .flags = flags,
@@ -2822,10 +2867,16 @@ static int ring_barrier(RDMAContext *rdma, RDMACmdType type, uint64_t flags, };if (ring_push (rdma, &cmd) <0 ) { +trace_rdma_ring_barrier_end (type, seq,0 ,-1 );return -1 ; } -return ring_wait_barrier (r, seq); +/* time spent blocked waiting for the consumer to complete the barrier */ + wait_ns =qemu_clock_get_ns (QEMU_CLOCK_REALTIME ); + ret =ring_wait_barrier (r, seq); + wait_ns =qemu_clock_get_ns (QEMU_CLOCK_REALTIME ) - wait_ns; +trace_rdma_ring_barrier_end (type, seq, wait_ns, ret); +return ret; }static bool ring_pop (RDMASendRing *r,RDMACmd *cmd)
@@ -2853,12 +2904,21 @@ static void ring_barrier_complete(RDMASendRing *r, unsigned seq)/* Consumer: sleep until the ring is non-empty or aborted. */ static void ring_idle_wait (RDMASendRing *r) { +int64_t start_ns, idle_ns; +qemu_mutex_lock (&r->wait_lock); r->consumer_waiting = true;smp_mb ();/* pair with ring_wake_consumer() */ + start_ns =qemu_clock_get_ns (QEMU_CLOCK_REALTIME ); +trace_rdma_consumer_idle_begin ();while (ring_empty (r) && !r->abort) {qemu_cond_wait (&r->not_empty, &r->wait_lock); } + idle_ns =qemu_clock_get_ns (QEMU_CLOCK_REALTIME ) - start_ns; +trace_rdma_consumer_idle_end (idle_ns); +if ((uint64_t )idle_ns > r->idle_max_ns) { + r->idle_max_ns = idle_ns; + } r->consumer_waiting = false;qemu_mutex_unlock (&r->wait_lock); }
@@ -2876,6 +2936,7 @@ static int rdma_consumer_reg_stop(RDMAContext *rdma, uint64_t flags,return -1 ; }rdma_nbsent_stats_dump (rdma); +rdma_prodcons_stats_dump (rdma->send_ring); }return qemu_rdma_exchange_send (rdma, &head,NULL ,NULL ,NULL ,NULL , errp); }
@@ -2886,6 +2947,9 @@ static int rdma_consumer_dispatch(RDMAContext *rdma, const RDMACmd *cmd,Error *err =NULL ;int ret =0 ; +trace_rdma_consumer_dispatch (cmd->type, cmd->block_index, + cmd->chunk_index, cmd->seq); +switch (cmd->type) {case RDMA_CMD_WRITE : ret = qemu_rdma_write_chunk(rdma, cmd->block_index,
@@ -2919,6 +2983,7 @@ static void *rdma_consumer_thread(void *opaque)bool stop = false;rcu_register_thread (); +trace_rdma_consumer_thread_start ();while (!qatomic_read (&r->abort) && !stop) {RDMACmd cmd;
@@ -2950,6 +3015,7 @@ static void *rdma_consumer_thread(void *opaque)/* make the failure visible to the producer's rdma_errored() checks */ rdma->errored = true; } +trace_rdma_consumer_thread_stop (qatomic_read (&r->abort));rcu_unregister_thread ();return NULL ; }
@@ -3363,6 +3429,13 @@ static void rdma_nbsent_stats_dump(RDMAContext *rdma) rdma->nbsent_full_hits, rdma->nbsent_zero_hits); } +/* Producer/consumer ring stats for the whole migration (consumer at FINISH). */ +static void rdma_prodcons_stats_dump (RDMASendRing *r) +{ +trace_rdma_prodcons_stats (r->idle_max_ns, r->pushwait_max_ns, + r->pushwait_total_ns, r->occupancy_max); +} +int rdma_control_save_page (QEMUFile *f,ram_addr_t block_offset,ram_addr_t offset,size_t size) {
migration/trace-events +13 -0
@@ -250,6 +250,19 @@ rdma_write_flush(int sent) "sent total: %d" rdma_write_one_post(uint64_t chunk, long addr, long remote, uint32_t len) "Posting chunk: %" PRIu64 ", addr: 0x%lx remote: 0x%lx, bytes %" PRIu32 rdma_post_send_queue_full(uint64_t wr_id, int sent) "send queue full, wr_id=%" PRIu64 ", outstanding writes=%d" rdma_nbsent_stats(uint64_t max, uint64_t posts, uint64_t full_hits, uint64_t zero_hits) "nb_sent max %" PRIu64 " posts %" PRIu64 " full-queue hits %" PRIu64 " drains to zero %" PRIu64 +rdma_consumer_dispatch(int type, uint32_t bidx, uint32_t cidx, uint32_t seq) "cmd type=%d block=%" PRIu32 " chunk=%" PRIu32 " seq=%" PRIu32 +rdma_ring_wake_producer(void) "waking blocked producer" +rdma_ring_wake_consumer(void) "waking idle consumer" +rdma_ring_push_begin(const char *type, uint32_t chunk_index) "push begin type=%s chunk_index=%" PRIu32 +rdma_ring_push_full_wait(int type, int64_t wait_ns) "push full-wait type=%d wait=%" PRId64 " ns" +rdma_ring_barrier_begin(int type, uint32_t seq) "barrier begin type=%d seq=%" PRIu32 +rdma_ring_barrier_end(int type, uint32_t seq, int64_t wait_ns, int ret) "barrier end type=%d seq=%" PRIu32 " wait=%" PRId64 " ns ret=%d" +rdma_consumer_thread_start(void) "consumer thread started" +rdma_consumer_thread_stop(bool aborted) "consumer thread stopped, aborted=%d" +rdma_consumer_idle_begin(void) "consumer idle: waiting for commands" +rdma_consumer_idle_end(int64_t idle_ns) "consumer idle: woke after %" PRId64 " ns" +rdma_ram_round_notify(uint32_t occupancy) "ram round notify: ring occupancy=%" PRIu32 +rdma_prodcons_stats(uint64_t idle_max, uint64_t pushwait_max, uint64_t pushwait_total, uint64_t occupancy_max) "idle max %" PRIu64 " ns, push full-wait max %" PRIu64 " ns total %" PRIu64 " ns, ring occupancy max %" PRIu64 rdma_write_one_recvregres(int mykey, int theirkey, uint64_t chunk) "Received registration result: my key: 0x%x their key 0x%x, chunk %" PRIu64 rdma_write_one_sendreg(uint64_t chunk, int len, int index, int64_t offset) "Sending registration request chunk %" PRIu64 " for %d bytes, index: %d, offset: %" PRId64 rdma_write_one_top(uint64_t chunks, uint64_t size) "Writing %" PRIu64 " chunks, (%" PRIu64 " MB)"
Signature · 1 line
2.39.5
RFC PATCH 9/13 migration/rdma: drain the send ring in batches
In reply to Dmitrii OmelchenkoThe consumer snapshots ring occupancy, processes up to RDMA_CONSUMER_BATCH_MAX commands, and advances the tail once per batch instead of once per command.
--- migration/rdma.c | 81 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 23 deletions(-)
migration/rdma.c +58 -23
@@ -67,6 +67,9 @@ static inline uint64_t rdma_merge_max(void)#define RDMA_CONTROL_MAX_BUFFER (512 * 1024)#define RDMA_CONTROL_MAX_COMMANDS_PER_MESSAGE 4096 +/* Max WRITE commands the consumer drains per pass. */ +#define RDMA_CONSUMER_BATCH_MAX 128 +#define RDMA_CONTROL_VERSION_CURRENT 1/* * Capabilities for negotiation.
@@ -2688,6 +2691,12 @@ static bool ring_empty(RDMASendRing *r)return (qatomic_load_acquire (&r->head) - r->tail) ==0 ; } +/* Consumer-side count of published, unconsumed commands. */ +static unsigned ring_avail (RDMASendRing *r) +{ +return qatomic_load_acquire (&r->head) - r->tail; +} +/* Producer-side snapshot of queued-but-unprocessed commands in the ring. */ static unsigned ring_occupancy (RDMAContext *rdma) {
@@ -2879,18 +2888,10 @@ static int ring_barrier(RDMAContext *rdma, RDMACmdType type, uint64_t flags,return ret; } -static bool ring_pop (RDMASendRing *r,RDMACmd *cmd) +/* Consumer: release n drained slots at once, waking a blocked producer once. */ +static void ring_advance_tail_by (RDMASendRing *r,unsigned n) { -if (ring_empty (r)) { -return false; - } - *cmd = r->ring[r->tail &RDMA_SEND_RING_MASK ];/* after load_acquire */ -return true; -} - -static void ring_advance_tail (RDMASendRing *r) -{ -qatomic_store_release (&r->tail, r->tail +1 ); +qatomic_store_release (&r->tail, r->tail + n);ring_wake_producer (r); }
@@ -2986,29 +2987,63 @@ static void *rdma_consumer_thread(void *opaque)trace_rdma_consumer_thread_start ();while (!qatomic_read (&r->abort) && !stop) { -RDMACmd cmd; +unsigned avail =ring_avail (r); +unsigned batch, processed =0 ; + +if (avail ==0 ) { +/* +* Ring empty: reap ready completions without blocking, then +* sleep until the next push so a new command wakes us at once. +* Round draining is handled by RDMA_CMD_FLUSH. +*/ +while (rdma->nb_sent >0 ) { +uint64_t wr_id; + +if (qemu_rdma_poll (rdma, rdma->send_cq, &wr_id,NULL ) <0 ) { +ring_set_abort (rdma); +break ; + } +if (wr_id ==RDMA_WRID_NONE ) { +break ;/* nothing more ready right now */ + } + } +if (qatomic_read (&r->abort)) { +break ; + } +ring_idle_wait (r); +continue ; + } + +/* +* Drain up to RDMA_CONSUMER_BATCH_MAX commands, or fewer if that is +* all that is queued, then publish tail once for the whole batch. +*/ + batch = avail <RDMA_CONSUMER_BATCH_MAX ? avail + :RDMA_CONSUMER_BATCH_MAX ; + +while (processed < batch) { +unsigned idx = r->tail + processed; +RDMACmd cmd = r->ring[idx &RDMA_SEND_RING_MASK ]; -if (ring_pop (r, &cmd)) {if (rdma_consumer_dispatch (rdma, &cmd, &stop) <0 ) {ring_set_abort (rdma);break ; } + processed++;if (cmd.seq) {ring_barrier_complete (r, cmd.seq); } -ring_advance_tail (r); -continue ; - } -if (rdma->nb_sent >0 ) { -/* Ring empty but writes in flight: reap (bounded, error-aware). */ -if (qemu_rdma_block_for_wrid (rdma,RDMA_WRID_RDMA_WRITE , -NULL ) <0 ) { -ring_set_abort (rdma); +if (stop) {break ; } -continue ; } -ring_idle_wait (r); + +if (processed) { +ring_advance_tail_by (r, processed); + } +if (qatomic_read (&r->abort)) { +break ; + } }if (qatomic_read (&r->abort)) {
Signature · 1 line
2.39.5
RFC PATCH 10/13 migration/rdma: size reg_resp.len by the register request count
In reply to Dmitrii OmelchenkoThe destination assumed repeat=1; set reg_resp.len to one RDMARegisterResult per requested chunk so a batched REGISTER_REQUEST is answered in full.
--- migration/rdma.c | 2 ++ 1 file changed, 2 insertions(+)
migration/rdma.c +2 -0
@@ -3870,6 +3870,8 @@ int rdma_registration_handle(QEMUFile *f)trace_rdma_registration_handle_register (head.repeat); reg_resp.repeat = head.repeat; +/* one result per requested chunk (source may batch registers) */ + reg_resp.len = head.repeat *sizeof (RDMARegisterResult); registers = (RDMARegister *) rdma->wr_data[idx].control_curr;/* Making sure the register buffers to read are valid */
Signature · 1 line
2.39.5
RFC PATCH 11/13 migration/rdma: batch chunk registration in one round-trip
In reply to Dmitrii OmelchenkoSplit qemu_rdma_write_one() into three helpers: write_one posts one dirty span of an already-registered chunk (both pin modes), write_compress handles an all-zero span, and write_register folds several never-seen chunks into one REGISTER_REQUEST. The consumer scans a run of WRITE commands, posting ready and zero spans inline and registering the never-seen chunks in a single round-trip before posting them.
--- migration/rdma.c | 372 ++++++++++++++++++++++++++++------------------- 1 file changed, 223 insertions(+), 149 deletions(-)
migration/rdma.c +223 -149
@@ -1208,6 +1208,7 @@ static uint64_t qemu_rdma_make_wrid(uint64_t wr_id, uint64_t index,static void rdma_nbsent_inc (RDMAContext *rdma,uint64_t nb_sent);static void rdma_nbsent_full (RDMAContext *rdma);static void rdma_nbsent_zero (RDMAContext *rdma); +static void rdma_reg_batch_record (RDMASendRing *r,unsigned count);static void rdma_consumer_start (RDMAContext *rdma);static void rdma_consumer_stop (RDMAContext *rdma);
@@ -1806,149 +1807,41 @@ static int qemu_rdma_exchange_recv(RDMAContext *rdma, RDMAControlHeader *head,return 0 ; } -/* -* Write an actual chunk of memory using RDMA. -* -* If we're using dynamic registration on the dest-side, we have to -* send a registration command first. -*/ -static int qemu_rdma_write_one (RDMAContext *rdma, -int current_index,uint64_t current_addr, -uint64_t length,Error **errp) +/* Post one dirty span of an already-registered chunk (both pin modes). */ +static int qemu_rdma_write_one (RDMAContext *rdma,int current_index, +uint64_t current_addr,uint64_t length, +Error **errp) {struct ibv_sge sge;struct ibv_send_wr send_wr = {0 }; -int reg_result_idx, ret; -uint64_t chunk, chunks; -uint64_t chunk_size =migrate_rdma_chunk_size (); +RDMALocalBlock *block = &rdma->local_ram_blocks.block[current_index]; +uint64_t chunk;uint8_t *chunk_start, *chunk_end; -RDMALocalBlock *block = &(rdma->local_ram_blocks.block[current_index]); -RDMARegister reg; -RDMARegisterResult *reg_result; -RDMAControlHeader resp = { .type =RDMA_CONTROL_REGISTER_RESULT }; -RDMAControlHeader head = { .len =sizeof (RDMARegister), - .type =RDMA_CONTROL_REGISTER_REQUEST , - .repeat =1 , - }; sge.addr = (uintptr_t )(block->local_host_addr + - (current_addr - block->offset)); + (current_addr - block->offset)); sge.length = length; - chunk =ram_chunk_index (block->local_host_addr, - (uint8_t *)(uintptr_t )sge.addr); + chunk =ram_chunk_index (block->local_host_addr, (uint8_t *)sge.addr); chunk_start =ram_chunk_start (block, chunk); - chunks = length / chunk_size; - -if (chunks && ((length % chunk_size) ==0 )) { - chunks--; - } - -trace_rdma_write_one_top (chunks +1 , - (chunks +1 ) * chunk_size /1024 /1024 ); + chunk_end =ram_chunk_end (block, chunk); - chunk_end =ram_chunk_end (block, chunk + chunks); - -if (!rdma->pin_all) { +if (rdma->pin_all) { + send_wr.wr.rdma.rkey = block->remote_rkey; + }else {if (!block->remote_keys[chunk]) { -/* -* This chunk has not yet been registered, so first check to see -* if the entire chunk is zero. If so, tell the other size to -* memset() + madvise() the entire chunk without RDMA. -*/ - -if (buffer_is_zero ((void *)(uintptr_t )sge.addr, length)) { -RDMACompress comp = { - .offset = current_addr, - .value =0 , - .block_idx = current_index, - .length = length, - }; - - head.len =sizeof (comp); - head.type =RDMA_CONTROL_COMPRESS ; - -trace_rdma_write_one_zero (chunk, sge.length, - current_index, current_addr); - -compress_to_network (rdma, &comp); - ret =qemu_rdma_exchange_send (rdma, &head, - (uint8_t *) &comp,NULL ,NULL ,NULL , errp); - -if (ret <0 ) { -return -1 ; - } - -/* -* TODO: Here we are sending something, but we are not -* accounting for anything transferred. The following is wrong: -* -* stat64_add(&mig_stats.rdma_bytes, sge.length); -* -* because we are using some kind of compression. I -* would think that head.len would be the more similar -* thing to a correct value. -*/ -qatomic_add (&mig_stats.zero_pages, - sge.length /qemu_target_page_size ()); -return 1 ; - } - -/* -* Otherwise, tell other side to register. -*/ - reg.current_index = current_index; - reg.current_addr = current_addr; - reg.chunks = chunks; - -trace_rdma_write_one_sendreg (chunk, sge.length, current_index, - current_addr); - -register_to_network (rdma, ®); - ret =qemu_rdma_exchange_send (rdma, &head, (uint8_t *) ®, - &resp, ®_result_idx,NULL , errp); -if (ret <0 ) { -return -1 ; - } - -/* try to overlap this single registration with the one we sent. */ -if (qemu_rdma_register_and_get_keys (rdma, block, sge.addr, - &sge.lkey,NULL , chunk, - chunk_start, chunk_end)) { -error_setg (errp,"cannot get lkey" ); -return -1 ; - } - - reg_result = (RDMARegisterResult *) - rdma->wr_data[reg_result_idx].control_curr; - -network_to_result (reg_result); - -trace_rdma_write_one_recvregres (block->remote_keys[chunk], - reg_result->rkey, chunk); - - block->remote_keys[chunk] = reg_result->rkey; - block->remote_host_addr = reg_result->host_addr; - }else { -/* already registered before */ -if (qemu_rdma_register_and_get_keys (rdma, block, sge.addr, - &sge.lkey,NULL , chunk, - chunk_start, chunk_end)) { -error_setg (errp,"cannot get lkey!" ); -return -1 ; - } +error_setg (errp,"rdma: write_one on unregistered chunk %" PRIu64, + chunk); +return -1 ; } - send_wr.wr.rdma.rkey = block->remote_keys[chunk]; - }else { - send_wr.wr.rdma.rkey = block->remote_rkey; + } -if (qemu_rdma_register_and_get_keys (rdma, block, sge.addr, - &sge.lkey,NULL , chunk, - chunk_start, chunk_end)) { -error_setg (errp,"cannot get lkey!" ); -return -1 ; - } +/* Local MR (idempotent): pins our own memory so the NIC can read it. */ +if (qemu_rdma_register_and_get_keys (rdma, block, sge.addr, &sge.lkey, +NULL , chunk, chunk_start, chunk_end)) { +error_setg (errp,"cannot get lkey" ); +return -1 ; }/*
@@ -1956,18 +1849,17 @@ static int qemu_rdma_write_one(RDMAContext *rdma,* We will use this information at the time of completion * to identify the completed write in trace output. */ - send_wr.wr_id =qemu_rdma_make_wrid (RDMA_WRID_RDMA_WRITE , - current_index, chunk); - + send_wr.wr_id =qemu_rdma_make_wrid (RDMA_WRID_RDMA_WRITE , current_index, + chunk); send_wr.opcode =IBV_WR_RDMA_WRITE ; send_wr.send_flags =IBV_SEND_SIGNALED ; send_wr.sg_list = &sge; send_wr.num_sge =1 ; send_wr.wr.rdma.remote_addr = block->remote_host_addr + - (current_addr - block->offset); + (current_addr - block->offset);trace_rdma_write_one_post (chunk, sge.addr, send_wr.wr.rdma.remote_addr, - sge.length); + sge.length);if (!qemu_rdma_post_send (rdma, &send_wr, errp)) {return -1 ;
@@ -1986,27 +1878,86 @@ static int qemu_rdma_write_one(RDMAContext *rdma,qatomic_add (&mig_stats.rdma_bytes, sge.length);ram_transferred_add (sge.length); rdma->total_writes++; + rdma->nb_sent++; +trace_rdma_write_flush (rdma->nb_sent); +rdma_nbsent_inc (rdma, rdma->nb_sent); +return 0 ; +} +/* Zero an all-zero span on the destination without RDMA (pin_all=off). */ +static int qemu_rdma_write_compress (RDMAContext *rdma,int current_index, +uint64_t current_addr,uint64_t length, +Error **errp) +{ +RDMALocalBlock *block = &rdma->local_ram_blocks.block[current_index]; +uint64_t chunk = (current_addr - block->offset) /migrate_rdma_chunk_size (); +RDMACompress comp = { + .offset = current_addr, + .value =0 , + .block_idx = current_index, + .length = length, + }; +RDMAControlHeader head = { .len =sizeof (comp), + .type =RDMA_CONTROL_COMPRESS , + .repeat =1 }; + +trace_rdma_write_one_zero (chunk, length, current_index, current_addr); + +compress_to_network (rdma, &comp); +if (qemu_rdma_exchange_send (rdma, &head, (uint8_t *)&comp, +NULL ,NULL ,NULL , errp) <0 ) { +return -1 ; + } +qatomic_add (&mig_stats.zero_pages, length /qemu_target_page_size ());return 0 ; } -/* Consumer: write one dirty span (offset/length are block-relative bytes) */ -static int qemu_rdma_write_chunk (RDMAContext *rdma,uint32_t block_index, -uint64_t offset,uint64_t length,Error **errp) +/* +* Register a batch of never-seen non-zero chunks in one REGISTER_REQUEST +* (pin_all=off); regs[] carry current_index/current_addr in host order. One +* control round-trip, then cache each chunk's remote rkey/host_addr so the +* following write_one() calls can post. +*/ +static int qemu_rdma_write_register (RDMAContext *rdma,RDMARegister *regs, +unsigned count,Error **errp) { -RDMALocalBlock *block = &rdma->local_ram_blocks.block[block_index]; -int ret; +RDMARegister net[RDMA_CONSUMER_BATCH_MAX ]; +RDMARegisterResult *results; +RDMAControlHeader resp = { .type =RDMA_CONTROL_REGISTER_RESULT }; +RDMAControlHeader head = { .type =RDMA_CONTROL_REGISTER_REQUEST }; +int reg_result_idx; +unsigned i; -/* write_one wants current_addr in ram_addr_t space (block->offset based) */ - ret =qemu_rdma_write_one (rdma, block_index, block->offset + offset, - length, errp); -if (ret <0 ) { +assert (count >=1 && count <=RDMA_CONSUMER_BATCH_MAX ); + +/* network-order copy to send; regs[] stays host-order for caching below */ +for (i =0 ; i < count; i++) { + net[i] = regs[i]; + net[i].chunks =0 ;/* exactly one chunk per entry */ +register_to_network (rdma, &net[i]); + } + head.len = count *sizeof (RDMARegister); + head.repeat = count; + +rdma_reg_batch_record (rdma->send_ring, count); + +if (qemu_rdma_exchange_send (rdma, &head, (uint8_t *)net, + &resp, ®_result_idx,NULL , errp) <0 ) {return -1 ; } -if (ret ==0 ) { - rdma->nb_sent++; -trace_rdma_write_flush (rdma->nb_sent); -rdma_nbsent_inc (rdma, rdma->nb_sent); + + results = (RDMARegisterResult *)rdma->wr_data[reg_result_idx].control_curr; +for (i =0 ; i < count; i++) { +RDMALocalBlock *block = + &rdma->local_ram_blocks.block[regs[i].current_index]; +uint64_t chunk = + (regs[i].current_addr - block->offset) /migrate_rdma_chunk_size (); + +network_to_result (&results[i]); +trace_rdma_write_one_recvregres (block->remote_keys[chunk], + results[i].rkey, chunk); + block->remote_keys[chunk] = results[i].rkey; + block->remote_host_addr = results[i].host_addr; }return 0 ; }
@@ -2679,6 +2630,7 @@ struct RDMASendRing {uint64_t pushwait_total_ns;/* total producer wait on a full ring */ uint64_t occupancy_max;/* peak queued commands */ uint64_t idle_max_ns;/* longest consumer wait on an empty ring */ +uint64_t reg_batch_max;/* largest batched REGISTER_REQUEST */ };static bool ring_full (RDMASendRing *r)
@@ -2942,6 +2894,105 @@ static int rdma_consumer_reg_stop(RDMAContext *rdma, uint64_t flags,return qemu_rdma_exchange_send (rdma, &head,NULL ,NULL ,NULL ,NULL , errp); } +/* +* Dirty-span parameters for a ring WRITE command: block, block-relative +* offset, migration address and length. The span is the actually-dirtied +* byte range (within one chunk), not the whole chunk. +*/ +static void rdma_get_chunk_args (RDMAContext *rdma,const RDMACmd *cmd, +RDMALocalBlock **block,uint64_t *off, +uint64_t *addr,uint64_t *len) +{ +RDMALocalBlock *b = &rdma->local_ram_blocks.block[cmd->block_index]; + + *block = b; + *off = cmd->offset; + *addr = b->offset + cmd->offset; + *len = cmd->length; +} + +/* +* Consumer: process the maximal run of consecutive RDMA_CMD_WRITE commands at +* ring position 'pos' (at most 'max'), stopping at the first barrier. Each +* command carries one dirty span. Post ready (already registered / pin_all) +* and zero spans as they are met; defer spans whose chunk needs registering, +* register those chunks in one batched REGISTER_REQUEST, then post the spans. +* Returns the WRITE commands consumed, or -1. +*/ +static int qemu_rdma_write_run (RDMAContext *rdma,RDMASendRing *r, +unsigned pos,unsigned max,Error **errp) +{ +unsigned reg_ring_idx[RDMA_CONSUMER_BATCH_MAX ];/* deferred span writes */ +RDMARegister regs[RDMA_CONSUMER_BATCH_MAX ];/* distinct chunks to reg */ +unsigned n =0 , regn =0 , nreg =0 , i; +int last_bidx =-1 ;/* last chunk queued into regs[] */ +int64_t last_cidx =-1 ; + +/* Phase 1: scan the run; post ready/zero now, defer never-registered. */ +while (n < max) { +RDMACmd *cmd = &r->ring[(pos + n) &RDMA_SEND_RING_MASK ]; +RDMALocalBlock *block; +uint64_t off, addr, len; + +if (cmd->type !=RDMA_CMD_WRITE ) { +break ; + } +rdma_get_chunk_args (rdma, cmd, &block, &off, &addr, &len); + +if (rdma->pin_all || block->remote_keys[cmd->chunk_index]) { +if (qemu_rdma_write_one (rdma, cmd->block_index, addr, len, + errp) <0 ) { +return -1 ; + } + }else if (buffer_is_zero ((void *)(uintptr_t ) + (block->local_host_addr + off), len)) { +if (qemu_rdma_write_compress (rdma, cmd->block_index, addr, len, + errp) <0 ) { +return -1 ; + } + }else { +/* Defer: record every span for a post-registration write. */ + reg_ring_idx[regn++] = pos + n; +/* +* Registration is per whole chunk. Chunk indices arrive +* non-decreasing within a run, so a repeat chunk is the last one +* queued; add each chunk to the REGISTER batch only once. +*/ +if ((int )cmd->block_index != last_bidx || + (int64_t )cmd->chunk_index != last_cidx) { + regs[nreg].current_index = cmd->block_index; + regs[nreg].current_addr = addr; + nreg++; + last_bidx = cmd->block_index; + last_cidx = cmd->chunk_index; + } + } + n++; + } + +if (regn ==0 ) { +return (int )n; + } + +/* Phase 2: one REGISTER_REQUEST for the deduped deferred chunks. */ +if (qemu_rdma_write_register (rdma, regs, nreg, errp) <0 ) { +return -1 ; + } + +/* Phase 3: post every deferred span (chunks are now registered). */ +for (i =0 ; i < regn; i++) { +RDMACmd *cmd = &r->ring[reg_ring_idx[i] &RDMA_SEND_RING_MASK ]; +RDMALocalBlock *block; +uint64_t off, addr, len; + +rdma_get_chunk_args (rdma, cmd, &block, &off, &addr, &len); +if (qemu_rdma_write_one (rdma, cmd->block_index, addr, len, errp) <0 ) { +return -1 ; + } + } +return (int )n; +} +static int rdma_consumer_dispatch (RDMAContext *rdma,const RDMACmd *cmd,bool *stop) {
@@ -2953,8 +3004,8 @@ static int rdma_consumer_dispatch(RDMAContext *rdma, const RDMACmd *cmd,switch (cmd->type) {case RDMA_CMD_WRITE : - ret =qemu_rdma_write_chunk (rdma, cmd->block_index, - cmd->offset, cmd->length, &err); +/* WRITE runs are handled in bulk by qemu_rdma_write_run(). */ +g_assert_not_reached ();break ;case RDMA_CMD_FLUSH : ret =qemu_rdma_drain_cq (rdma, &err);
@@ -2981,6 +3032,7 @@ static void *rdma_consumer_thread(void *opaque) {RDMAContext *rdma = opaque;RDMASendRing *r = rdma->send_ring; +Error *err =NULL ;bool stop = false;rcu_register_thread ();
@@ -3025,6 +3077,21 @@ static void *rdma_consumer_thread(void *opaque)unsigned idx = r->tail + processed;RDMACmd cmd = r->ring[idx &RDMA_SEND_RING_MASK ]; +/* WRITE runs are drained in bulk with batched registration. */ +if (cmd.type ==RDMA_CMD_WRITE ) { +int n =qemu_rdma_write_run (rdma, r, idx, + batch - processed, &err); +if (n <0 ) { +if (err) { +error_report_err (err); + } +ring_set_abort (rdma); +break ; + } + processed += (unsigned )n; +continue ; + } +if (rdma_consumer_dispatch (rdma, &cmd, &stop) <0 ) {ring_set_abort (rdma);break ;
@@ -3447,6 +3514,13 @@ static void rdma_nbsent_zero(RDMAContext *rdma) rdma->nbsent_zero_hits++; } +static void rdma_reg_batch_record (RDMASendRing *r,unsigned count) +{ +if (count > r->reg_batch_max) { + r->reg_batch_max = count; + } +} +static void rdma_stats_reset (RDMAContext *rdma) { rdma->nbsent_max =0 ;
Signature · 1 line
2.39.5
RFC PATCH 12/13 migration/rdma: trace batching and register runs
In reply to Dmitrii OmelchenkoAdd trace points for the consumer batch drain, the WRITE run, and the batched REGISTER_REQUEST, and log the peak register batch size at migration end.
--- migration/rdma.c | 6 +++++- migration/trace-events | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-)
migration/rdma.c +5 -1
@@ -1940,6 +1940,7 @@ static int qemu_rdma_write_register(RDMAContext *rdma, RDMARegister *regs, head.repeat = count;rdma_reg_batch_record (rdma->send_ring, count); +trace_rdma_write_register_batch (count);if (qemu_rdma_exchange_send (rdma, &head, (uint8_t *)net, &resp, ®_result_idx,NULL , errp) <0 ) {
@@ -2970,6 +2971,7 @@ static int qemu_rdma_write_run(RDMAContext *rdma, RDMASendRing *r, n++; } +trace_rdma_write_run (n);if (regn ==0 ) {return (int )n; }
@@ -3072,6 +3074,7 @@ static void *rdma_consumer_thread(void *opaque)*/ batch = avail <RDMA_CONSUMER_BATCH_MAX ? avail :RDMA_CONSUMER_BATCH_MAX ; +trace_rdma_consumer_batch (batch, avail);while (processed < batch) {unsigned idx = r->tail + processed;
@@ -3542,7 +3545,8 @@ static void rdma_nbsent_stats_dump(RDMAContext *rdma)static void rdma_prodcons_stats_dump (RDMASendRing *r) {trace_rdma_prodcons_stats (r->idle_max_ns, r->pushwait_max_ns, - r->pushwait_total_ns, r->occupancy_max); + r->pushwait_total_ns, r->occupancy_max, + r->reg_batch_max); } int rdma_control_save_page(QEMUFile *f, ram_addr_t block_offset,
migration/trace-events +4 -1
@@ -247,10 +247,13 @@ rdma_register_odp_mr(const char *name) "Try to register On-Demand Paging memory rdma_advise_mr(const char *name, uint32_t len, uint64_t addr, const char *res) "Try to advise block %s prefetch at %" PRIu32 "@0x%" PRIx64 ": %s" rdma_resolve_host_trying(const char *host, const char *ip) "Trying %s => %s" rdma_write_flush(int sent) "sent total: %d" +rdma_write_run(uint32_t n) "consumer write run: %" PRIu32 " chunks" +rdma_write_register_batch(uint32_t count) "batch register: %" PRIu32 " chunks" rdma_write_one_post(uint64_t chunk, long addr, long remote, uint32_t len) "Posting chunk: %" PRIu64 ", addr: 0x%lx remote: 0x%lx, bytes %" PRIu32 rdma_post_send_queue_full(uint64_t wr_id, int sent) "send queue full, wr_id=%" PRIu64 ", outstanding writes=%d" rdma_nbsent_stats(uint64_t max, uint64_t posts, uint64_t full_hits, uint64_t zero_hits) "nb_sent max %" PRIu64 " posts %" PRIu64 " full-queue hits %" PRIu64 " drains to zero %" PRIu64 rdma_consumer_dispatch(int type, uint32_t bidx, uint32_t cidx, uint32_t seq) "cmd type=%d block=%" PRIu32 " chunk=%" PRIu32 " seq=%" PRIu32 +rdma_consumer_batch(uint32_t batch, uint32_t occupancy) "consumer batch: draining %" PRIu32 " of %" PRIu32 " queued" rdma_ring_wake_producer(void) "waking blocked producer" rdma_ring_wake_consumer(void) "waking idle consumer" rdma_ring_push_begin(const char *type, uint32_t chunk_index) "push begin type=%s chunk_index=%" PRIu32
@@ -262,7 +265,7 @@ rdma_consumer_thread_stop(bool aborted) "consumer thread stopped, aborted=%d" rdma_consumer_idle_begin(void) "consumer idle: waiting for commands" rdma_consumer_idle_end(int64_t idle_ns) "consumer idle: woke after %" PRId64 " ns" rdma_ram_round_notify(uint32_t occupancy) "ram round notify: ring occupancy=%" PRIu32 -rdma_prodcons_stats(uint64_t idle_max, uint64_t pushwait_max, uint64_t pushwait_total, uint64_t occupancy_max) "idle max %" PRIu64 " ns, push full-wait max %" PRIu64 " ns total %" PRIu64 " ns, ring occupancy max %" PRIu64 +rdma_prodcons_stats(uint64_t idle_max, uint64_t pushwait_max, uint64_t pushwait_total, uint64_t occupancy_max, uint64_t reg_batch_max) "idle max %" PRIu64 " ns, push full-wait max %" PRIu64 " ns total %" PRIu64 " ns, ring occupancy max %" PRIu64 ", register batch max %" PRIu64 rdma_write_one_recvregres(int mykey, int theirkey, uint64_t chunk) "Received registration result: my key: 0x%x their key 0x%x, chunk %" PRIu64 rdma_write_one_sendreg(uint64_t chunk, int len, int index, int64_t offset) "Sending registration request chunk %" PRIu64 " for %d bytes, index: %d, offset: %" PRId64 rdma_write_one_top(uint64_t chunks, uint64_t size) "Writing %" PRIu64 " chunks, (%" PRIu64 " MB)"
Signature · 1 line
2.39.5
RFC PATCH 13/13 migration/rdma: negotiate batched chunk registration
In reply to Dmitrii OmelchenkoFrom: Jack Wang <[email protected]>
An old peer echoes REGISTER_RESULT.repeat unchanged but only sends one result, so a batched source would read stale rkeys for the rest. Negotiate RDMA_CAPABILITY_REG_BATCH like PIN_ALL and cap requests at one chunk when unsupported; check resp.len instead of resp.repeat.
--- migration/rdma.c | 37 ++++++++++++++++++++++++++++++++++--- migration/trace-events | 1 + 2 files changed, 35 insertions(+), 3 deletions(-)
migration/rdma.c +34 -3
@@ -75,12 +75,15 @@ static inline uint64_t rdma_merge_max(void)* Capabilities for negotiation. */ #define RDMA_CAPABILITY_PIN_ALL 0x01 +/* Peer replies to a batched REGISTER_REQUEST with one result per chunk. */ +#define RDMA_CAPABILITY_REG_BATCH 0x02/* * Add the other flags above to this list of known capabilities * as they are introduced. */ -static uint32_t known_capabilities =RDMA_CAPABILITY_PIN_ALL ; +static uint32_t known_capabilities =RDMA_CAPABILITY_PIN_ALL | +RDMA_CAPABILITY_REG_BATCH ;/* * A work request ID is 64-bits and we split up these bits
@@ -352,6 +355,7 @@ typedef struct RDMAContext {uint64_t pend_length;/* span length in bytes (within one chunk) */ bool pin_all; +bool reg_batch;/* peer negotiated RDMA_CAPABILITY_REG_BATCH */ /* * infiniband-specific variables for opening the device
@@ -1947,6 +1951,14 @@ static int qemu_rdma_write_register(RDMAContext *rdma, RDMARegister *regs,return -1 ; } +/* resp.repeat is echoed back unchanged by old peers; resp.len is not. */ +if (resp.len != count *sizeof (RDMARegisterResult)) { +error_setg (errp,"rdma: REGISTER_RESULT size mismatch: " +"requested %u chunks (%zu bytes), peer returned %u bytes" , + count, count *sizeof (RDMARegisterResult), resp.len); +return -1 ; + } + results = (RDMARegisterResult *)rdma->wr_data[reg_result_idx].control_curr;for (i =0 ; i < count; i++) { RDMALocalBlock *block =
@@ -2172,6 +2184,8 @@ static int qemu_rdma_connect(RDMAContext *rdma, bool return_path, cap.flags |=RDMA_CAPABILITY_PIN_ALL ; } + cap.flags |=RDMA_CAPABILITY_REG_BATCH ; +caps_to_network (&cap); ret =qemu_rdma_post_recv_control (rdma,RDMA_WRID_READY , errp);
@@ -2219,6 +2233,13 @@ static int qemu_rdma_connect(RDMAContext *rdma, bool return_path, rdma->pin_all = false; } + rdma->reg_batch = !!(cap.flags &RDMA_CAPABILITY_REG_BATCH ); +if (!rdma->reg_batch) { +warn_report ("RDMA: Server does not support batched chunk " +"registration. Will register one chunk per request." ); + } +trace_rdma_connect_reg_batch_outcome (rdma->reg_batch); +trace_rdma_connect_pin_all_outcome (rdma->pin_all);rdma_ack_cm_event (cm_event);
@@ -2952,6 +2973,14 @@ static int qemu_rdma_write_run(RDMAContext *rdma, RDMASendRing *r,return -1 ; } }else { +bool new_chunk = (int )cmd->block_index != last_bidx || + (int64_t )cmd->chunk_index != last_cidx; + +/* Cap at one never-seen chunk per request without REG_BATCH. */ +if (new_chunk && nreg ==1 && !rdma->reg_batch) { +break ; + } +/* Defer: record every span for a post-registration write. */ reg_ring_idx[regn++] = pos + n;/*
@@ -2959,8 +2988,7 @@ static int qemu_rdma_write_run(RDMAContext *rdma, RDMASendRing *r,* non-decreasing within a run, so a repeat chunk is the last one * queued; add each chunk to the REGISTER batch only once. */ -if ((int )cmd->block_index != last_bidx || - (int64_t )cmd->chunk_index != last_cidx) { +if (new_chunk) { regs[nreg].current_index = cmd->block_index; regs[nreg].current_addr = addr; nreg++;
@@ -3663,6 +3691,9 @@ static int qemu_rdma_accept(RDMAContext *rdma)if (cap.flags &RDMA_CAPABILITY_PIN_ALL ) { rdma->pin_all = true; } +if (cap.flags &RDMA_CAPABILITY_REG_BATCH ) { + rdma->reg_batch = true; + } rdma->cm_id = cm_event->id; verbs = cm_event->id->verbs;
migration/trace-events +1 -0
@@ -227,6 +227,7 @@ rdma_cleanup_disconnect(void) "" rdma_close(void) "" rdma_connect_pin_all_requested(void) "" rdma_connect_pin_all_outcome(bool pin) "%d" +rdma_connect_reg_batch_outcome(bool reg_batch) "%d" rdma_dest_init_trying(const char *host, const char *ip) "%s => %s" rdma_dump_id_failed(const char *who) "%s RDMA Device opened, but can't query port information" rdma_dump_id(const char *who, const char *name, const char *dev_name, const char *dev_path, const char *ibdev_path, int transport, const char *transport_name) "%s RDMA Device opened: kernel name %s uverbs device name %s, infiniband_verbs class device path %s, infiniband class device path %s, transport: (%d) %s"
Signature · 1 line
2.39.5
Dmitrii,
With the help of GPT-5.6 Luna, I have reviewed this series.
Overall, this is a well-motivated and useful series.
The producer/consumer split addresses a real limitation in the current RDMA migration path: dirty-page scanning, RDMA posting, registration, and completion polling are currently serialized in the migration thread. Moving QP ownership and CQ processing to a dedicated consumer thread should allow dirty-page scanning and RDMA transmission to overlap.
The wire protocol compatibility is also handled appropriately. In particular, batched registration is capability-negotiated, and the source falls back to one registration request per chunk when communicating with an older destination. The ring barriers preserve ordering for RAM-round flushes, control-channel messages, registration stops, and final cleanup.
I tested the complete series on its declared base. The QEMU build and git diff --check passed, and both existing RDMA precopy tests passed:
/x86_64/migration/precopy/rdma/plain
/x86_64/migration/precopy/rdma/plain/ipv6
I also locally exercised injected errors during the migration. The source QEMU remained QMP-responsive after injected migration failures, and the migration failure and cancellation paths completed as expected.
- consumer write failure;
- registration failure;
- producer backpressure with a full ring;
- cancellation while a barrier was delayed;
- repeated error/cleanup iterations;
- ring and barrier sequence counters crossing UINT_MAX.
The overall design and performance motivation look good to me.
For the whole series
However, with the help of GPT-5.6 Luna, I found one concurrency issue in patch 4/13 that should be addressed before the series is merged. Other than that, the series is definitely moving in the right direction.
Thanks
Zhijian
Quoted 72 lines · Dmitrii Omelchenko
This is a rebase with no code changes; the prerequisites (Yanfei Xu's two RDMA series) were merged in migration-20260925-pull-request.
v1: https://lore.kernel.org/qemu-devel/[email protected]/
This series splits the RDMA live-migration send path into two threads:
- a producer thread (the migration thread) that scans dirty pages and records which chunks must be migrated, pushing chunk descriptors (block + chunk index + dirty byte span) onto a lock-free SPSC ring; and
- a consumer thread that owns the RDMA queue pair: it drains the ring, posts the RDMA writes and control messages, handles chunk registration and barriers, and polls the completion queue.Chunk-granular descriptors on the ring replace the previous per-page merge logic on the migration thread.
Patches 1-12 are the producer/consumer split. Patch 13 (Jack Wang) negotiates the batched chunk registration capability so a peer that lacks it falls back to one chunk per request.
Results
-------
64 GiB guest, single-path RDMA, memload workload (one-time RAM fill, fixed dirty set). "before" is the single-thread send path with only the nb_sent-stats patch (patch 1) applied; "after" is the full producer/consumer series, so the delta isolates the split itself.pin_all=off:
- total migration time: -16.2% (24 GiB fill), -14.2% (60 GiB fill)- transfer-phase time: -16.3% (24 GiB fill), -14.2% (60 GiB fill) - RAM throughput: +17.2% (24 GiB fill), +16.6% (60 GiB fill) - guest downtime: -55.3% (24 GiB fill), -23.1% (60 GiB fill)pin_all=on (whole-RAM registration at setup is unchanged and dominates total time, so the split shows mainly in the transfer phase):
- total migration time: -5.0%- transfer-phase time: -10.9% - transfer speed: +12.9% - guest downtime: -1.5%QP saturation was measured with the new
nb_sentsend-queue counter:
send-queue "drain to zero" events (queue starved empty) fell by ~99% in the transfer-bound pin_all=off cases (24 GiB: 26609 -> 276; 60 GiB:
63771 -> 641), while "full-queue" hits rose.This is the first step toward RDMA multipath migration.
Dmitrii Omelchenko (12):
migration/rdma: Addnb_sentsend-queue-depth stats migration/rdma: Add producer/consumer send-ring types migration/rdma: Add send-ring lifecycle (no consumer thread yet) migration/rdma: Add the send consumer thread (drain only) migration/rdma: Handle write/flush/reg-stop on the consumer migration/rdma: Extractqemu_rdma_write_file()and dispatch FILE migration/rdma: Switch the producer onto the send ring migration/rdma: Add producer/consumer tuning instrumentation migration/rdma: drain the send ring in batches migration/rdma: size reg_resp.len by the register request count migration/rdma: batch chunk registration in one round-trip migration/rdma: trace batching and register runsJack Wang (1):
migration/rdma: negotiate batched chunk registrationmigration/rdma.c | 1275 +++++++++++++++++++++++++++++----------- migration/trace-events | 18 + 2 files changed, 956 insertions(+), 337 deletions(-)base-commit:
71f25c75417d31c214ba2cd26b273c114fc99987