mac: MU-MIMO DL co-scheduling for orthogonal-PMI UEs (nakaolab)

Adds opt-in MU-MIMO downlink co-scheduling to the production
proportional-fair scheduler (pf_dl). When OAI_MU_MIMO=1 is set in
the gNB process env, after the leader UE is allocated we look ahead
in the PF-sorted candidate list for an orthogonal-PMI partner. If
found and the partner can secure its own CCE+PUCCH, both UEs share
the same RB allocation with distinct DMRS CDM groups (port 1000 vs
port 1001) and are forced to the same MCS/TBS.

Orthogonality test (4-port Type I rank-1 codebook, N1=2, N2=1):
same x1 (same beam), x2 differing by 2 (co-phasing pairs {0,2} or
{1,3}). Both UEs must report rank-1.

Default OFF — set OAI_MU_MIMO=1 to enable. Builds on the upstream
FAPI/PHY MU-MIMO plumbing (commits 3707d52c95, 498d241d75,
9952996c5d) which carries antenna-port and DMRS-CDM metadata down
to the PHY transmit path.
This commit is contained in:
2026-04-29 13:09:30 +09:00
parent d7d67277f3
commit 766d0a4bd3
@@ -593,8 +593,46 @@ typedef struct UEsched_s {
float coef;
NR_UE_info_t * UE;
int selected_mcs;
bool mumimo_paired; /* nakaolab: already allocated as MU-MIMO partner; main loop must skip */
} UEsched_t;
/* nakaolab: cached MU-MIMO enable state.
* Read once via OAI_MU_MIMO env var. 0 = SU-MIMO (default), 1 = MU-MIMO co-scheduling. */
static int mumimo_enabled(void)
{
static int cached = -1;
if (cached < 0) {
const char *e = getenv("OAI_MU_MIMO");
cached = (e && atoi(e) > 0) ? 1 : 0;
if (cached)
LOG_W(NR_MAC, "[nakaolab] MU-MIMO downlink scheduling ENABLED (OAI_MU_MIMO=1)\n");
}
return cached;
}
/* 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. */
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;
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);
}
static int comparator(const void *p, const void *q)
{
const UEsched_t *pp = p;
@@ -735,6 +773,12 @@ static void pf_dl(gNB_MAC_INST *mac,
/* Loop UE_sched to find max coeff and allocate transmission */
while (iterator->UE != NULL) {
/* nakaolab: skip UEs already allocated as a MU-MIMO partner of an earlier UE */
if (iterator->mumimo_paired) {
iterator++;
continue;
}
NR_UE_sched_ctrl_t *sched_ctrl = &iterator->UE->UE_sched_ctrl;
const uint16_t rnti = iterator->UE->rnti;
@@ -877,8 +921,122 @@ 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 (we trust the leader
* UE's PF coefficient and channel report). */
UEsched_t *partner = NULL;
int partner_CCE = -1;
int partner_alloc = -1;
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. */
for (UEsched_t *cand = iterator + 1; cand->UE != NULL; cand++) {
if (cand->mumimo_paired)
continue;
if (cand->UE->UE_sched_ctrl.available_dl_harq.head < 0)
continue;
if (!pmi_is_orthogonal(iterator->UE, cand->UE))
continue;
/* Candidate's beam must match ours so the precoding is on the same TRX
* resources. (Different beams would also work but our config has a
* single beam pool.) */
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;
}
/* 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,
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) {
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) {
reset_beam_status(&mac->beam_info, frame, slot, cand->UE->UE_beam_index,
slots_per_frame, pbeam.new_beam);
continue;
}
}
partner = cand;
LOG_D(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 (partner != NULL) {
/* Lift the leader's DMRS group count so the receiver can separate. */
sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2;
}
}
post_process_dlsch(mac, pp_pdsch, iterator->UE, &sched_pdsch);
/* nakaolab: now allocate the MU-MIMO partner on the same RBs. */
if (partner != NULL) {
NR_UE_sched_ctrl_t *p_sched_ctrl = &partner->UE->UE_sched_ctrl;
NR_UE_DL_BWP_t *p_dl_bwp = &partner->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->aggregation_level, beam.idx);
const int p_l = 1; /* MU-MIMO mode: rank-1 per UE */
NR_sched_pdsch_t p_sched_pdsch = {
.rbStart = sched_pdsch.rbStart,
.rbSize = sched_pdsch.rbSize,
.mcs = sched_pdsch.mcs, /* match leader */
.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->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->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;
p_sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2;
post_process_dlsch(mac, pp_pdsch, partner->UE, &p_sched_pdsch);
partner->mumimo_paired = true;
remainUEs[beam.idx]--;
}
/* transmissions: directly allocate */
n_rb_sched[beam.idx] -= sched_pdsch.rbSize;