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
+80 -11
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,17 +1356,64 @@ 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);
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++) {
uint32_t ant_diff = std::abs(aatx - aarx);
H_awgn_mimo_coeff[aatx] = ant_diff ? (0.2 / ant_diff) : 1.0;
}
for (uint aatx = 0; aatx < ant_buffers.size(); aatx++) {
for (int i = 0; i < nsamps; i++) {
samples[aarx][i].r += ant_buffers[aatx][i].r * H_awgn_mimo_coeff[aatx];
samples[aarx][i].i += ant_buffers[aatx][i].i * H_awgn_mimo_coeff[aatx];
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++) {
uint32_t ant_diff = std::abs(aatx - aarx);
H_awgn_mimo_coeff[aatx] = ant_diff ? (0.2 / ant_diff) : 1.0;
}
for (uint aatx = 0; aatx < ant_buffers.size(); aatx++) {
for (int i = 0; i < nsamps; i++) {
samples[aarx][i].r += ant_buffers[aatx][i].r * H_awgn_mimo_coeff[aatx];
samples[aarx][i].i += ant_buffers[aatx][i].i * H_awgn_mimo_coeff[aatx];
}
}
}
}