From 766d0a4bd396cecc6971ca7b995bdd2fe7ef7893 Mon Sep 17 00:00:00 2001 From: makigo Date: Wed, 29 Apr 2026 13:09:30 +0900 Subject: [PATCH] mac: MU-MIMO DL co-scheduling for orthogonal-PMI UEs (nakaolab) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c index c65df50e80..03acd3aa7f 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c @@ -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;