- LLM Cache Policy Learning: SFT / GRPO / DAPO (based on TRL + Unsloth)
-
cache_env/: Multi-base-station caching simulation environment + prompt/action-format utilitiesunified_multi_bs_cache_env.py: Environment (state, cache, hit rate, frequency features, step/reset, etc.)unified_data_loader.py: Data generation/loading (Zipf, multi-user, etc.)prompt_utils.py: Formats environment state into LLM prompts (system/user messages)history_rebuilder.py: SFT “teacher policy” (lookahead/exhaustive search) and related toolsaction_validator.py: Strictly parses/validates LLM action output format (Replace/NoOp)
-
llm/: Training and evaluation entry scriptstrain_sft.py: Generate SFT data (teacher decisions) and run LoRA SFTtrain_grpo.py: GRPO training (rewards based on cache hit rate/gain, etc.)train_dapo.py: DAPO training (GRPOConfig(loss_type="dapo"))evaluate_unified.py: Unified evaluation script (optional; includes multiple evaluation logic fallbacks via defensive imports)
-
tools/: Helper toolsdownload_hf_model.py: Download Hugging Face model snapshots to a local directorymerge_lora.py: Merge a LoRA adapter into the base model and save as a merged model
-
baselines/sac_baseline/: SAC baseline (original repo README preserved in this directory)
Recommended: Linux + NVIDIA GPU.
pip install -r requirements.txtNotes:
requirements.txtincludesrequirements-llm.txt+requirements-sac.txt.- For
torch, it’s recommended to install via the official method based on your CUDA version (platform/driver differences can be significant).
torch: Training and inference computationtransformers: Tokenizer / model loading / generation configurationtrl:SFTTrainer,GRPOTrainer,GRPOConfigunsloth: Accelerated LoRA/SFT/GRPO training (optional but recommended)datasets: Training dataset wrapper (Dataset)numpy/pandas: Environment and data generation/processingtqdm: Progress barhuggingface_hub: Used bytools/download_hf_model.pyto download modelstensorboard/wandb: Training logsgym: Environment interface forbaselines/sac_baseline
python tools/download_hf_model.py --repo_id Qwen/Qwen2.5-7B-Instruct --out models/Qwen2.5-7B-Instruct- Download the base model (or prepare a local model directory)
- Run
llm/train_sft.pyto train LoRA via SFT (teacher policy generates data) - Use
tools/merge_lora.pyto merge LoRA into the base model and produce a merged model directory (optional, but GRPO/DAPO is usually more convenient this way) - Run
llm/train_grpo.pyorllm/train_dapo.pyfor preference/policy optimization
LoRA merge example:
python tools/merge_lora.py --base_model models/Qwen2.5-7B-Instruct --adapter outputs/sft/<run>/final_sft_checkpoint --out models/merge7B_exbertAll these scripts support overriding paths via environment variables (defaults write into data/ and outputs/ under the repo):
-
SFT:
SFT_MODEL_PATH: Base model pathSFT_DATA_DIR: SFT data output directorySFT_OUTPUT_DIR: SFT training output directory
python llm/train_sft.py-
GRPO:
GRPO_MODEL_PATH: Base/merged model pathGRPO_DATA_ROOT: GRPO data root directoryGRPO_OUTPUT_ROOT: GRPO output root directoryUSE_VLLM=1: Enable vLLM (if vLLM is not installed, it will automatically fall back to disabled)
python llm/train_grpo.py-
DAPO:
DAPO_MODEL_PATH/DAPO_DATA_ROOT/DAPO_OUTPUT_ROOTare analogous
python llm/train_dapo.pyIn this repo’s training scripts, the number of base stations / number of users / and other environment hyperparameters are defined as global constants at the top of the scripts. The current default values are:
llm/train_sft.py: default B=2 (NUM_BASE_STATIONS=2, NUM_USERS=20)llm/train_grpo.py: default B=5 (NUM_BASE_STATIONS=5, NUM_USERS=40)llm/train_dapo.py: default B=2 (NUM_BASE_STATIONS=2, NUM_USERS=20)llm/evaluate_unified.py: switch B=2 / B=5 via command-line arguments
Below is a consolidated write-up of the B=2 and B=5 training/evaluation workflow and the hyperparameters you need. Follow it as-is to reproduce the experiments.
Script: llm/train_sft.py
Key environment hyperparameters:
NUM_BASE_STATIONS=2NUM_USERS=20NUM_CONTENTS=100CACHE_SIZES=[10,10]ZIPF_PARAM=1.2NUM_EPOCHS_DATA_SFT=3000(SFT data volume)MAX_SEQ_LENGTH=2048
Run:
# (Optional) Specify the base model directory (HF format); if not set, defaults to models/Qwen2.5-7B-Instruct
export SFT_MODEL_PATH=models/Qwen2.5-7B-Instruct
python llm/train_sft.pyOutput notes:
- SFT data: written to
data/sft/, filenames likesft_data_no_cot_replacement_only_v1_nbs2_seed666.json - SFT LoRA: written to
outputs/sft/SFT_Qwen2_Cache_<timestamp>/final_sft_checkpoint/
python tools/merge_lora.py \
--base_model models/Qwen2.5-7B-Instruct \
--adapter outputs/sft/SFT_Qwen2_Cache_<timestamp>/final_sft_checkpoint \
--out models/merge7B_exbertIf you want to train GRPO with B=2, change the global constants at the top of llm/train_grpo.py to the following:
# ===== B=2 (Two Base Stations) GRPO Hyperparameters =====
MAX_COMPLETION_LENGTH = 64
MAX_PROMPT_LENGTH = 2048
MAX_SEQ_LENGTH = MAX_PROMPT_LENGTH + MAX_COMPLETION_LENGTH
LORA_RANK = 32
LORA_ALPHA = 32
NUM_BASE_STATIONS = 2
NUM_USERS = 20
NUM_CONTENTS = 100
CACHE_SIZES = [10, 10]
ZIPF_PARAM = 1.2
NUM_EPOCHS_FOR_GRPO_DATA = 10000
FUTURE_STEPS_REWARD = 10
FUTURE_STEPS_SAMPLING = 5
GRPO_DATA_SEED = 4567
TRAINER_SEED = 42
ANNEAL_STRATEGY = "cosine"
ANNEAL_TEMP_START = 0.90
ANNEAL_TEMP_END = 0.75
ANNEAL_TOP_P_START = 0.95
ANNEAL_TOP_P_END = 0.85
NUM_GENERATIONS = 4
TEMPERATURE = ANNEAL_TEMP_START
TOP_P = ANNEAL_TOP_P_START
TOP_K = 50
REPETITION_PENALTY = 1.0
LEARNING_RATE = 1e-4
PER_DEVICE_TRAIN_BATCH = 4
GRAD_ACCUM_STEPS = 12
NUM_TRAIN_EPOCHS = 1
LOGGING_STEPS = 1
SAVE_STEPS = 100
MAX_GRAD_NORM = 0.1
DEBUG_SAMPLING_SMOKE_TEST = True
PRINT_ALL_GROUPS = True
MAX_GROUPS_TO_PRINT = 3
SHOW_GLOBAL_BEST_WORST = True
RESET_CACHE_EVERY = 1500
REWARD_HIT_MODE = "delta_weighted"
GAIN_SCALE = 1.0
EARLY_PHASE_STEPS = 120
NOOP_PENALTY_LATE = 0.005
NOOP_PENALTY_EARLY_WITH_OPP = 0.0075
NOOP_PENALTY_EARLY_NO_OPP = 0.0025
MAX_JOINT_COMBOS_PER_STEP = 50000
UNCOND_NOOP_PENALTY_STEPS = 30
UNCOND_NOOP_PENALTY = 0.005
OPP_EVAL_STEPS = 5
OPP_DISCOUNT_GAMMA = 0.9
SLOT_INDEX_BASE = 0
ANNEAL_ENABLED = bool(int(os.getenv("ANNEAL_ENABLED", "1")))
ANNEAL_LOG_EVERY = int(os.getenv("ANNEAL_LOG_EVERY", "5")))Then run:
# (Optional) Disable annealing: ANNEAL_ENABLED=0; by default ANNEAL_ENABLED=1 enables it
export ANNEAL_ENABLED=1
export GRPO_MODEL_PATH=models/merge7B_exbert
python llm/train_grpo.pyOutput notes:
- GRPO data: written to
data/grpo/grpo_cache_data_<timestamp>/ - GRPO LoRA: written to
outputs/grpo/grpo_cache_<timestamp>/final_lora_weights/
Script: llm/train_dapo.py (current code defaults to B=2)
export DAPO_MODEL_PATH=models/merge7B_exbert
python llm/train_dapo.pyIf you want to train SFT with B=5, it is recommended to at least change the following constants:
NUM_BASE_STATIONS = 5NUM_USERS = 40CACHE_SIZES = [10,10,10,10,10]MAX_SEQ_LENGTH = 4096(B=5 prompts are longer; if too small, many samples will be filtered out)
After changes, run as usual:
export SFT_MODEL_PATH=models/Qwen2.5-7B-Instruct
python llm/train_sft.pySame as above: produce models/merge7B_exbert (or a custom merged directory) for GRPO.
If you want to train GRPO with B=5, change the global constants at the top of llm/train_grpo.py to the following:
# ===== B=5 (Five Base Stations) GRPO Hyperparameters (matches current code) =====
MAX_COMPLETION_LENGTH = 128
MAX_PROMPT_LENGTH = 4096
MAX_SEQ_LENGTH = MAX_PROMPT_LENGTH + MAX_COMPLETION_LENGTH
LORA_RANK = 32
LORA_ALPHA = 32
NUM_BASE_STATIONS = 5
NUM_USERS = 40
NUM_CONTENTS = 100
CACHE_SIZES = [10, 10, 10, 10, 10]
ZIPF_PARAM = 1.2
NUM_EPOCHS_FOR_GRPO_DATA = 10000
FUTURE_STEPS_REWARD = 10
FUTURE_STEPS_SAMPLING = 5
GRPO_DATA_SEED = 4567
TRAINER_SEED = 42
ANNEAL_STRATEGY = "cosine"
ANNEAL_TEMP_START = 0.90
ANNEAL_TEMP_END = 0.75
ANNEAL_TOP_P_START = 0.95
ANNEAL_TOP_P_END = 0.85
NUM_GENERATIONS = 4
TEMPERATURE = ANNEAL_TEMP_START
TOP_P = ANNEAL_TOP_P_START
TOP_K = 50
REPETITION_PENALTY = 1.0
LEARNING_RATE = 1e-4
PER_DEVICE_TRAIN_BATCH = 4
GRAD_ACCUM_STEPS = 12
NUM_TRAIN_EPOCHS = 1
LOGGING_STEPS = 1
SAVE_STEPS = 100
MAX_GRAD_NORM = 0.1
DEBUG_SAMPLING_SMOKE_TEST = True
PRINT_ALL_GROUPS = True
MAX_GROUPS_TO_PRINT = 3
SHOW_GLOBAL_BEST_WORST = True
RESET_CACHE_EVERY = 1500
REWARD_HIT_MODE = "delta_weighted"
GAIN_SCALE = 1.0
EARLY_PHASE_STEPS = 120
NOOP_PENALTY_LATE = 0.0075
NOOP_PENALTY_EARLY_WITH_OPP = 0.01
NOOP_PENALTY_EARLY_NO_OPP = 0.005
MAX_JOINT_COMBOS_PER_STEP = 50000
UNCOND_NOOP_PENALTY_STEPS = 30
UNCOND_NOOP_PENALTY = 0.0075
OPP_EVAL_STEPS = 5
OPP_DISCOUNT_GAMMA = 0.9
SLOT_INDEX_BASE = 0
ANNEAL_ENABLED = bool(int(os.getenv("ANNEAL_ENABLED", "1")))
ANNEAL_LOG_EVERY = int(os.getenv("ANNEAL_LOG_EVERY", "5")))Run:
export GRPO_MODEL_PATH=models/merge7B_exbert
python llm/train_grpo.pyUnified evaluation entry script: llm/evaluate_unified.py
It evaluates multiple strategies on the same frozen dataset (same requests & connectivity) and writes results as JSON.
By default, evaluation uses greedy decoding.
Common arguments:
--num_base_stations: number of base stations (2 or 5)--cache_sizes: cache size per base station (comma-separated), e.g.10,10or10,10,10,10,10--num_contents: number of contents (default 100)--num_users_list: list of user counts to test (comma-separated), e.g. to test only 40 users use40--sac_ckpt: SAC baseline checkpoint (.pt) or directory (auto-selectssac_final.pt/ latest step); defaults tosac_baseline_B5/sac_final.pt--grpo_five_lora_dir/--grpo_ten_lora_dir: if you want to evaluate LLMs (SFT/GRPO) together, provide the LoRA directories--num_steps: number of steps per evaluation (default 300)--num_seeds: number of seeds to average (default 3)
Output location:
- Results are written to
outputs/evaluation_outputs/(can be overridden with--output_dir) - Filenames automatically include the user-count suffix, e.g.
evaluation_results_users40.json
- Prepare (optional) LLM:
EVAL_MODEL_PATHpoints to your base/merged model directory (Hugging Face format)--grpo_five_lora_dir/--grpo_ten_lora_dirpoint to the corresponding LoRA adapter directories
- Prepare SAC checkpoint (if you want SAC comparison):
- Suppose your weights are in
sac_baseline_B2/sac_final.pt(if not, specify the actual path via--sac_ckpt)
- Run example (test only 20 users):
python llm/evaluate_unified.py --output eval_b2.json --num_base_stations 2 --cache_sizes 10,10 --num_contents 100 --num_users_list 20 --sac_ckpt sac_baseline_B2/sac_final.ptYour existing SAC weights directory: sac_baseline_B5/ (contains sac_final.pt and multiple sac_step_*.pt)
Run example (test only 40 users):
python llm/evaluate_unified.py --output eval_b5.json --num_base_stations 5 --cache_sizes 10,10,10,10,10 --num_contents 100 --num_users_list 40 --sac_ckpt sac_baseline_B5If you also want to evaluate LLM (SFT/GRPO) simultaneously, append to the above command:
--grpo_five_lora_dir <your GRPO_FIVE adapter directory>--grpo_ten_lora_dir <your GRPO_TEN adapter directory>
LLM outputs are strictly parsed by cache_env/action_validator.py. By default, only two types of actions are allowed (one line per base station):
- Replace:
Base station X’s decision is: use content Y to replace content W in slot Z. - No-op:
Base station X’s decision is: do not perform any caching operation.
If the output format does not satisfy constraints (wrong number of lines, out-of-range IDs, replacement content not from the current request, etc.), that sample is considered invalid and will use fallback logic.
Run according to the README under baselines/sac_baseline/, for example:
cd baselines/sac_baseline
python main.py --automatic_entropy_tuning True --target_update_interval 1000 --lr 1e-4 --exp-case case3 --cudaIf you find our research helpful, please consider citing the following papers:
Cooperative Edge Caching with Large Language Model in Wireless Networks. > Ning Yang, Wentao Wang, Lingtao Ouyang, and Haijun Zhang. arXiv preprint arXiv:2602.13307, 2026.
[Paper]
Click to expand BibTeX
@misc{yang2026cooperative,
title = {Cooperative Edge Caching with Large Language Model in Wireless Networks},
author = {Ning Yang and Wentao Wang and Lingtao Ouyang and Haijun Zhang},
year = {2026},
eprint = {2602.13307},
archivePrefix = {arXiv},
primaryClass = {cs.NI},
url = {[https://arxiv.org/abs/2602.13307](https://arxiv.org/abs/2602.13307)}
}