mac: Phase 2 N-way MU-MIMO co-scheduling (up to 4 UEs/slot)

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;
<w_a, w_b> = 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
This commit is contained in:
2026-04-30 08:08:55 +09:00
parent df26524d80
commit e1f315bee9
+118 -119
View File
@@ -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). /* 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 * The 4 codewords sharing the same spatial beam (same pmi_x1) are constructed
* co-phasing (indexed by pmi_x2 in {0,1,2,3}). * with mutually orthogonal co-phasings (theta_n in {1, j, -1, -j}):
* Two codewords W1, W2 are orthogonal iff: * w_(x1, n) = (1/sqrt(4)) * [v_x1 ; j^n * v_x1]
* - same x1 (same beam) AND x2 differ by 2 (theta_n + theta_n' = 0) * Pairwise inner product <w_(x1,a), w_(x1,b)> for any a != b is zero, so all
* -> orthogonal pairs are {0,2} and {1,3} * 4 codewords with the same x1 are mutually orthogonal. Up to 4 UEs (one per
* For different x1 we conservatively don't pair (DFT vectors at adjacent indices * x2 class) can be co-scheduled simultaneously.
* are not strictly orthogonal). This catches the common case at low UE mobility *
* where multiple UEs report the same dominant beam with different polarizations. */ * 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) 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 *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; 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) if (ra->ri != 0 || rb->ri != 0)
return false; 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; static int relaxed = -1;
if (relaxed < 0) { if (relaxed < 0) {
const char *e = getenv("OAI_MU_MIMO_RELAXED"); 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; return true;
if (ra->pmi_x1 != rb->pmi_x1) if (ra->pmi_x1 != rb->pmi_x1)
return false; return false;
/* x2 in {0,1,2,3}; orthogonal pairs are (0,2) and (1,3). */ /* Same beam, distinct co-phasing -> mutually orthogonal codewords. */
return ((ra->pmi_x2 ^ rb->pmi_x2) == 2); return ra->pmi_x2 != rb->pmi_x2;
} }
static int comparator(const void *p, const void *q) 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.tb_size,
&sched_pdsch.rbSize); &sched_pdsch.rbSize);
/* nakaolab MU-MIMO: try to find an orthogonal-PMI partner UE that can ride on /* nakaolab MU-MIMO: find up to MAX_MU_MIMO_PARTNERS orthogonal-PMI partner UEs
* the same RB allocation as this UE. The partner is given a separate DCI with * that can ride on the same RB allocation. Each partner gets its own DCI/CCE,
* its own CCE, and a different DMRS CDM group so the receiver can separate * its own DMRS port (1000+layer_idx), and is forced to the leader's MCS/TBS.
* the two layers. Both UEs are forced to the same MCS/TBS. */ * Up to 4 UEs total per slot (one per orthogonal x2 class). */
NR_UE_info_t *partner = NULL; #define MAX_MU_MIMO_PARTNERS 3
int partner_CCE = -1; NR_UE_info_t *partners[MAX_MU_MIMO_PARTNERS] = {NULL};
int partner_alloc = -1; 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) { if (mumimo_enabled() && remainUEs[beam.idx] > 0) {
/* For MU-MIMO, the leader UE's DMRS group must be 2 so the second group is const uint8_t leader_x2 = iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
* available for the partner. Patch the just-built sched_pdsch in place. */ uint8_t classes_used = 1 << leader_x2;
static int diag_count = 0; static int diag_count = 0;
/* Log every Nth call to avoid log flood, but never stop logging. */
bool log_diag = ((diag_count++ % 200) == 0); bool log_diag = ((diag_count++ % 200) == 0);
if (log_diag) { if (log_diag) {
int n_cand = 0; 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) c->UE_sched_ctrl.available_dl_harq.head >= 0)
n_cand++; n_cand++;
} }
LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d ENTER partner-search after RNTI %04x; eligible_others=%d (from connected_ue_list)\n", 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, n_cand); frame, slot, iterator->UE->rnti, leader_x2, n_cand);
} }
/* nakaolab Phase 3: scan ALL connected UEs (not just UE_sched). PF often has /* Find up to MAX_MU_MIMO_PARTNERS partners, one per remaining x2 class. */
* just one UE per slot in UE_sched even with many UEs sending traffic; the while (n_partners < MAX_MU_MIMO_PARTNERS && remainUEs[beam.idx] > 0) {
* MU-MIMO partner can be any orthogonal UE that has buffered data. */ NR_UE_info_t *new_partner = NULL;
UE_iterator (UE_list, cand_ue) { int new_CCE = -1;
if (cand_ue == iterator->UE) int new_alloc = -1;
continue; NR_beam_alloc_t new_pbeam = {0};
if (cand_ue->UE_sched_ctrl.mumimo_paired_slot == (int64_t)mumimo_pf_call_id) { UE_iterator (UE_list, cand_ue) {
if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: already paired this slot\n", frame, slot, cand_ue->rnti); if (cand_ue == iterator->UE) continue;
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. */
if (!nr_mac_ue_is_active(cand_ue)) { bool already_partner = false;
if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: inactive\n", frame, slot, cand_ue->rnti); for (int i = 0; i < n_partners; i++)
continue; if (partners[i] == cand_ue) { already_partner = true; break; }
} if (already_partner) continue;
if (cand_ue->UE_sched_ctrl.available_dl_harq.head < 0) { /* Skip if cand's class is already represented (leader or earlier partner). */
if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no HARQ\n", frame, slot, cand_ue->rnti); const uint8_t cand_x2 = cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
continue; if (classes_used & (1 << cand_x2)) continue;
} if (!nr_mac_ue_is_active(cand_ue)) continue;
/* Pull RLC bytes into the partner's buffer; if no data, skip. */ if (cand_ue->UE_sched_ctrl.available_dl_harq.head < 0) continue;
update_dlsch_buffer(pp_pdsch->frame, pp_pdsch->slot, cand_ue); update_dlsch_buffer(pp_pdsch->frame, pp_pdsch->slot, cand_ue);
if (!dlsch_to_schedule(&cand_ue->UE_sched_ctrl)) { if (!dlsch_to_schedule(&cand_ue->UE_sched_ctrl)) continue;
if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: no data\n", frame, slot, cand_ue->rnti); if (!pmi_is_orthogonal(iterator->UE, cand_ue)) continue;
continue; NR_beam_alloc_t pbeam = beam_allocation_procedure(&mac->beam_info, frame, slot,
} cand_ue->UE_beam_index,
if (!pmi_is_orthogonal(iterator->UE, cand_ue)) { slots_per_frame);
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", if (pbeam.idx != beam.idx) {
frame, slot, cand_ue->rnti, reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index,
iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, slots_per_frame, pbeam.new_beam);
iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2, continue;
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); int p_CCE = get_cce_index(mac, CC_id, slot, cand_ue->rnti,
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,
&cand_ue->UE_sched_ctrl.aggregation_level, &cand_ue->UE_sched_ctrl.aggregation_level,
pbeam.idx, pbeam.idx,
cand_ue->UE_sched_ctrl.search_space, cand_ue->UE_sched_ctrl.search_space,
cand_ue->UE_sched_ctrl.coreset, cand_ue->UE_sched_ctrl.coreset,
&cand_ue->UE_sched_ctrl.sched_pdcch, &cand_ue->UE_sched_ctrl.sched_pdcch,
cand_ue->UE_sched_ctrl.pdcch_cl_adjust); cand_ue->UE_sched_ctrl.pdcch_cl_adjust);
if (partner_CCE < 0) { if (p_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);
reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index, reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index,
slots_per_frame, pbeam.new_beam); slots_per_frame, pbeam.new_beam);
continue; 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; if (new_partner == NULL) break;
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", partners[n_partners] = new_partner;
frame, slot, partner_CCEs[n_partners] = new_CCE;
iterator->UE->rnti, partner_allocs[n_partners] = new_alloc;
iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, const uint8_t p_x2 = new_partner->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2, classes_used |= 1 << p_x2;
cand_ue->rnti, n_partners++;
cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1, (void)new_pbeam; /* beam already taken via beam_allocation_procedure */
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 (partner != NULL) { if (n_partners > 0) {
/* Lift the leader's DMRS group count so the receiver can separate. */ /* 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; sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2;
} }
} }
@@ -1064,41 +1061,43 @@ static void pf_dl(gNB_MAC_INST *mac,
if (mumimo_enabled()) if (mumimo_enabled())
iterator->UE->UE_sched_ctrl.mumimo_paired_slot = (int64_t)mumimo_pf_call_id; 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. */ /* nakaolab: allocate each MU-MIMO partner on the same RBs.
if (partner != NULL) { * DMRS port assignment: leader = port 1000 (mask bit 0), partner_i = port
NR_UE_sched_ctrl_t *p_sched_ctrl = &partner->UE_sched_ctrl; * 1001+i (mask bit i+1). For 4-way: 1000 (CDM group 0), 1001 (CDM group 0),
NR_UE_DL_BWP_t *p_dl_bwp = &partner->current_DL_BWP; * 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; p_sched_ctrl->cce_index = partner_CCEs[i];
fill_pdcch_vrb_map(mac, CC_id, &p_sched_ctrl->sched_pdcch, partner_CCE, fill_pdcch_vrb_map(mac, CC_id, &p_sched_ctrl->sched_pdcch, partner_CCEs[i],
p_sched_ctrl->aggregation_level, beam.idx); 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 = { NR_sched_pdsch_t p_sched_pdsch = {
.rbStart = sched_pdsch.rbStart, .rbStart = sched_pdsch.rbStart,
.rbSize = sched_pdsch.rbSize, .rbSize = sched_pdsch.rbSize,
.mcs = sched_pdsch.mcs, /* match leader */ .mcs = sched_pdsch.mcs,
.R = sched_pdsch.R, .R = sched_pdsch.R,
.Qm = sched_pdsch.Qm, .Qm = sched_pdsch.Qm,
.tb_size = sched_pdsch.tb_size, .tb_size = sched_pdsch.tb_size,
.dl_harq_pid = p_sched_ctrl->available_dl_harq.head, .dl_harq_pid = p_sched_ctrl->available_dl_harq.head,
.pucch_allocation = partner_alloc, .pucch_allocation = partner_allocs[i],
.pm_index = get_pm_index(mac, partner, p_dl_bwp->dci_format, p_l, .pm_index = get_pm_index(mac, p_ue, p_dl_bwp->dci_format, p_l,
mac->radio_config.pdsch_AntennaPorts.XP), mac->radio_config.pdsch_AntennaPorts.XP),
.nrOfLayers = p_l, .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), .dmrs_parms = get_dl_dmrs_params(scc, p_dl_bwp, &tda_info, p_l),
.time_domain_allocation = tda, .time_domain_allocation = tda,
.tda_info = tda_info, .tda_info = tda_info,
.action = NULL, .action = NULL,
}; };
/* Partner uses DMRS CDM group 1 (port 1001). dmrs_ports_id is a bitmask; /* Partner i (i in 0..2) uses DMRS port 1001+i: mask bit (i+1). */
* 2 = bit 1 set -> uses the second port pair. numDmrsCdmGrpsNoData=2 is p_sched_pdsch.dmrs_parms.dmrs_ports_id = 1u << (i + 1);
* required so DMRS pilots from both groups are reserved. */
p_sched_pdsch.dmrs_parms.dmrs_ports_id = 2;
p_sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2; 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; p_sched_ctrl->mumimo_paired_slot = (int64_t)mumimo_pf_call_id;
remainUEs[beam.idx]--; remainUEs[beam.idx]--;
} }