MAC Scheduler architecture
10 minute read
Target audience: RAN engineers. Scope: the MAC scheduler implementation in OCUDU (lib/scheduler/) — class ownership, slot indication pipeline, resource grid ring buffer, UE state model, HARQ management, PDCCH/PUCCH allocation, random access, RAN slicing, configuration manager, and concurrency.
1. What the Scheduler Does
The MAC scheduler (lib/scheduler/) is the component inside DU-High that decides, once per slot per cell, which UEs get uplink and downlink grants, on which resources, with which MCS. It consumes PHY feedback (HARQ-ACK, CRC, CQI) and MAC events (BSR, SR) and writes a sched_result structure consumed by the MAC layer to build DCIs and transport-block PDUs.
The scheduler is entirely self-contained behind the mac_scheduler interface. The only public entry point called during normal operation is slot_indication(slot_point, du_cell_index_t). Everything else — UE creation, cell configuration, HARQ feedback, metrics — arrives through separate event queues or configuration calls.
2. Top-Level Ownership
Two separate class hierarchies handle the two resource-management concerns.
scheduler_impl (scheduler_impl.h:16–83) is the public facade. It holds:
slotted_id_table<du_cell_group_index_t, ue_scheduler_impl>— oneue_scheduler_implper cell group (CA groups share a single scheduler).slotted_id_table<du_cell_index_t, cell_scheduler>— onecell_schedulerper cell, independent of CA.
cell_scheduler (cell_scheduler.h:29–101) owns all per-cell fixed resources: SSB, CSI-RS, SI, PRACH, RA, paging, PDCCH allocator, PUCCH allocator, SRS allocator, and the resource-grid ring buffer.
ue_scheduler_impl (ue_scheduling/ue_scheduler_impl.h:24–119) owns UE state shared across a cell group: the UE repository, event manager, slice schedulers, and intra-slice scheduler. In non-CA mode each group has one cell, so there is a 1:1 relationship. In CA mode multiple cell_scheduler instances share one ue_scheduler_impl, serialized by cell_group_mutex.
Factory: create_scheduler() in scheduler_factory.cpp:10–13 returns a unique_ptr<mac_scheduler>. It takes a scheduler_config containing expert parameters and logging hooks; no further runtime injection is needed.
3. Slot Indication Pipeline
slot_indication(sl_tx, cell_index) (scheduler_impl.cpp:199–215) calls cell_scheduler::run_slot(sl_tx) on the matching cell and returns cell.last_result(). It is single-threaded per cell; the MAC layer must not call it concurrently for the same cell.
cell_scheduler::run_slot() (cell_scheduler.cpp:76–132) executes these stages in order:
Each stage writes into the shared cell_resource_allocator ring buffer. No stage re-reads what a prior stage wrote; they only call collides() to check whether a resource is already taken.
Stage details:
| Stage | File | What it touches |
|---|---|---|
| Reset resource grid | cell/resource_grid.cpp | Advances ring, clears DL/UL grids, resets PDCCH/PUCCH/UCI/SRS per-slot state |
| SSB | common_scheduling/ssb_scheduler | Fixed positions in DL grid (no collision check needed) |
| CSI-RS | common_scheduling/csi_rs_scheduler | DL resource grid (symbol × CRB) |
| SI (SIB1 + on-demand SI) | common_scheduling/si_scheduler | DL grid + PDCCH allocator (DCI 1_0, RA-RNTI) |
| PRACH | common_scheduling/prach_scheduler | UL grid PRACH slots (fixed by config) |
| RA (RAR, Msg3, Msg4) | common_scheduling/ra_scheduler | DL/UL grids, PDCCH allocator, cell HARQ manager |
| Paging | common_scheduling/paging_scheduler | DL grid, PDCCH allocator (P-RNTI) |
| UE grants | ue_scheduling/ue_scheduler_impl | PDCCH, PUCCH allocators, resource grid |
UE Grant Sub-Pipeline
ue_scheduler_impl::run_slot_impl() (ue_scheduler_impl.cpp:114–163) runs in this order:
- Event processing (
ue_event_manager::run_slot) — dequeues CRC/UCI/HARQ-ACK feedback from lock-free MPMC queues, updates HARQ state machines, applies deferred UE config changes. - UE state advance (
ue_repository::slot_indication) — advances DRX timers, TA tracking, logical channel state. - UCI scheduling (
uci_scheduler_impl::run_slot) — allocates periodic SR and CSI PUCCH resources. - SRS scheduling (
srs_scheduler_impl::run_slot) — allocates periodic SRS before UE grants to prevent collision. - Fallback scheduler (
ue_fallback_scheduler::run_slot) — grants SRB0 PUSCH/PDCCH to UEs not yet fully configured (uses cellConfigCommon search spaces only, round-robin policy). - Slice prioritization (
inter_slice_scheduler::slot_indication) — produces ordered DL and UL slice candidate queues. - Intra-slice scheduling — for each DL slice candidate, calls
intra_slice_scheduler::dl_sched; for each UL candidate, callsul_sched. - Post-processing (
intra_slice_scheduler::post_process_results) — finalizes HARQ-ACK multiplexing onto PUCCH per k1 timing.
4. Resource Grid
Ring Buffer Structure
cell_resource_allocator (cell/resource_grid.h:312–396) is a circular vector of cell_slot_resource_allocator entries. The ring covers enough future slots to allow lookahead up to max(k0, k1, k2):
SCHEDULER_MAX_K0 = 15— max PDCCH-to-PDSCH offset.SCHEDULER_MAX_K1 = 15— max PDSCH-to-PUCCH HARQ-ACK offset.SCHEDULER_MAX_K2 = 11— max PDCCH-to-PUSCH offset.RING_MAX_HISTORY_SIZE = 16— past slots retained for error-indication recovery.
Ring size is at least 31 slots (≈3 ms at 30 kHz SCS). Operators access future slots with res_grid[k], where k is the scheduling delay.
Per-Slot Entry
Each cell_slot_resource_allocator (cell/resource_grid.h:278–308) holds:
sched_result result— the scheduling decisions (PDCCHs, PDSCHs, PUSCHs, PUCCH) that the MAC reads at the end of the slot.cell_slot_resource_grid dl_res_grid— DL symbol × CRB bitmap.cell_slot_resource_grid ul_res_grid— UL symbol × CRB bitmap.
cell_slot_resource_grid (resource_grid.h:160–275) manages multiple SCS-specific carriers. For each SCS, a carrier_subslot_resource_grid is a bitmap of 14 symbols × up to 275 PRBs. All allocators call collides(grant_info) before writing to this bitmap to detect overlaps.
5. UE State Model
Cell-Group-Wide: ue
ue (ue_context/ue.h:28–124) is the shared object for a UE across all serving cells in a group:
lc_ch_mgr— per-logical-channel DL buffer occupancy and UL BSR.ta_mgr— timing advance tracking.drx— DRX active timer controller.cells— bidirectional lookup betweendu_cell_indexandserv_cell_index.
Per-UE, Per-Cell: ue_cell
ue_cell (ue_context/ue_cell.h:38–210) holds state that is specific to one serving cell:
harqs—unique_ue_harq_entitymanaging DL and UL HARQ processes.channel_state_manager— tracks CQI, RI, PMI from CSI reports and SRS.link_adaptation_controller— translates CQI/SINR to MCS index.active_bwp_id()— always returns 0; no dynamic BWP switching is implemented.is_in_fallback_mode()— true when the UE has not yet received dedicated configuration; grants use cellConfigCommon search spaces only.
Carrier Aggregation
In a CA cell group one ue holds multiple ue_cell objects: one for PCell (serv_cell_index=0) and one per SCell. The ue_cell_lookup maps both du_cell_index and serv_cell_index to the corresponding ue_cell. Joint scheduling across cells is serialized by cell_group_mutex (ue_scheduler_impl.h:113).
6. HARQ Management
Pools and Structure
cell_harq_repository<IsDl> (cell/cell_harq_manager.h:134–194) maintains per-UE HARQ entities as a vector of process objects plus a recycling list of free IDs. Separate template instantiations cover DL and UL.
Each HARQ process (base_harq_process, cell/cell_harq_manager.h:41–77) holds:
harq_state_t status { empty | pending_retx | waiting_ack }
harq_mode_t mode { normal | feedback_disabled_or_mode_b }
slot_point slot_tx
slot_point slot_ack
slot_point slot_timeout
bool ndi
uint8_t nof_retxs
uint8_t max_nof_harq_retxs
State Transitions
Timeout Wheel
A circular array indexed by slot holds intrusive lists of processes whose timeout expires in that slot. slot_indication() on the HARQ manager advances the wheel and fires callbacks for expired processes. This avoids scanning all active HARQ processes every slot.
Msg3 HARQ
The RA scheduler allocates HARQ process 0 for Msg3 (ra_scheduler.h:77–79) per TS 38.321 Section 5.4.2.1. This HARQ process belongs to the cell HARQ manager, not to the UE HARQ entity, because the UE does not yet have a dedicated UE context at the time of Msg3 transmission.
7. PDCCH Allocation
pdcch_resource_allocator_impl (pdcch_scheduling/pdcch_resource_allocator_impl.h:19–89) maintains a ring of pdcch_slot_allocator entries (ring size ≥ 16 slots, from get_allocator_ring_size_gt_min(SCHEDULER_MAX_K0)). Each entry tracks PDCCH candidates allocated in that future slot.
Two search-space types are supported:
- Common search spaces (cellConfigCommon): used for RA-RNTI, P-RNTI, SI messages.
- Dedicated search spaces (cellConfigDedicated): UE-specific DCI scheduling.
Aggregation levels 1, 2, 4, 8, 16 map to CCE locations within the CORESET (TS 38.213 Section 8.1.1). The allocator selects the coarsest level that fits the DCI payload. DCI format selection:
| RNTI / Mode | DL DCI | UL DCI |
|---|---|---|
| RA-RNTI, P-RNTI, fallback mode | Format 1_0 | Format 0_0 |
| C-RNTI (dedicated) | Format 1_1 | Format 0_1 |
8. PUCCH Allocation
pucch_allocator_impl (pucch_scheduling/pucch_allocator_impl.h) tracks three grant types per UE per slot:
harq_ack— HARQ-ACK feedback from a PDSCH scheduled k1 slots earlier.sr— Scheduling Request at its configured periodicity.csi— CSI report.
pucch_resource_manager prevents resource conflicts and manages multiplexing. When HARQ bits, SR bits, and CSI bits together exceed Format 0/1 capacity, the allocator promotes to Format 2/3/4.
PUCCH HARQ-ACK is allocated at PDSCH_slot + k1 (TS 38.213 Section 9.2.3). The post_process_results() call in the UE grant sub-pipeline finalizes this multiplexing after all PDSCH grants for the slot are known.
9. Random Access Pipeline
ra_scheduler (common_scheduling/ra_scheduler.h:25–140) dequeues rach_indication_message objects from a lock-free MPMC queue. PHY writes to this queue from its thread; the scheduler reads it at stage 5 of run_slot().
Two procedures are supported:
4-step RACH (Msg1 → RAR → Msg3 → Msg4):
- PHY detects preamble →
rach_indication_messageenqueued. - Scheduler allocates RAR PDSCH with RA-RNTI within the RAR window.
- Scheduler allocates Msg3 PUSCH with TC-RNTI (HARQ process 0).
- After Msg3 CRC success, scheduler allocates Msg4 PDSCH (Contention Resolution MAC CE).
2-step RACH (MsgA → Msg4):
- PHY detects combined preamble + PUSCH (MsgA).
- Scheduler skips RAR, goes directly to Msg4 PDSCH allocation.
Key structures:
pending_rar_alloc— holds RA-RNTI, PRACH slot, RAR window, list of TC-RNTIs per preamble.pending_msg3_alloc— holds preamble info and the cell HARQ process for Msg3 retransmission.
RA-RNTI is derived from PRACH slot and occasion index (TS 38.321 Section 5.1.1). TC-RNTI is allocated per preamble. UE confirms contention resolution when Msg4 carries its UE ID in the Contention Resolution MAC CE.
10. RAN Slicing
Slicing runs in two layers inside the UE grant sub-pipeline.
Inter-Slice Scheduler
inter_slice_scheduler (slicing/inter_slice_scheduler.h:15–150) produces an ordered priority queue of DL and UL slice candidates each slot. Priority is computed per ran_slice_sched_context::get_prio() based on:
- Slice SLA min/max PRB thresholds.
- Recent PRB utilization.
- Pending buffer depth.
Slices with lower recent utilization or higher configured priority rank higher.
Intra-Slice Scheduler
intra_slice_scheduler (ue_scheduling/intra_slice_scheduler.h:18–100) allocates UE grants within each slice candidate:
- Retransmission candidates first — UEs with HARQ processes in
pending_retxstate. - New transmission candidates next — ranked by the slice’s
scheduler_policy. grant_params_selectorcomputes MCS, RV, and HARQ ID per UE.ue_cell_grid_allocatorwrites PDSCH/PUSCH grants into the resource grid.
Scheduling Policies
scheduler_policy (policy/scheduler_policy.h:37–71) is an abstract interface with two concrete implementations:
scheduler_time_rr— time-domain round-robin per UE.scheduler_time_qos— weighted round-robin with buffer-aware QoS weighting.
The policy interface exposes:
compute_ue_dl_priorities(pdcch_slot, pdsch_slot, ue_candidates)— rank UE candidates.compute_ue_ul_priorities(pdcch_slot, pusch_slot, ue_candidates)— rank UE candidates.save_dl_newtx_grants()/save_ul_newtx_grants()— update internal state after allocation.
ue_sched_priority is a double. forbid_sched_priority = numeric_limits<double>::lowest() means “do not schedule this UE this slot.”
11. Configuration Manager
sched_config_manager (config/sched_config_manager.h:80–111) handles cell and UE lifecycle:
- Cell add:
add_cell()validates thesched_cell_configuration_request_messageand returns an immutablecell_configurationshared across the scheduler. - UE add / update / remove: Each operation returns a
ue_config_update_eventorue_config_delete_event. These events are applied at the next slot boundary insideue_event_manager::run_slot(), not immediately. This prevents configuration changes from occurring mid-slot where state may be partially updated.
12. Concurrency Model
slot_indication()is not thread-safe. The MAC layer guarantees single-threaded calls per cell.- PHY and RLC threads push RACH indications, CRC results, and UCI feedback through lock-free MPMC queues (
ra_scheduler.h:99,ue_event_manager.h:128). The scheduler drains these queues at the start of each slot. - In CA mode, multiple
cell_schedulerinstances in the same group share oneue_scheduler_impl. The CA code path acquirescell_group_mutex(ue_scheduler_impl.cpp:116–119) before callingrun_slot_impl(), serializing slot processing across cells in the group. - No locking occurs outside CA paths and the MPMC queue boundaries.
13. Metrics
scheduler_metrics_handler (logging/scheduler_metrics_handler.h:26–120) aggregates per-slot data. A cell_metrics_handler inside it accumulates, per UE and per slot:
- HARQ-ACK counts and delays.
- CRC success and failure counts.
- DL/UL transport block byte counts.
- MCS values.
- PRB usage.
- TA and CQI statistics.
Metrics are reported at a configurable interval by the owning MAC layer component.
14. Scheduler Constants and Limits
From include/ocudu/scheduler/sched_consts.h:22–33:
| Constant | Value | Meaning |
|---|---|---|
SCHEDULER_MAX_K0 | 15 | Max PDCCH-to-PDSCH slot offset |
SCHEDULER_MAX_K1 | 15 | Max PDSCH-to-PUCCH HARQ-ACK offset |
SCHEDULER_MAX_K2 | 11 | Max PDCCH-to-PUSCH slot offset |
RING_MAX_HISTORY_SIZE | 16 | Past slots retained in resource grid ring |
MAX_DU_CELL_GROUPS | — | Max cell groups (bounds groups table) |
MAX_NOF_DU_CELLS | — | Max cells (bounds cells table) |
15. Not Implemented / Partial
| Feature | Status |
|---|---|
| Dynamic BWP switching | Not implemented — active_bwp_id() always returns 0 |
| NTN HARQ mode B / feedback-disabled | Structure present (harq_mode_t enum), scheduling not exercised in normal operation |
| Scheduler policy pluggability at runtime | Policy is selected at UE creation; no runtime hot-swap mechanism |
| SRS-based link adaptation | SRS scheduling and SRS estimator exist; loop back into link adaptation controller not fully traced |