Slice-ML: Hierarchical Deep Learning for RAN Resource Slicing
Slice-ML divides one radio carrier between an eMBB slice and a URLLC slice, and then decides who transmits inside each. These are two different problems at two different speeds, so the feature solves them with two independent control loops: a slow controller moves the boundary between slices once per second, and per-slot attention schedulers rank users inside a slice. It runs on the OCUDU-RAN distributed unit rather than in simulation.
As with the other AI-RAN features in this stack, every part defaults to disabled, the conventional scheduler is the fallback at every decision point, and no ML framework is embedded in the DU.
What is decided, and by what
eMBB (enhanced mobile broadband) carries large downloads. It wants throughput and tolerates delay. URLLC (ultra-reliable low-latency communication) carries small frequent packets. It tolerates low throughput and does not tolerate delay. Both draw on the same carrier. A slice is the logical partition serving one class, identified by a service type (sst) and differentiator (sd).
| Loop | Period | What it decides | Model |
|---|---|---|---|
| Slice control | 1 s | the min/max PRB ratio each slice gets | LSTM, FC, softmax over 19 actions |
| In-slice scheduling | one slot | which users to serve, and how much | attention encoder-decoder, one per slice |
The two loops are independent. They meet at exactly one point: when the controller changes a slice's minimum ratio, that slice's scheduler sees the new dedicated size as an input feature. Neither invokes the other.
The objective
The slice controller optimises one quantity, evaluated per one-second period:
R = 0.01 . eMBB_Mbps - 5.0 . max(0, 0.90 - SSR_eMBB) - 5.0 . max(0, 0.99 - SSR_URLLC)
SSR is the slice satisfaction ratio, the fraction of a slice's users whose demand was met. Each penalty applies only when its target is missed.
The weighting is deliberately asymmetric. An SLA breach costs roughly 500 times more than a megabit of throughput earns. A controller trained on this reward is expected to trade throughput for reliability whenever the two conflict. That is the objective working as designed, not a defect.
Satisfaction is computed inside the DU's metrics path and reported per slice, per period. SSR_eMBB counts users with an empty download buffer or a delivered rate at or above the configured target. SSR_URLLC counts users with an empty download buffer and nothing pending uplink.
The reward exists twice, in C++ for the live controller and in Python for training and analysis. The analysis tooling imports the constants directly from the training script, so the two cannot drift apart.
Implementation
Two halves
In-RAN runtime (C++). A metrics consumer registered with the DU's reporting service owns one slice controller per cell. Two self-contained predictors perform inference with no ML framework, no dynamic allocation on the hot path, and single-precision arithmetic with a cached projection for the attention encoder. Model files are a small text header followed by weights, and they can be hot-reloaded.
Offline platform (Python). Four training scripts, one per model. A reward analyser that shares its constants with the training code. Verification harnesses that check the C++ predictors reproduce the training framework's arithmetic, and a rollout benchmark. The offline C++ tools compile standalone and are not part of the DU build.
The four models
| # | Role | Architecture | Runs in the DU |
|---|---|---|---|
| 1 | Slice controller, chooses the PRB split | LSTM 256, FC 512, softmax(19) | yes |
| 2 | Value baseline, trains the controller | LSTM 256, FC 512, scalar | no, training only |
| 3 | URLLC scheduler, ranks users in the slice | attention enc-dec, 3 feat, 2 ctx | yes |
| 4 | eMBB scheduler, ranks users in the slice | attention enc-dec, 2 feat, 1 ctx | yes |
Model 2 has no counterpart in the DU. It produces the learning signal that trains Model 1 and is then set aside. Training order is sequential, with the URLLC scheduler trained before the eMBB scheduler that learns against it, but at runtime the three deployed models never call each other.
Slice control loop
Once per second the scheduler emits a metrics report. The controller reduces it to four observations, appends a one-hot encoding of the action currently in force for 23 inputs in total, runs one LSTM step, and takes the argmax over 19 candidate actions.
Each action is a pair of minimum and maximum PRB ratios for the two slices. Twelve come from the source paper's allocation zones. Seven are plain splits added for this deployment. The minimum ratio is a floor a slice is guaranteed. The maximum is a ceiling it may borrow up to when the other slice is idle.
A proposed change must clear two stages before it is applied. First hysteresis: the same action must repeat for several consecutive periods. Then safety gates: a URLLC minimum floor, a cap on total reservation, and a minimum interval since the last change.
Actuation is a runtime reconfiguration request through the DU's existing path, never a config-file edit. Editing YAML would require a restart and drop every connected user.
On the following period the controller re-reads the ratios the cell actually reports and warns if they do not match what was requested. This matters: a reconfiguration whose slice identity does not match is accepted and then silently dropped, so without the read-back the log would claim a switch that never happened.
Each cell gets its own controller, created on first sight of that cell, so recurrent state and switch counters cannot leak between cells.
In-slice scheduling
The DU already constructs one scheduling policy per slice, so the models attach with no dispatch logic. Each configured slice names its own policy, and any slice that does not is left with the conventional scheduler.
The models work on units, which are groups of adjacent PRBs, rather than individual resource blocks. The grouping factor is configured per slice and does two jobs: it keeps the problem inside the range the models were trained over, and it controls cost, because the encoder's work scales with the square of the pair count. A model asked to run far outside its trained range refuses to load, and the slice keeps the conventional scheduler.
The model does not allocate spectrum. The decoder emits (user, unit) pairs. The number of units a user wins becomes that user's priority, and the DU's existing allocator performs frequency placement. The model contributes the who-and-how-much judgement, while the allocator keeps the 3GPP placement constraints the model was never given.
Both roles are downlink only. Uplink on both slices is delegated to the conventional policy.
| Aspect | eMBB scheduler | URLLC scheduler |
|---|---|---|
| Per-pair features | 2: channel gain, shared-region flag | 3: channel gain, pending demand, shared-region flag |
| Context inputs | 1: units still free | 2: power remaining, units still free |
| Decoder objective | lift users toward a rate floor | drain pending demand within a power budget |
| Selection constraint | unit must be free | unit free, power feasible, demand outstanding |
Safety
- Every rollout is timed against the slot budget. One that overruns is discarded and the conventional policy runs for that slot instead, so an overrun costs scheduling quality and never the slot deadline.
- After repeated consecutive overruns the model is switched off for the rest of the run, and the slice falls back permanently.
- A model is accepted onto a slice only if its declared role, feature widths and context widths match what that slice requires. Any mismatch logs a warning and keeps the conventional scheduler.
- The controller can run complete, observing and inferring and logging, with actuation switched off. This validates a model against live traffic without touching the radio.
- Replacing the conventional policy on a slice also replaces its quality-of-service weighting, so guaranteed-bitrate commitments on that slice become the model's responsibility.
Configuration
There are two blocks. Per-slice policy sits inside cell_cfg.slicing, and the controller and dataset logging sit in a top-level slice_ml block. A complete working example ships in the repository as configs/slicemanager.yaml.
cell_cfg:
slicing:
- sst: 1 # eMBB
sd: 1
sched_cfg:
min_prb_policy_ratio: 50 # the controller overwrites this at runtime
max_prb_policy_ratio: 100
policy:
attention_ml:
model_path: ml/slicemanager/embb_scheduler.model
role: embb
prb_group: 32
- sst: 2 # URLLC
sd: 1
sched_cfg:
min_prb_policy_ratio: 50
max_prb_policy_ratio: 100
policy:
attention_ml:
model_path: ml/slicemanager/urllc_scheduler.model
role: urllc
prb_group: 16
slice_ml:
dataset_logging:
enabled: true
output_dir: ml/datasets/slice_datasets
scenario: baseline # free-text label, becomes a CSV column
target_dl_rate_kbps: 4000.0 # eMBB satisfaction yardstick
delay_budget_ms: 10.0
inference:
enabled: true
model_path: ml/slicemanager/slicemanager_actor.model
apply: false # false is shadow mode: observe and log, never actuate
default_action_idx: 13 # must match the ratios configured above
min_urllc_prb_ratio: 10
max_total_min_ratio: 100
switch_hysteresis_periods: 5
min_periods_between_switches: 30
plmn: "00101"
embb_sst: 1
embb_sd: 1 # must match cell_cfg.slicing exactly
urllc_sst: 2
urllc_sd: 1
Keys that need care
| Key | Why it matters |
|---|---|
apply | false is shadow mode. Leave it false until a model has been validated against live traffic. |
default_action_idx | Must correspond to the ratios actually configured on the slices, or the controller starts from a false belief about the current state. |
embb_sd / urllc_sd | Slice identity must match cell_cfg.slicing exactly, including network, service type and differentiator. A mismatch means reconfiguration is addressed to a slice that does not exist, and it is silently dropped. |
prb_group | Must match the grouping the model was trained at. Too small a value pushes the problem outside the trained range and the policy refuses to load. |
The URLLC slice must also be advertised by the CU. Add it to the tracking-area slice support list in the CU configuration alongside the eMBB slice, or UEs will never attach to it.
Getting the source and building
git clone https://github.com/TOSSI-Foundation/OCUDU-RAN.git
cd OCUDU-RAN
git checkout slice_ml
Every path below is relative to that clone, and the commands run from its root. Install dependencies and build with the project's own scripts, which are what CI uses:
sudo docker/scripts/install_dependencies.sh
docker/scripts/builder.sh -c gcc -m "-j$(nproc)" -DBUILD_TESTING=On .
This produces the CU and DU binaries under build/apps/. To confirm the tree is sound before going further:
cd build && ctest -j"$(nproc)" --schedule-random --output-on-failure
The Python side needs numpy for analysis and torch to train any of the four models. The standalone C++ verification and benchmark tools compile themselves on first use and need only a C++17 compiler. They are not part of the DU build.
Rollout
The feature is designed to be adopted in stages, and each one is safe to stop at.
| Stage | dataset_logging.enabled | inference.enabled | inference.apply | Slice policy |
|---|---|---|---|---|
| 1, Collect | true | false | false | omitted |
| 2, Shadow | true | true | false | omitted |
| 3, Slice control | true | true | true | omitted |
| 4, Full | true | true | true | attention_ml |
Stage 3 enables the slow loop alone, and stage 4 adds the per-slot schedulers. Because the two loops are independent, either can run without the other.
Collecting data
Run the stack with dataset logging enabled and inference off, then generate traffic that represents what you want the controller to learn: an eMBB download alongside a periodic small-packet URLLC flow.
The DU writes two CSV streams per run into dataset_logging.output_dir, distinguished by prefix: slice_ml_slicemanager_<timestamp>.csv, one row per slice per one-second period, and slice_ml_ue_<timestamp>.csv, one row per UE per period. The two files of a run share a timestamp and are used together, and the analysis tooling derives the second path from the first. Set dataset_logging.scenario to a distinct label per run so captures can be grouped later.
Collect enough periods for the controller to have seen varied conditions. A run that never puts both slices under load teaches nothing: if the URLLC slice carries no traffic, its satisfaction is trivially perfect, the penalty term is identically zero, and the controller optimises a throughput-only objective.
Training
All four models are trained from the repository root. Every script defaults to CPU, so pass --device cuda to any of them to use a GPU. Set a variable for the capture directory so the commands stay portable:
DS=ml/datasets/slice_datasets
Model 3, URLLC scheduler
This trains on a synthetic environment and reads no captures. Train it before Model 4.
python3 ml/training/train_urllc_scheduler.py \
--model-out ml/slicemanager/urllc_scheduler.model \
--d-model 32 --n-heads 4 \
--cell-bw-mhz 100 --prb-group 16 \
--epochs 100 --batch 512 --steps-per-epoch 20 \
--users 4 --arrival 1.5
Model 4, eMBB scheduler
This trains against the URLLC model, so it must come second.
python3 ml/training/train_embb_scheduler.py \
--model-out ml/slicemanager/embb_scheduler.model \
--urllc-model ml/slicemanager/urllc_scheduler.model \
--d-model 32 --n-heads 4 \
--cell-bw-mhz 100 --prb-group 32 \
--epochs 100 --batch 128 --steps-per-epoch 20 \
--users 6
Set --d-model and --n-heads explicitly for both schedulers. The script defaults are wider than a slot budget allows. Inference cost scales with the square of the model width, so a model trained at the defaults will overrun its deadline, trip the watchdog and disable itself, and the slice then silently runs the conventional scheduler. Always confirm with the benchmark below before trusting a live run.
Model 1, slice controller
This trains on the captures collected above.
python3 ml/training/train_slicemanager_actor.py "$DS"/slice_ml_slicemanager_*.csv \
--model-out ml/slicemanager/slicemanager_actor.model \
--d-hidden 256 --d-fc 512 \
--epochs 200 --lr 1e-3 \
--shuffle-prev-action
--shuffle-prev-action matters. The controller takes the previous action as an input, and in collected data that action usually equals the correct next one. Without the flag the network learns the identity function and never switches. It is a deliberate departure from the paper's input formulation.
The script reports held-out accuracy against a majority-class baseline. If it warns that the model does not beat that baseline, do not promote it. A constant policy is doing at least as well, and the usual cause is too few distinct runs or too little variation across them.
Model 2, value baseline
Same captures. This is training only, and nothing loads it at runtime.
python3 ml/training/train_slicemanager_critic.py "$DS"/slice_ml_slicemanager_*.csv \
--model-out ml/slicemanager/slicemanager_critic.model \
--d-hidden 256 --d-fc 512 \
--epochs 200 --gamma 0.99
Verifying a trained model
Run these after every retrain, before any live run. First check that the C++ predictors reproduce the training framework's arithmetic:
bash ml/analysis/verify_slice_ml_forward.sh ml/slicemanager/slicemanager_actor.model
bash ml/analysis/verify_attention_forward.sh ml/slicemanager/urllc_scheduler.model
bash ml/analysis/verify_attention_forward.sh ml/slicemanager/embb_scheduler.model
Each prints a maximum absolute difference and whether the selected actions agree. Expect a negligible difference and identical selections. Then confirm the schedulers fit their deadline:
bash ml/analysis/bench_attention_rollout.sh ml/slicemanager/embb_scheduler.model 50
bash ml/analysis/bench_attention_rollout.sh ml/slicemanager/urllc_scheduler.model 50
The benchmark prints total microseconds per rollout across a grid of user and unit counts. Find the row matching your deployment, meaning the UE count you expect on that slice and the unit count implied by your carrier width and prb_group, then confirm it sits under one slot. At 30 kHz subcarrier spacing a slot is 500 us.
These scripts locate their own dependencies and compile into a temporary directory, so the first run takes noticeably longer than later ones.
Running
./build/apps/cu/ocu -c configs/cu.yml
./build/apps/du/odu -c configs/slicemanager.yaml
Confirm from the DU log that each piece came up: the controller announcing itself, its mode (apply or shadow) and its starting action; one attention-model load line per configured slice, giving role, unit count and deadline; and a periodic heartbeat carrying the observation, the utility and the chosen action.
Two log lines indicate a real problem. A warning that an actuation did not take effect means the slice identities in slice_ml.inference do not match cell_cfg.slicing. A warning that a model was disabled after consecutive deadline overruns means the model is too wide, or prb_group too small for the carrier. Retrain narrower or raise prb_group, then re-check with the benchmark.
Analysing a run
Score captures with the reward the controller was trained on:
python3 ml/analysis/slice_ml_reward.py --by file
--dir points the tool at a capture directory other than the configured default. --by file reports one row per capture, which is what you want when comparing runs; the default groups by the scenario label instead, which collides when several runs share a label. --scenario filters to captures whose key contains a substring, and --quiet drops the explanatory header.
The output gives, per capture, the mean reward and the three terms that sum to it, alongside both satisfaction ratios and eMBB throughput. A second table reports radio-side context such as throughput, error rates and modulation per slice. That context explains why a penalty term is what it is, without being part of the reward.
To compare configurations, run each one identically except for the setting under test, label them distinctly with dataset_logging.scenario, and compare their rewards. Reading the result honestly needs two habits:
- Check that both slices were actually loaded. A run where the URLLC slice was idle scores near zero because the penalty term is absent, not because the configuration was good.
- Compare ranges, not just means. With a small number of runs per configuration, only a separation where the ranges do not overlap is evidence of anything.
Hot-swapping a model
The controller watches its model file for modification. A replacement is validated, swapped in atomically, and the recurrent state is reset, all without restarting the DU. A file that fails validation is rejected and the running model kept. Writing a new model to the configured path is therefore enough to promote it.
References
M. Setayesh, S. Bahrami and V. W. S. Wong, "Resource Slicing for eMBB and URLLC Services in Radio Access Network Using Hierarchical Deep Learning", IEEE Transactions on Wireless Communications, vol. 21, no. 11, 2022. PDF
3GPP scope
The scheduling algorithm is implementation-specific in TS 38.300. The standard constrains what a scheduler emits, which remains the allocator's responsibility. Slice-ML introduces no new procedures, no new UE signalling and no radio-interface changes. Slice reconfiguration uses the DU's existing RRM policy path.
Source
OCUDU-RAN, branch slice_ml: github.com/TOSSI-Foundation/OCUDU-RAN
Related AI-RAN work: ML-based UL MCS, ML-based BSR periodicity and predictive CSI for link adaptation. End-to-end slicing across UE, RAN, Core and SMO is covered in network slicing.