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
+99 -100
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).
* 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 <w_(x1,a), w_(x1,b)> 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. */
/* 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) {
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. */
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)) {
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. */
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) {
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,
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);
if (p_CCE < 0) {
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. */
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,
partner_CCE);
partner_alloc = nr_acknack_scheduling(mac, cand_ue, frame, slot,
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 (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_alloc < 0) {
reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index,
slots_per_frame, pbeam.new_beam);
continue;
}
}
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);
new_partner = cand_ue;
new_CCE = p_CCE;
new_alloc = p_alloc;
new_pbeam = pbeam;
break;
}
if (partner != NULL) {
/* Lift the leader's DMRS group count so the receiver can separate. */
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 (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]--;
}