MAC Scheduler architecture

How OCUDU’s MAC scheduler works — slot indication pipeline, resource grid, HARQ management, PDCCH/PUCCH allocation, random access, RAN slicing, and the concurrency model.

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> — one ue_scheduler_impl per cell group (CA groups share a single scheduler).
  • slotted_id_table<du_cell_index_t, cell_scheduler> — one cell_scheduler per 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:

StageFileWhat it touches
Reset resource gridcell/resource_grid.cppAdvances ring, clears DL/UL grids, resets PDCCH/PUCCH/UCI/SRS per-slot state
SSBcommon_scheduling/ssb_schedulerFixed positions in DL grid (no collision check needed)
CSI-RScommon_scheduling/csi_rs_schedulerDL resource grid (symbol × CRB)
SI (SIB1 + on-demand SI)common_scheduling/si_schedulerDL grid + PDCCH allocator (DCI 1_0, RA-RNTI)
PRACHcommon_scheduling/prach_schedulerUL grid PRACH slots (fixed by config)
RA (RAR, Msg3, Msg4)common_scheduling/ra_schedulerDL/UL grids, PDCCH allocator, cell HARQ manager
Pagingcommon_scheduling/paging_schedulerDL grid, PDCCH allocator (P-RNTI)
UE grantsue_scheduling/ue_scheduler_implPDCCH, PUCCH allocators, resource grid

UE Grant Sub-Pipeline

ue_scheduler_impl::run_slot_impl() (ue_scheduler_impl.cpp:114–163) runs in this order:

  1. 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.
  2. UE state advance (ue_repository::slot_indication) — advances DRX timers, TA tracking, logical channel state.
  3. UCI scheduling (uci_scheduler_impl::run_slot) — allocates periodic SR and CSI PUCCH resources.
  4. SRS scheduling (srs_scheduler_impl::run_slot) — allocates periodic SRS before UE grants to prevent collision.
  5. 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).
  6. Slice prioritization (inter_slice_scheduler::slot_indication) — produces ordered DL and UL slice candidate queues.
  7. Intra-slice scheduling — for each DL slice candidate, calls intra_slice_scheduler::dl_sched; for each UL candidate, calls ul_sched.
  8. 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 between du_cell_index and serv_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:

  • harqsunique_ue_harq_entity managing 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 / ModeDL DCIUL DCI
RA-RNTI, P-RNTI, fallback modeFormat 1_0Format 0_0
C-RNTI (dedicated)Format 1_1Format 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):

  1. PHY detects preamble → rach_indication_message enqueued.
  2. Scheduler allocates RAR PDSCH with RA-RNTI within the RAR window.
  3. Scheduler allocates Msg3 PUSCH with TC-RNTI (HARQ process 0).
  4. After Msg3 CRC success, scheduler allocates Msg4 PDSCH (Contention Resolution MAC CE).

2-step RACH (MsgA → Msg4):

  1. PHY detects combined preamble + PUSCH (MsgA).
  2. 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:

  1. Retransmission candidates first — UEs with HARQ processes in pending_retx state.
  2. New transmission candidates next — ranked by the slice’s scheduler_policy.
  3. grant_params_selector computes MCS, RV, and HARQ ID per UE.
  4. ue_cell_grid_allocator writes 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 the sched_cell_configuration_request_message and returns an immutable cell_configuration shared across the scheduler.
  • UE add / update / remove: Each operation returns a ue_config_update_event or ue_config_delete_event. These events are applied at the next slot boundary inside ue_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_scheduler instances in the same group share one ue_scheduler_impl. The CA code path acquires cell_group_mutex (ue_scheduler_impl.cpp:116–119) before calling run_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:

ConstantValueMeaning
SCHEDULER_MAX_K015Max PDCCH-to-PDSCH slot offset
SCHEDULER_MAX_K115Max PDSCH-to-PUCCH HARQ-ACK offset
SCHEDULER_MAX_K211Max PDCCH-to-PUSCH slot offset
RING_MAX_HISTORY_SIZE16Past slots retained in resource grid ring
MAX_DU_CELL_GROUPSMax cell groups (bounds groups table)
MAX_NOF_DU_CELLSMax cells (bounds cells table)

15. Not Implemented / Partial

FeatureStatus
Dynamic BWP switchingNot implemented — active_bwp_id() always returns 0
NTN HARQ mode B / feedback-disabledStructure present (harq_mode_t enum), scheduling not exercised in normal operation
Scheduler policy pluggability at runtimePolicy is selected at UE creation; no runtime hot-swap mechanism
SRS-based link adaptationSRS scheduling and SRS estimator exist; loop back into link adaptation controller not fully traced