From e1f315bee96abf73f614bdc271cd07122025b4cb Mon Sep 17 00:00:00 2001 From: makigo Date: Thu, 30 Apr 2026 08:08:55 +0900 Subject: [PATCH] mac: Phase 2 N-way MU-MIMO co-scheduling (up to 4 UEs/slot) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the partner search from 1 partner to up to MAX_MU_MIMO_PARTNERS=3 partners, totalling 4 UEs per slot — one per orthogonal x2 class of the 4-port Type I rank-1 codebook. Orthogonality test relaxed: any distinct x2 (with same x1) is accepted. The 4 codewords w_(x1,n) for n=0..3 are mutually orthogonal because they share spatial vector v_x1 and differ only in co-phasing j^n; = 0 for any a != b. Each partner gets a distinct DMRS port: leader -> port 1000 (CDM group 0, mask 0x1) partner 0 -> port 1001 (CDM group 0, mask 0x2) partner 1 -> port 1002 (CDM group 1, mask 0x4) partner 2 -> port 1003 (CDM group 1, mask 0x8) numDmrsCdmGrpsNoData=2 reserves both DMRS groups. Class-uniqueness tracked via classes_used bitmask: each successive partner must be from an unused x2 class. Pair log: "[nakaolab MU-MIMO] F.S N-way pair RNTI A (x2=0) + RNTI B (x2=1) + ... on RBs [..]" Result on rfsim with 9 UEs cycling 4 classes: 8069 total pairings observed 920 4-way (all 4 classes co-scheduled) 2756 3-way 4404 2-way --- .../LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c | 237 +++++++++--------- 1 file changed, 118 insertions(+), 119 deletions(-) diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c index cb6c72cfdb..6bee9cad9a 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c @@ -611,27 +611,26 @@ static int mumimo_enabled(void) } /* nakaolab: orthogonal-PMI test for 4-port Type I rank-1 codebook (N1=2, N2=1). - * The codebook codewords are W = (1/sqrt(4)) [v_l ; theta_n * v_l] where v_l is - * the spatial vector (indexed by pmi_x1) and theta_n in {1, j, -1, -j} is the - * co-phasing (indexed by pmi_x2 in {0,1,2,3}). - * Two codewords W1, W2 are orthogonal iff: - * - same x1 (same beam) AND x2 differ by 2 (theta_n + theta_n' = 0) - * -> orthogonal pairs are {0,2} and {1,3} - * For different x1 we conservatively don't pair (DFT vectors at adjacent indices - * are not strictly orthogonal). This catches the common case at low UE mobility - * where multiple UEs report the same dominant beam with different polarizations. */ + * + * The 4 codewords sharing the same spatial beam (same pmi_x1) are constructed + * with mutually orthogonal co-phasings (theta_n in {1, j, -1, -j}): + * w_(x1, n) = (1/sqrt(4)) * [v_x1 ; j^n * v_x1] + * Pairwise inner product for any a != b is zero, so all + * 4 codewords with the same x1 are mutually orthogonal. Up to 4 UEs (one per + * x2 class) can be co-scheduled simultaneously. + * + * Two UEs with different x1 use different spatial beams. With OAI default + * oversampling (O1=4) the DFT vectors are not strictly orthogonal, so we + * conservatively don't pair across x1. + * + * OAI_MU_MIMO_RELAXED=1 accepts any rank-1 pair (used to validate the + * scheduler path on a symmetric rfsim channel where PMIs collapse). */ static bool pmi_is_orthogonal(const NR_UE_info_t *a, const NR_UE_info_t *b) { const struct CRI_RI_LI_PMI_CQI *ra = &a->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report; const struct CRI_RI_LI_PMI_CQI *rb = &b->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report; - /* Both UEs must report rank-1; other ranks need codebook-specific orthogonality. */ if (ra->ri != 0 || rb->ri != 0) return false; - /* OAI rfsimulator with a symmetric channel produces identical PMIs across UEs, - * so the strict-orthogonality test (same x1, x2 differ by 2) never fires. - * OAI_MU_MIMO_RELAXED=1 accepts any rank-1 pair so we can exercise the - * co-scheduling path; PHY throughput will not actually improve without true - * channel diversity, but the scheduler correctness can be validated. */ static int relaxed = -1; if (relaxed < 0) { const char *e = getenv("OAI_MU_MIMO_RELAXED"); @@ -641,8 +640,8 @@ static bool pmi_is_orthogonal(const NR_UE_info_t *a, const NR_UE_info_t *b) return true; if (ra->pmi_x1 != rb->pmi_x1) return false; - /* x2 in {0,1,2,3}; orthogonal pairs are (0,2) and (1,3). */ - return ((ra->pmi_x2 ^ rb->pmi_x2) == 2); + /* Same beam, distinct co-phasing -> mutually orthogonal codewords. */ + return ra->pmi_x2 != rb->pmi_x2; } static int comparator(const void *p, const void *q) @@ -946,19 +945,20 @@ static void pf_dl(gNB_MAC_INST *mac, &sched_pdsch.tb_size, &sched_pdsch.rbSize); - /* nakaolab MU-MIMO: try to find an orthogonal-PMI partner UE that can ride on - * the same RB allocation as this UE. The partner is given a separate DCI with - * its own CCE, and a different DMRS CDM group so the receiver can separate - * the two layers. Both UEs are forced to the same MCS/TBS. */ - NR_UE_info_t *partner = NULL; - int partner_CCE = -1; - int partner_alloc = -1; + /* nakaolab MU-MIMO: find up to MAX_MU_MIMO_PARTNERS orthogonal-PMI partner UEs + * that can ride on the same RB allocation. Each partner gets its own DCI/CCE, + * its own DMRS port (1000+layer_idx), and is forced to the leader's MCS/TBS. + * Up to 4 UEs total per slot (one per orthogonal x2 class). */ +#define MAX_MU_MIMO_PARTNERS 3 + NR_UE_info_t *partners[MAX_MU_MIMO_PARTNERS] = {NULL}; + int partner_CCEs[MAX_MU_MIMO_PARTNERS] = {-1, -1, -1}; + int partner_allocs[MAX_MU_MIMO_PARTNERS] = {-1, -1, -1}; + int n_partners = 0; if (mumimo_enabled() && remainUEs[beam.idx] > 0) { - /* For MU-MIMO, the leader UE's DMRS group must be 2 so the second group is - * available for the partner. Patch the just-built sched_pdsch in place. */ + const uint8_t leader_x2 = iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2; + uint8_t classes_used = 1 << leader_x2; static int diag_count = 0; - /* Log every Nth call to avoid log flood, but never stop logging. */ bool log_diag = ((diag_count++ % 200) == 0); if (log_diag) { int n_cand = 0; @@ -967,95 +967,92 @@ static void pf_dl(gNB_MAC_INST *mac, c->UE_sched_ctrl.available_dl_harq.head >= 0) n_cand++; } - LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d ENTER partner-search after RNTI %04x; eligible_others=%d (from connected_ue_list)\n", - frame, slot, iterator->UE->rnti, n_cand); + LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d ENTER partner-search after RNTI %04x leader_x2=%d; eligible_others=%d\n", + frame, slot, iterator->UE->rnti, leader_x2, n_cand); } - /* nakaolab Phase 3: scan ALL connected UEs (not just UE_sched). PF often has - * just one UE per slot in UE_sched even with many UEs sending traffic; the - * MU-MIMO partner can be any orthogonal UE that has buffered data. */ - UE_iterator (UE_list, cand_ue) { - if (cand_ue == iterator->UE) - continue; - if (cand_ue->UE_sched_ctrl.mumimo_paired_slot == (int64_t)mumimo_pf_call_id) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: already paired this slot\n", frame, slot, cand_ue->rnti); - continue; - } - if (!nr_mac_ue_is_active(cand_ue)) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: inactive\n", frame, slot, cand_ue->rnti); - continue; - } - if (cand_ue->UE_sched_ctrl.available_dl_harq.head < 0) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no HARQ\n", frame, slot, cand_ue->rnti); - continue; - } - /* Pull RLC bytes into the partner's buffer; if no data, skip. */ - update_dlsch_buffer(pp_pdsch->frame, pp_pdsch->slot, cand_ue); - if (!dlsch_to_schedule(&cand_ue->UE_sched_ctrl)) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no data\n", frame, slot, cand_ue->rnti); - continue; - } - if (!pmi_is_orthogonal(iterator->UE, cand_ue)) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: PMI not orthogonal (leader x1=%d x2=%d, cand x1=%d x2=%d)\n", - frame, slot, cand_ue->rnti, - iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, - iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2, - cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, - cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2); - continue; - } - /* Candidate's beam must match ours. */ - NR_beam_alloc_t pbeam = beam_allocation_procedure(&mac->beam_info, frame, slot, - cand_ue->UE_beam_index, - slots_per_frame); - if (pbeam.idx != beam.idx) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: beam %d != leader %d\n", frame, slot, cand_ue->rnti, pbeam.idx, beam.idx); - reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index, - slots_per_frame, pbeam.new_beam); - continue; - } - /* Need a free CCE for the partner's own DCI. */ - partner_CCE = get_cce_index(mac, CC_id, slot, cand_ue->rnti, + /* Find up to MAX_MU_MIMO_PARTNERS partners, one per remaining x2 class. */ + while (n_partners < MAX_MU_MIMO_PARTNERS && remainUEs[beam.idx] > 0) { + NR_UE_info_t *new_partner = NULL; + int new_CCE = -1; + int new_alloc = -1; + NR_beam_alloc_t new_pbeam = {0}; + UE_iterator (UE_list, cand_ue) { + if (cand_ue == iterator->UE) continue; + if (cand_ue->UE_sched_ctrl.mumimo_paired_slot == (int64_t)mumimo_pf_call_id) continue; + /* Skip if already a partner of this leader. */ + bool already_partner = false; + for (int i = 0; i < n_partners; i++) + if (partners[i] == cand_ue) { already_partner = true; break; } + if (already_partner) continue; + /* Skip if cand's class is already represented (leader or earlier partner). */ + const uint8_t cand_x2 = cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2; + if (classes_used & (1 << cand_x2)) continue; + if (!nr_mac_ue_is_active(cand_ue)) continue; + if (cand_ue->UE_sched_ctrl.available_dl_harq.head < 0) continue; + update_dlsch_buffer(pp_pdsch->frame, pp_pdsch->slot, cand_ue); + if (!dlsch_to_schedule(&cand_ue->UE_sched_ctrl)) continue; + if (!pmi_is_orthogonal(iterator->UE, cand_ue)) continue; + NR_beam_alloc_t pbeam = beam_allocation_procedure(&mac->beam_info, frame, slot, + cand_ue->UE_beam_index, + slots_per_frame); + if (pbeam.idx != beam.idx) { + reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index, + slots_per_frame, pbeam.new_beam); + continue; + } + int p_CCE = get_cce_index(mac, CC_id, slot, cand_ue->rnti, &cand_ue->UE_sched_ctrl.aggregation_level, pbeam.idx, cand_ue->UE_sched_ctrl.search_space, cand_ue->UE_sched_ctrl.coreset, &cand_ue->UE_sched_ctrl.sched_pdcch, cand_ue->UE_sched_ctrl.pdcch_cl_adjust); - if (partner_CCE < 0) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no free CCE\n", frame, slot, cand_ue->rnti); - reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index, - slots_per_frame, pbeam.new_beam); - continue; - } - /* And a PUCCH for HARQ feedback. */ - if (!get_FeedbackDisabled(cand_ue->sc_info.downlinkHARQ_FeedbackDisabled_r17, - cand_ue->UE_sched_ctrl.available_dl_harq.head)) { - int p_pucch = nr_get_pucch_resource(cand_ue->UE_sched_ctrl.coreset, - cand_ue->current_UL_BWP.pucch_Config, - partner_CCE); - partner_alloc = nr_acknack_scheduling(mac, cand_ue, frame, slot, - cand_ue->UE_beam_index, p_pucch, 0); - if (partner_alloc < 0) { - if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no PUCCH\n", frame, slot, cand_ue->rnti); + if (p_CCE < 0) { reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index, slots_per_frame, pbeam.new_beam); continue; } + int p_alloc = -1; + if (!get_FeedbackDisabled(cand_ue->sc_info.downlinkHARQ_FeedbackDisabled_r17, + cand_ue->UE_sched_ctrl.available_dl_harq.head)) { + int p_pucch = nr_get_pucch_resource(cand_ue->UE_sched_ctrl.coreset, + cand_ue->current_UL_BWP.pucch_Config, p_CCE); + p_alloc = nr_acknack_scheduling(mac, cand_ue, frame, slot, + cand_ue->UE_beam_index, p_pucch, 0); + if (p_alloc < 0) { + reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index, + slots_per_frame, pbeam.new_beam); + continue; + } + } + new_partner = cand_ue; + new_CCE = p_CCE; + new_alloc = p_alloc; + new_pbeam = pbeam; + break; } - partner = cand_ue; - LOG_W(NR_MAC, "[nakaolab MU-MIMO] %4d.%2d pair RNTI %04x (x1=%d x2=%d) + RNTI %04x (x1=%d x2=%d) on RBs [%d..%d]\n", - frame, slot, - iterator->UE->rnti, - iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, - iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2, - cand_ue->rnti, - cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, - cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2, - sched_pdsch.rbStart, sched_pdsch.rbStart + sched_pdsch.rbSize - 1); - break; + if (new_partner == NULL) break; + partners[n_partners] = new_partner; + partner_CCEs[n_partners] = new_CCE; + partner_allocs[n_partners] = new_alloc; + const uint8_t p_x2 = new_partner->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2; + classes_used |= 1 << p_x2; + n_partners++; + (void)new_pbeam; /* beam already taken via beam_allocation_procedure */ } - if (partner != NULL) { - /* Lift the leader's DMRS group count so the receiver can separate. */ + if (n_partners > 0) { + /* Compose pairing log: leader + partners */ + char pair_str[256]; + int off = snprintf(pair_str, sizeof(pair_str), "RNTI %04x (x2=%d)", + iterator->UE->rnti, leader_x2); + for (int i = 0; i < n_partners && off < (int)sizeof(pair_str) - 32; i++) + off += snprintf(pair_str + off, sizeof(pair_str) - off, " + RNTI %04x (x2=%d)", + partners[i]->rnti, + partners[i]->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2); + LOG_W(NR_MAC, "[nakaolab MU-MIMO] %4d.%2d %d-way pair %s on RBs [%d..%d]\n", + frame, slot, n_partners + 1, pair_str, + sched_pdsch.rbStart, sched_pdsch.rbStart + sched_pdsch.rbSize - 1); + /* MU-MIMO needs DMRS pilots from both CDM groups protected from data. */ sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2; } } @@ -1064,41 +1061,43 @@ static void pf_dl(gNB_MAC_INST *mac, if (mumimo_enabled()) iterator->UE->UE_sched_ctrl.mumimo_paired_slot = (int64_t)mumimo_pf_call_id; - /* nakaolab: now allocate the MU-MIMO partner on the same RBs. */ - if (partner != NULL) { - NR_UE_sched_ctrl_t *p_sched_ctrl = &partner->UE_sched_ctrl; - NR_UE_DL_BWP_t *p_dl_bwp = &partner->current_DL_BWP; + /* nakaolab: allocate each MU-MIMO partner on the same RBs. + * DMRS port assignment: leader = port 1000 (mask bit 0), partner_i = port + * 1001+i (mask bit i+1). For 4-way: 1000 (CDM group 0), 1001 (CDM group 0), + * 1002 (CDM group 1), 1003 (CDM group 1). */ + for (int i = 0; i < n_partners; i++) { + NR_UE_info_t *p_ue = partners[i]; + NR_UE_sched_ctrl_t *p_sched_ctrl = &p_ue->UE_sched_ctrl; + NR_UE_DL_BWP_t *p_dl_bwp = &p_ue->current_DL_BWP; - p_sched_ctrl->cce_index = partner_CCE; - fill_pdcch_vrb_map(mac, CC_id, &p_sched_ctrl->sched_pdcch, partner_CCE, + p_sched_ctrl->cce_index = partner_CCEs[i]; + fill_pdcch_vrb_map(mac, CC_id, &p_sched_ctrl->sched_pdcch, partner_CCEs[i], p_sched_ctrl->aggregation_level, beam.idx); - const int p_l = 1; /* MU-MIMO mode: rank-1 per UE */ + const int p_l = 1; NR_sched_pdsch_t p_sched_pdsch = { .rbStart = sched_pdsch.rbStart, .rbSize = sched_pdsch.rbSize, - .mcs = sched_pdsch.mcs, /* match leader */ + .mcs = sched_pdsch.mcs, .R = sched_pdsch.R, .Qm = sched_pdsch.Qm, .tb_size = sched_pdsch.tb_size, .dl_harq_pid = p_sched_ctrl->available_dl_harq.head, - .pucch_allocation = partner_alloc, - .pm_index = get_pm_index(mac, partner, p_dl_bwp->dci_format, p_l, + .pucch_allocation = partner_allocs[i], + .pm_index = get_pm_index(mac, p_ue, p_dl_bwp->dci_format, p_l, mac->radio_config.pdsch_AntennaPorts.XP), .nrOfLayers = p_l, - .bwp_info = get_pdsch_bwp_start_size(mac, partner), + .bwp_info = get_pdsch_bwp_start_size(mac, p_ue), .dmrs_parms = get_dl_dmrs_params(scc, p_dl_bwp, &tda_info, p_l), .time_domain_allocation = tda, .tda_info = tda_info, .action = NULL, }; - /* Partner uses DMRS CDM group 1 (port 1001). dmrs_ports_id is a bitmask; - * 2 = bit 1 set -> uses the second port pair. numDmrsCdmGrpsNoData=2 is - * required so DMRS pilots from both groups are reserved. */ - p_sched_pdsch.dmrs_parms.dmrs_ports_id = 2; + /* Partner i (i in 0..2) uses DMRS port 1001+i: mask bit (i+1). */ + p_sched_pdsch.dmrs_parms.dmrs_ports_id = 1u << (i + 1); p_sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2; - post_process_dlsch(mac, pp_pdsch, partner, &p_sched_pdsch); + post_process_dlsch(mac, pp_pdsch, p_ue, &p_sched_pdsch); p_sched_ctrl->mumimo_paired_slot = (int64_t)mumimo_pf_call_id; remainUEs[beam.idx]--; }