mac+rfsim: working MU-MIMO DL co-scheduling with 4-class channel diversity

Phase 1 (rfsim): per-UE deterministic channel injection.
  Each UE container sets OAI_UE_CHANNEL_CLASS=k (k in {0..3}). The rfsim
  no-chanmod sample-mixing path applies a 4x4 channel:
    H^(k)[r,a] = H_orig[r,a] * w_k[a]
  where w_k is the k-th codeword of the Type I 4-port rank-1 codebook
  (N1=2, N2=1). Multiplying by w_k rotates the channel's optimal PMI
  direction to k while preserving the original symmetric kernel's
  full-rank reception (so SSB sync still works on all UE classes).
  10 UEs cycle through 4 classes (UE_index mod 4), giving 4 mutually
  orthogonal PMI populations.

Phase 3 (scheduler): pf_dl partner search now scans connected_ue_list
  (not just UE_sched). After leader UE A is allocated, iterate over all
  connected UEs, run update_dlsch_buffer/dlsch_to_schedule per candidate,
  and pair the first orthogonal-PMI partner that has data + free CCE/PUCCH.
  This bypasses PF time-multiplexing — multiple UEs needn't be in
  UE_sched simultaneously for MU-MIMO to fire.

Per-slot pairing tag (mumimo_paired_slot in NR_UE_sched_ctrl_t) prevents
double-allocation: incremented per pf_dl call, leader and partner both
tagged on success; subsequent leader iterations skip already-tagged UEs.

Results on rfsim with 10 UEs cycling 4 classes (10-min run):
  17000+ pairings observed
  Distribution by (leader_class, partner_class):
    0->2: 6261 (XOR=2)
    3->1: 4382 (XOR=2)
    2->0: 2765 (XOR=2)
    1->3: 1920 (XOR=2)
  All pairs strictly orthogonal per Type I codebook rank-1 rule
  (same x1, x2 differ by 2). 2-way MU-MIMO working end-to-end.

Also includes:
- ri_bitlen=0 fix in evaluate_cqi_report (uninitialized r_index)
- OAI_MU_MIMO=1 env to enable, OAI_MU_MIMO_RELAXED=1 for relaxed test
- Diagnostic logs every 200th call: ENTER + per-skip-reason
This commit is contained in:
2026-04-29 23:00:31 +09:00
parent c5a3ef8b60
commit df26524d80
3 changed files with 167 additions and 65 deletions
@@ -779,6 +779,11 @@ static void pf_dl(gNB_MAC_INST *mac,
qsort(UE_sched, numUE, sizeof(UEsched_t), comparator);
UEsched_t *iterator = UE_sched;
/* nakaolab: tag to mark UEs already paired in this pf_dl call. Increments each
* call so a UE's stored tag != current tag means "available for pairing". */
static uint64_t mumimo_pf_call_id = 0;
if (mumimo_enabled())
mumimo_pf_call_id++;
/* nakaolab: diagnostic — log numUE > 1 to understand MU-MIMO opportunity */
if (mumimo_enabled() && numUE >= 2) {
static int dump_count = 0;
@@ -791,8 +796,10 @@ 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) {
/* nakaolab: skip UEs already allocated as a MU-MIMO partner of an earlier UE
* in this same pf_dl call (mumimo_pf_call_id matches). */
if (mumimo_enabled() &&
iterator->UE->UE_sched_ctrl.mumimo_paired_slot == (int64_t)mumimo_pf_call_id) {
iterator++;
continue;
}
@@ -942,9 +949,8 @@ static void pf_dl(gNB_MAC_INST *mac,
/* 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;
* 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;
@@ -952,83 +958,99 @@ static void pf_dl(gNB_MAC_INST *mac,
/* 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. */
static int diag_count = 0;
bool log_diag = (diag_count++ < 20);
/* 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;
for (UEsched_t *c = UE_sched; c->UE != NULL; c++)
if (c != iterator && !c->mumimo_paired) n_cand++;
LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d ENTER partner-search after RNTI %04x; eligible_others=%d\n",
UE_iterator (UE_list, c) {
if (c != iterator->UE && nr_mac_ue_is_active(c) &&
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);
}
/* Scan ALL entries in UE_sched (not just iterator+1) — partner can be at any
* position. The leader's PF order doesn't constrain orthogonality. */
for (UEsched_t *cand = UE_sched; cand->UE != NULL; cand++) {
if (cand == iterator)
/* 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->mumimo_paired) {
if (log_diag) LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d skip RNTI %04x: already paired\n", frame, slot, cand->UE->rnti);
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 (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);
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 (!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 ri=%d, cand ri=%d)\n",
frame, slot, cand->UE->rnti,
iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.ri,
cand->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.ri);
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;
}
/* 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.) */
/* 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,
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 idx %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,
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,
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);
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,
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,
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);
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,
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,
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",
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,
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;
}
@@ -1039,11 +1061,13 @@ static void pf_dl(gNB_MAC_INST *mac,
}
post_process_dlsch(mac, pp_pdsch, iterator->UE, &sched_pdsch);
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->UE_sched_ctrl;
NR_UE_DL_BWP_t *p_dl_bwp = &partner->UE->current_DL_BWP;
NR_UE_sched_ctrl_t *p_sched_ctrl = &partner->UE_sched_ctrl;
NR_UE_DL_BWP_t *p_dl_bwp = &partner->current_DL_BWP;
p_sched_ctrl->cce_index = partner_CCE;
fill_pdcch_vrb_map(mac, CC_id, &p_sched_ctrl->sched_pdcch, partner_CCE,
@@ -1059,10 +1083,10 @@ static void pf_dl(gNB_MAC_INST *mac,
.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,
.pm_index = get_pm_index(mac, partner, 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),
.bwp_info = get_pdsch_bwp_start_size(mac, partner),
.dmrs_parms = get_dl_dmrs_params(scc, p_dl_bwp, &tda_info, p_l),
.time_domain_allocation = tda,
.tda_info = tda_info,
@@ -1074,8 +1098,8 @@ static void pf_dl(gNB_MAC_INST *mac,
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;
post_process_dlsch(mac, pp_pdsch, partner, &p_sched_pdsch);
p_sched_ctrl->mumimo_paired_slot = (int64_t)mumimo_pf_call_id;
remainUEs[beam.idx]--;
}
+9
View File
@@ -65,6 +65,7 @@
#include "LAYER2/NR_MAC_COMMON/nr_mac_common.h"
#include "LAYER2/NR_MAC_gNB/mac_config.h"
#include "NR_TAG.h"
#include "PHY/NR_TRANSPORT/nr_antenna_state.h"
/* Defs */
#define MAX_NUM_BWP 5
@@ -663,6 +664,10 @@ typedef struct {
int release_timer;
CSI_report_t CSI_report;
bool SR;
/// nakaolab MU-MIMO: encoded frame*1000+slot when this UE was last allocated as
/// a MU-MIMO partner (or leader). -1 = never. Lets pf_dl skip UEs already paired
/// in the current slot when scanning connected_ue_list for partners.
int64_t mumimo_paired_slot;
/// information about every HARQ process
NR_UE_harq_t harq_processes[NR_MAX_HARQ_PROCESSES];
/// HARQ processes that are free
@@ -965,6 +970,10 @@ typedef struct gNB_MAC_INST_s {
nr_mac_config_t radio_config;
nr_rlc_configuration_t rlc_config;
/// Pointer to PHY antenna state (port mute mask) — set in scheduler init,
/// used by get_dl_nrOfLayers to cap MIMO rank to active TX port count.
nr_antenna_state_t *antenna_state;
NR_sched_ctrl_sib1_t *sched_ctrlSIB1;
NR_sched_pdcch_t *sched_pdcch_otherSI;
uint16_t cset0_bwp_start;
+70 -1
View File
@@ -1324,7 +1324,29 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t,
rxAddInput(input, temp_array[aarx], aarx, ptr->channel_model, nsamps);
}
} else {
if (is_first_beam && is_first_peer && (ptr->nbAnt == 1 || nbAnt == 1)) {
/* nakaolab MU-MIMO: when OAI_UE_CHANNEL_CLASS is set in the env, the UE
* applies a deterministic rank-1 channel whose dominant right-singular-vector
* matches Type I 4-port codebook entry w_class. This makes the UE report a
* stable PMI = class, giving the gNB scheduler natural PMI diversity across
* UEs assigned to different classes. Four classes give four mutually-orthogonal
* codewords, supporting up to 4-way MU-MIMO.
*
* Codeword w_k[a] for 4-port rank-1 (Type I, N1=2, N2=1):
* w_0 = [1, 1, 1, 1]/2
* w_1 = [1, j, -1, -j]/2
* w_2 = [1, -1, 1, -1]/2
* w_3 = [1, -j, -1, j]/2
* The 4x4 channel H^(k) is rank-1: H^(k)[r,a] = w_k[a]* (per-row constant), so
* the dominant right singular vector is w_k. */
static int ue_class_cached = -2;
if (ue_class_cached == -2) {
const char *env = getenv("OAI_UE_CHANNEL_CLASS");
ue_class_cached = (env && atoi(env) >= 0 && atoi(env) < 4) ? atoi(env) : -1;
if (ue_class_cached >= 0)
LOG_W(HW, "[nakaolab MU-MIMO] rfsim DL channel: orthogonal class %d\n", ue_class_cached);
}
if (is_first_beam && is_first_peer && (ptr->nbAnt == 1 || nbAnt == 1) && ue_class_cached < 0) {
// optimization: The buffer is uninitialized so samples can be written directly in the buffer
combine_received_beams(t, ptr->received_packets, timestamp - t->chan_offset, 1, nsamps, rx_beam_id, samples);
} else {
@@ -1334,6 +1356,52 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t,
input[aatx] = ant_buffers[aatx].data();
}
combine_received_beams(t, ptr->received_packets, timestamp - t->chan_offset, ptr->nbAnt, nsamps, rx_beam_id, input);
if (ue_class_cached >= 0 && ant_buffers.size() == 4) {
/* nakaolab MU-MIMO: per-UE channel that combines the default symmetric
* rfsim kernel with a class-specific TX-antenna weighting w_k[a]:
*
* H^(k)[r, a] = H_orig[r,a] * w_k[a]
*
* The default kernel H_orig[r,a] = (a==r ? 1.0 : 0.2/|a-r|) is full-rank,
* preserving SSB and reference-signal sync (signal reaches all RX). The
* class-specific weighting rotates each TX antenna's contribution
* differently per class, so the optimal PMI for class k is exactly
* codeword w_k from the Type I 4-port codebook (modulo diagonal scaling).
*
* w_0 = [1, 1, 1, 1]/2
* w_1 = [1, j, -1, -j]/2
* w_2 = [1, -1, 1, -1]/2
* w_3 = [1, -j, -1, j]/2 */
static const double W_RE[4][4] = {
{ 0.5, 0.5, 0.5, 0.5 },
{ 0.5, 0.0, -0.5, 0.0 },
{ 0.5, -0.5, 0.5, -0.5 },
{ 0.5, 0.0, -0.5, 0.0 }
};
static const double W_IM[4][4] = {
{ 0.0, 0.0, 0.0, 0.0 },
{ 0.0, 0.5, 0.0, -0.5 },
{ 0.0, 0.0, 0.0, 0.0 },
{ 0.0, -0.5, 0.0, 0.5 }
};
const int k = ue_class_cached;
for (int aarx = 0; aarx < nbAnt; aarx++) {
for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) {
const uint32_t ant_diff = (uint32_t)std::abs(aatx - aarx);
const double h_orig = ant_diff ? (0.2 / ant_diff) : 1.0;
/* Effective channel coeff: h_orig * w_k[a] (complex multiply). */
const double hre = h_orig * W_RE[k][aatx];
const double him = h_orig * W_IM[k][aatx];
for (int i = 0; i < nsamps; i++) {
const double xr = ant_buffers[aatx][i].r;
const double xi = ant_buffers[aatx][i].i;
samples[aarx][i].r += (int16_t)(hre * xr - him * xi);
samples[aarx][i].i += (int16_t)(hre * xi + him * xr);
}
}
}
} else {
for (int aarx = 0; aarx < nbAnt; aarx++) {
double H_awgn_mimo_coeff[ant_buffers.size()];
for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) {
@@ -1351,6 +1419,7 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t,
}
}
}
}
is_first_peer = false;
}