Upcycled smolLM to convert a Dense LM to an MoE. CPT'd the model to train the router. Two sub experiments, first, using only 1% of the total available training data to cpt, built selection and training regime to choose the 1% of the samples to train on. CPT'd the MoE with custom loss and training regime to force Expert specialisation.
0
stars
1
commits
Python
primary language
Jun 23, 2026
updated
I built a compact SmolLM-style Mixture-of-Experts pipeline end to end: a custom decoder/MoE backbone, dense-to-MoE sparse upcycling from SmolLM weights, small continued pretraining, and two research extensions. The Cosmopedia track studies whether 1000 carefully selected examples can improve continued pretraining efficiency, while the Nemotron track trains chat/code/math expert specialization with a staged curriculum.
This repository is a from-scratch implementation and experimental study of a small SmolLM-style Mixture-of-Experts language model. It was built for the Cohere Labs Scholars take-home assignment, but the project is organized as a complete engineering and research pipeline rather than as a notebook-only exercise.
The project starts with a custom Transformer/MoE implementation, initializes it from a dense SmolLM checkpoint through sparse upcycling, continues pretraining the upcycled model, and then uses the resulting checkpoint for two open-ended experiments:
cosmopedia-100k
and measure whether data selection improves continued pretraining
efficiency.This repository is the cleaned public project root. Large model checkpoints,
raw datasets, and generated per-row metric files are intentionally excluded; see
ARTIFACTS.md for the expected local artifact locations when reproducing full
runs.
I implemented a compact SmolLM-style causal language model with:
| Component | Value |
|---|---|
| Transformer layers | 30 |
| Hidden size | 576 |
| MLP/intermediate size | 1536 |
| Query heads | 9 |
| KV heads | 3 |
| Experts per MoE layer | 3 |
| Experts selected per token | 1 |
| Vocabulary size | 49152 |
| Approximate parameters | 322M |
The dense SmolLM checkpoint is loaded from:
smolLM_Dense_weights/
The upcycled MoE checkpoint is:
upcycled_smolMoE/upcycled_smolMoE.pt
The continued-pretrained MoE checkpoint used by the later experiments is:
upcycled_smolMoE/continued_pretrained_smolMoE.pt
Detailed report:
efficient_training/README.md
Best heldout result from the downloaded Kaggle outputs:
| Checkpoint | Heldout token-weighted CE | Heldout token-weighted PPL |
|---|---|---|
| CPT baseline | 2.2861 | 9.8368 |
| Best one-epoch run, random seed 42 | 1.8749 | 6.5202 |
| Best overall, sqrt-diverse epoch 3 | 1.8107 | 6.1147 |
The best overall Cosmopedia run reduced token-weighted CE by 20.80% and
token-weighted perplexity by 37.84% relative to the CPT baseline.
The key finding: random in-domain training is already a strong baseline, but a diversity-aware, moderately hard selection becomes best after a few epochs. The best stopping point was epoch 3; more epochs over the same 1000 rows overfit.
Detailed report:
nemotron_specialization/README.md
Final held-out summary from the downloaded Kaggle outputs:
| Phase | Overall CE | Overall PPL | Router accuracy |
|---|---|---|---|
| Baseline CPT MoE | 2.1410 | 8.5080 | 0.3269 |
| After phase 1 | 1.8503 | 6.3614 | 0.3302 |
| After phase 2 | 1.6944 | 5.4431 | 0.8245 |
| After phase 3 | 1.6537 | 5.2263 | 0.8500 |
The key finding: a staged curriculum with forced routing, router annealing, and fully learned routing significantly improved both language-modeling loss and domain-router alignment.
The assignment had four coding-challenge parts:
SmolMoELM.This project treats those parts as a single build:
custom MoE architecture
-> dense checkpoint upcycling
-> continued pretraining
-> dataset intervention research
-> expert specialization research
The original assignment described the first part as a debugging challenge. In this repository, the work is presented as a from-scratch model implementation and validation pipeline: the model architecture, routing behavior, upcycling logic, training loop, metrics, and open-ended experiments are all included as explicit code and artifacts.
.
README.md
ARTIFACTS.md
assignment.md
moe/
backbone.py
utils/
config.py
dataset.py
load_model.py
utils.py
smolLM_Dense_weights/
...
upcycle.py
train_router.py
test_moe_backbone.py
upcycled_smolMoE/
README.md
cpt_metrics.png
gini_spec.png
cpt.txt
config.txt
efficient_training/
README.md
...
nemotron_specialization/
README.md
...
The two Part 4 experiments are intentionally isolated:
efficient_training/ contains the Cosmopedia dataset-intervention work.nemotron_specialization/ contains the Nemotron expert-specialization work.This keeps the base model/upcycling pipeline separate from the research extensions.
The core model is implemented in:
moe/backbone.py
The model is a decoder-only causal language model inspired by SmolLM/Llama-style building blocks:
lm_head.The implementation centers on three classes:
| Class | Purpose |
|---|---|
MoE | Top-k sparse expert MLP block with router, expert banks, and load-balancing statistics. |
LlamaDecoder | One decoder block containing attention, normalization, MoE MLP, and residual connections. |
smolMoELM | Full causal LM wrapper returning logits and exposing expert-utilization metrics. |
The MoE layer replaces the dense feed-forward MLP with expert banks:
gate_bank: E x D x H
up_bank: E x D x H
down_bank: E x H x D
where:
E = number of experts
D = hidden size
H = intermediate size
For each token, the router produces expert logits:
router_logits = gate(hidden_state)
The layer selects the top k experts per token, applies SwiGLU-style expert
MLPs, gathers the selected expert outputs, and combines them with the selected
router probabilities.
In this assignment configuration:
num_experts = 3
num_experts_per_tok = 1
so each token routes to one expert.
The attention block uses:
num_heads = 9
kv_heads = 3
This gives grouped-query attention: key/value heads are repeated to match the number of query heads. Rotary embeddings are applied to query and key states.
The MoE layer computes a Switch Transformer-style load-balancing auxiliary term. For each layer:
load_i = fraction of tokens assigned to expert i
importance_i = mean router probability mass for expert i
L_lb = E * sum_i(load_i * importance_i)
The model exposes:
expert_utilization_per_layer, lb_loss = model.get_expert_utilization()
This is used in the base continued-pretraining loop to monitor whether routing is collapsing or staying balanced.
Basic model validation lives in:
test_moe_backbone.py
It instantiates the model, loads provided trial weights, generates from a test prompt, and checks that the load-balancer loss matches the expected value.
Upcycling is implemented in:
upcycle.py
The goal is to initialize the custom MoE from an already useful dense SmolLM checkpoint instead of training the MoE from random weights.
This follows the sparse-upcycling idea from Sparse Upcycling: Training Mixture-of-Experts from Dense Checkpoints: reuse a dense checkpoint, copy useful dense weights into a sparse MoE layout, and continue training from that stronger initialization.
The dense checkpoint is:
smolLM_Dense_weights/
The upcycling process copies:
| Dense component | MoE destination |
|---|---|
| token embeddings | model.embed_tokens |
| final norm | model.norm |
| LM head | lm_head |
| attention q/k/v/o projections | custom attention projections |
| input layernorm | pre-attention RMSNorm |
| post-attention layernorm | pre-MoE RMSNorm |
| dense MLP gate/up/down projections | every expert bank |
The important design choice is that every expert initially receives the same dense MLP weights:
expert 0 = dense MLP
expert 1 = dense MLP
expert 2 = dense MLP
That makes the upcycled MoE behave like the dense model at initialization, provided the router chooses one of the identical experts. The router weights are zeroed so the top-1 selection deterministically chooses expert 0 in eval mode.
This gives a stable starting point:
dense model knowledge
-> copied into shared blocks
-> copied into all expert MLPs
-> router starts simple
The saved output is:
upcycled_smolMoE/upcycled_smolMoE.pt
Continued pretraining is implemented in:
train_router.py
This stage starts from the upcycled checkpoint:
upcycled_smolMoE/upcycled_smolMoE.pt
and produces:
upcycled_smolMoE/continued_pretrained_smolMoE.pt
The base CPT run uses
HuggingFaceTB/cosmopedia-100k
through:
utils/dataset.py
The dataset utility:
text column,For the base CPT run:
max_samples = 1000
block_size = 256
val_fraction = 0.2
batch_size = 4
steps = 100
This stage is deliberately small. It is meant to adapt the upcycled MoE and exercise the router/expert path, not to perform large-scale pretraining.
The base CPT objective is:
loss = causal_lm_ce + LB_ALPHA * load_balancing_loss
with:
LB_ALPHA = 0.01
The causal LM loss predicts the next token over the whole fixed-length text block. The load-balancing loss nudges expert usage away from pathological collapse.
For Part 3, I implemented a Gini-style specialization metric:
specialization = (sum_i p_i^2 - 1/E) / (1 - 1/E)
where p_i is the expert load distribution for a layer and E is the number
of experts.
Interpretation:
0% = uniform expert usage
100% = all tokens routed to a single expert
This is not a "higher is always better" metric. It is a compact way to track whether routing is uniform, specialized, or collapsed. In the later Nemotron experiment, specialization is evaluated more directly with domain-by-expert routing matrices and router accuracy.
upcycled_smolMoE/cpt_metrics.png
upcycled_smolMoE/gini_spec.png
upcycled_smolMoE/cpt.txt
upcycled_smolMoE/continued_pretrained_smolMoE.pt
The saved CPT log shows validation CE around 2.136 by step 100 and confirms
that the model still generates coherent text after continued pretraining.
Detailed report:
efficient_training/README.md
This subproject answers:
If only 1000 training examples can be used, can we process the full
[cosmopedia-100k](https://huggingface.co/datasets/HuggingFaceTB/cosmopedia-100k)
dataset and choose those examples better?
The efficient_training/ directory includes:
| File or directory | Purpose |
|---|---|
compute_cosmopedia_metrics.py | Computes token counts, empirical token entropy, dense CE, and PPL over Cosmopedia rows. |
embed_cosmopedia_diversity.py | Embeds rows with Snowflake Arctic Embed, clusters them, and builds a diversity map. |
select_cosmopedia_diverse_ce.py | Builds the sqrt-cluster, moderate-difficulty curated subset. |
select_cosmopedia_extra_ablations.py | Builds the low/mid CE and high-token-entropy ablation subsets. |
select_cosmopedia_random_extra_seeds.py | Builds additional random baselines. |
train_cosmopedia_cpt_cuda.py | DDP CUDA trainer for the selected 1000-row subsets. |
eval_cosmopedia_cpt_cuda.py | Heldout CE/PPL evaluator. |
run_cosmopedia_followups_kaggle.sh | Orchestrates extra random seeds and multi-epoch followups. |
The tested 1000-sample sets were:
The strongest selection idea was the sqrt-diverse set:
quota_c proportional to sqrt(N_c)
where N_c is the size of embedding cluster c.
Within each cluster, rows were filtered to:
token_count in [512, 1536]
dense CE between cluster-local p50 and p85
This protects smaller semantic clusters while avoiding extreme hard examples and extreme sequence lengths.
| Checkpoint | Token-weighted CE | Token-weighted PPL |
|---|---|---|
| CPT baseline | 2.2861 | 9.8368 |
| Best one-epoch random | 1.8749 | 6.5202 |
| Best overall sqrt-diverse epoch 3 | 1.8107 | 6.1147 |
The detailed README includes plots:
efficient_training/report_figures/
and compact result tables:
efficient_training/report_tables/
The biggest lesson is that random in-domain continued training is a strong baseline. The curated selector did not beat random after one epoch, but it did become the best checkpoint after three epochs.
The result is not "hardest rows win." In fact:
This suggests that small-data continued pretraining needs a balance of:
domain relevance + semantic coverage + moderate difficulty + length control
Detailed report:
nemotron_specialization/README.md
This subproject answers:
Can the existing three experts specialize on chat, code, and math SFT data
rather than routing uniformly?
The nemotron_specialization/ directory is a self-contained training pipeline:
| Area | Purpose |
|---|---|
scripts/ | Download and normalize Nemotron chat/code/math SFT data. |
data_selected/ | Balanced 39k/domain selected data. |
data_splits/ | 37k/1k/1k per-domain train/val/test splits. |
modeling/ | Local MoE model copy with controllable routing modes. |
training/ | DDP training, metrics, losses, schedules, checkpoints, evaluation. |
analysis/ | Summary CSVs and plots from the completed run. |
train.py | Main launcher. |
config.py | Central knobs; no argparse-heavy interface. |
The original backbone outside the directory is not edited. The experiment uses a local model copy so the specialization work stays isolated.
Source:
nvidia/Llama-Nemotron-Post-Training-Dataset
Domains:
chat
code
math
Balanced selected data:
| Domain | Samples |
|---|---|
| chat | 39,000 |
| code | 39,000 |
| math | 39,000 |
Split:
| Split | Samples/domain | Total |
|---|---|---|
| train | 37,000 | 111,000 |
| val | 1,000 | 3,000 |
| test | 1,000 | 3,000 |
The completed Kaggle run used smaller disjoint phase slices for time reasons, as documented in the subdirectory README.
The three experts are assigned fixed intended domains:
DOMAIN_TO_EXPERT = {
"chat": 0,
"code": 1,
"math": 2,
}
The specialization method has three phases:
| Phase | Routing | Trainable modules | Purpose |
|---|---|---|---|
| Phase 1 | forced domain routing | experts only | Give each expert direct domain-specific practice. |
| Phase 2 | mixed routing with teacher-forcing anneal | router + experts | Teach router to recover domain assignment. |
| Phase 3 | fully learned routing | router + experts | Remove teacher forcing and regularize specialization. |
Phase 3 includes:
The orthogonality term is behavioral: it pushes average router distributions for chat/code/math away from each other. It does not directly penalize expert weight similarity.
| Phase | Overall CE | Overall PPL | Router accuracy |
|---|---|---|---|
| Baseline | 2.1410 | 8.5080 | 0.3269 |
| After phase 1 | 1.8503 | 6.3614 | 0.3302 |
| After phase 2 | 1.6944 | 5.4431 | 0.8245 |
| After phase 3 | 1.6537 | 5.2263 | 0.8500 |
The important qualitative shift is that phase 1 improves LM loss through forced expert practice, while phase 2 is where the router learns the domain mapping. Phase 3 improves both CE/PPL and router accuracy further under fully learned routing.
The detailed README links the analysis plots:
nemotron_specialization/analysis/
The base MoE lives in:
moe/backbone.py
The Cosmopedia CUDA trainer uses a local patched model copy under:
efficient_training/cuda_modeling/
The Nemotron curriculum uses another local copy under:
nemotron_specialization/modeling/
This is intentional. The open-ended experiments required CUDA dtype fixes, forced routing, router logits, and extra routing metadata. Instead of mutating the base assignment implementation repeatedly, the experimental model copies keep each research track self-contained.
The project uses:
The metrics are simple enough to inspect manually and specific enough to answer the assignment questions.
The project was developed across a local Mac with MPS and Kaggle T4 GPUs.
Important constraints:
The final stable training setup for Cosmopedia used:
2 x T4
block_size = 1024
batch_size_per_gpu = 1
grad_accum_steps = 4
effective_batch = 8
The Nemotron completed run similarly used smaller phase slices than the full planned 37k/domain training split.
The 3000-row heldout set was used for both validation and model selection in the follow-up runs. A stricter final report should split it into:
1000 validation rows
2000 final test rows
and only use the 2000-row test set once at the end.
Model checkpoints, exported model binaries, raw dataset shards, selected training parquets, caches, and large per-row eval files are intentionally not tracked. The no-weights result artifacts are present, including logs, configs, metrics, plots, and compact CSV/JSON summaries. The README reports are based on those downloaded no-weights outputs.
The custom backbone is intentionally direct and readable. It does not use FlashAttention, sequence packing, activation checkpointing, fused kernels, or other production training optimizations.
The project evaluates CE/PPL and router behavior. It does not run broad downstream tasks, human evaluation, or generation-quality benchmarks.
python test_moe_backbone.py
python upcycle.py
This writes:
upcycled_smolMoE/upcycled_smolMoE.pt
python train_router.py
This writes:
upcycled_smolMoE/continued_pretrained_smolMoE.pt
See:
efficient_training/README.md
Key scripts:
python efficient_training/compute_cosmopedia_metrics.py
python efficient_training/embed_cosmopedia_diversity.py
python efficient_training/select_cosmopedia_diverse_ce.py
python efficient_training/train_cosmopedia_cpt_cuda.py
python efficient_training/eval_cosmopedia_cpt_cuda.py
The final report artifacts are:
efficient_training/report_figures/
efficient_training/report_tables/
efficient_training/experimentResults/
See:
nemotron_specialization/README.md
Main launcher:
python nemotron_specialization/train.py
Analysis artifacts:
nemotron_specialization/analysis/
Recommended order:
moe/backbone.py to understand the custom architecture.upcycle.py to see how dense weights become MoE weights.train_router.py for the base CPT loop and MoE metric.efficient_training/README.md for the dataset-intervention research.nemotron_specialization/README.md for the expert-specialization
research.The two subdirectory READMEs are intentionally detailed. This root README is the project map and high-level technical narrative; the sub-READMEs are the experiment logs and analysis reports.
This project builds a small MoE language-model pipeline end to end:
custom architecture
-> dense weight transfer
-> continued pretraining
-> data selection research
-> expert specialization research
The Cosmopedia work shows that data selection matters, but simple random continued training is a serious baseline. The best intervention was not the hardest examples or the highest-entropy examples, but a diversity-aware, moderately hard subset with early stopping.
The Nemotron work shows that expert specialization can be trained directly with a staged curriculum. Forced expert warmup improves the experts, router annealing teaches domain routing, and fully learned routing with light regularization produces strong router/domain alignment.
Together, the two experiments show the same broader lesson: with a small upcycled MoE, the useful question is not only "does the loss go down?" It is "what data or routing signal made the model improve, and can we show that improvement with the right heldout and MoE-specific metrics?"
1 commits
Python
97.1%
Shell
2.9%
Upcycled smolLM to convert a Dense LM to an MoE. CPT'd the model to train the router. Two sub experiments, first, using only 1% of the total available training data to cpt, built selection and training regime to choose the 1% of the samples to train on. CPT'd the MoE with custom loss and training regime to force Expert specialisation.
0
stars
1
commits
Python
primary language
Jun 23, 2026
updated
I built a compact SmolLM-style Mixture-of-Experts pipeline end to end: a custom decoder/MoE backbone, dense-to-MoE sparse upcycling from SmolLM weights, small continued pretraining, and two research extensions. The Cosmopedia track studies whether 1000 carefully selected examples can improve continued pretraining efficiency, while the Nemotron track trains chat/code/math expert specialization with a staged curriculum.
This repository is a from-scratch implementation and experimental study of a small SmolLM-style Mixture-of-Experts language model. It was built for the Cohere Labs Scholars take-home assignment, but the project is organized as a complete engineering and research pipeline rather than as a notebook-only exercise.
The project starts with a custom Transformer/MoE implementation, initializes it from a dense SmolLM checkpoint through sparse upcycling, continues pretraining the upcycled model, and then uses the resulting checkpoint for two open-ended experiments:
cosmopedia-100k
and measure whether data selection improves continued pretraining
efficiency.This repository is the cleaned public project root. Large model checkpoints,
raw datasets, and generated per-row metric files are intentionally excluded; see
ARTIFACTS.md for the expected local artifact locations when reproducing full
runs.
I implemented a compact SmolLM-style causal language model with:
| Component | Value |
|---|---|
| Transformer layers | 30 |
| Hidden size | 576 |
| MLP/intermediate size | 1536 |
| Query heads | 9 |
| KV heads | 3 |
| Experts per MoE layer | 3 |
| Experts selected per token | 1 |
| Vocabulary size | 49152 |
| Approximate parameters | 322M |
The dense SmolLM checkpoint is loaded from:
smolLM_Dense_weights/
The upcycled MoE checkpoint is:
upcycled_smolMoE/upcycled_smolMoE.pt
The continued-pretrained MoE checkpoint used by the later experiments is:
upcycled_smolMoE/continued_pretrained_smolMoE.pt
Detailed report:
efficient_training/README.md
Best heldout result from the downloaded Kaggle outputs:
| Checkpoint | Heldout token-weighted CE | Heldout token-weighted PPL |
|---|---|---|
| CPT baseline | 2.2861 | 9.8368 |
| Best one-epoch run, random seed 42 | 1.8749 | 6.5202 |
| Best overall, sqrt-diverse epoch 3 | 1.8107 | 6.1147 |
The best overall Cosmopedia run reduced token-weighted CE by 20.80% and
token-weighted perplexity by 37.84% relative to the CPT baseline.
The key finding: random in-domain training is already a strong baseline, but a diversity-aware, moderately hard selection becomes best after a few epochs. The best stopping point was epoch 3; more epochs over the same 1000 rows overfit.
Detailed report:
nemotron_specialization/README.md
Final held-out summary from the downloaded Kaggle outputs:
| Phase | Overall CE | Overall PPL | Router accuracy |
|---|---|---|---|
| Baseline CPT MoE | 2.1410 | 8.5080 | 0.3269 |
| After phase 1 | 1.8503 | 6.3614 | 0.3302 |
| After phase 2 | 1.6944 | 5.4431 | 0.8245 |
| After phase 3 | 1.6537 | 5.2263 | 0.8500 |
The key finding: a staged curriculum with forced routing, router annealing, and fully learned routing significantly improved both language-modeling loss and domain-router alignment.
The assignment had four coding-challenge parts:
SmolMoELM.This project treats those parts as a single build:
custom MoE architecture
-> dense checkpoint upcycling
-> continued pretraining
-> dataset intervention research
-> expert specialization research
The original assignment described the first part as a debugging challenge. In this repository, the work is presented as a from-scratch model implementation and validation pipeline: the model architecture, routing behavior, upcycling logic, training loop, metrics, and open-ended experiments are all included as explicit code and artifacts.
.
README.md
ARTIFACTS.md
assignment.md
moe/
backbone.py
utils/
config.py
dataset.py
load_model.py
utils.py
smolLM_Dense_weights/
...
upcycle.py
train_router.py
test_moe_backbone.py
upcycled_smolMoE/
README.md
cpt_metrics.png
gini_spec.png
cpt.txt
config.txt
efficient_training/
README.md
...
nemotron_specialization/
README.md
...
The two Part 4 experiments are intentionally isolated:
efficient_training/ contains the Cosmopedia dataset-intervention work.nemotron_specialization/ contains the Nemotron expert-specialization work.This keeps the base model/upcycling pipeline separate from the research extensions.
The core model is implemented in:
moe/backbone.py
The model is a decoder-only causal language model inspired by SmolLM/Llama-style building blocks:
lm_head.The implementation centers on three classes:
| Class | Purpose |
|---|---|
MoE | Top-k sparse expert MLP block with router, expert banks, and load-balancing statistics. |
LlamaDecoder | One decoder block containing attention, normalization, MoE MLP, and residual connections. |
smolMoELM | Full causal LM wrapper returning logits and exposing expert-utilization metrics. |
The MoE layer replaces the dense feed-forward MLP with expert banks:
gate_bank: E x D x H
up_bank: E x D x H
down_bank: E x H x D
where:
E = number of experts
D = hidden size
H = intermediate size
For each token, the router produces expert logits:
router_logits = gate(hidden_state)
The layer selects the top k experts per token, applies SwiGLU-style expert
MLPs, gathers the selected expert outputs, and combines them with the selected
router probabilities.
In this assignment configuration:
num_experts = 3
num_experts_per_tok = 1
so each token routes to one expert.
The attention block uses:
num_heads = 9
kv_heads = 3
This gives grouped-query attention: key/value heads are repeated to match the number of query heads. Rotary embeddings are applied to query and key states.
The MoE layer computes a Switch Transformer-style load-balancing auxiliary term. For each layer:
load_i = fraction of tokens assigned to expert i
importance_i = mean router probability mass for expert i
L_lb = E * sum_i(load_i * importance_i)
The model exposes:
expert_utilization_per_layer, lb_loss = model.get_expert_utilization()
This is used in the base continued-pretraining loop to monitor whether routing is collapsing or staying balanced.
Basic model validation lives in:
test_moe_backbone.py
It instantiates the model, loads provided trial weights, generates from a test prompt, and checks that the load-balancer loss matches the expected value.
Upcycling is implemented in:
upcycle.py
The goal is to initialize the custom MoE from an already useful dense SmolLM checkpoint instead of training the MoE from random weights.
This follows the sparse-upcycling idea from Sparse Upcycling: Training Mixture-of-Experts from Dense Checkpoints: reuse a dense checkpoint, copy useful dense weights into a sparse MoE layout, and continue training from that stronger initialization.
The dense checkpoint is:
smolLM_Dense_weights/
The upcycling process copies:
| Dense component | MoE destination |
|---|---|
| token embeddings | model.embed_tokens |
| final norm | model.norm |
| LM head | lm_head |
| attention q/k/v/o projections | custom attention projections |
| input layernorm | pre-attention RMSNorm |
| post-attention layernorm | pre-MoE RMSNorm |
| dense MLP gate/up/down projections | every expert bank |
The important design choice is that every expert initially receives the same dense MLP weights:
expert 0 = dense MLP
expert 1 = dense MLP
expert 2 = dense MLP
That makes the upcycled MoE behave like the dense model at initialization, provided the router chooses one of the identical experts. The router weights are zeroed so the top-1 selection deterministically chooses expert 0 in eval mode.
This gives a stable starting point:
dense model knowledge
-> copied into shared blocks
-> copied into all expert MLPs
-> router starts simple
The saved output is:
upcycled_smolMoE/upcycled_smolMoE.pt
Continued pretraining is implemented in:
train_router.py
This stage starts from the upcycled checkpoint:
upcycled_smolMoE/upcycled_smolMoE.pt
and produces:
upcycled_smolMoE/continued_pretrained_smolMoE.pt
The base CPT run uses
HuggingFaceTB/cosmopedia-100k
through:
utils/dataset.py
The dataset utility:
text column,For the base CPT run:
max_samples = 1000
block_size = 256
val_fraction = 0.2
batch_size = 4
steps = 100
This stage is deliberately small. It is meant to adapt the upcycled MoE and exercise the router/expert path, not to perform large-scale pretraining.
The base CPT objective is:
loss = causal_lm_ce + LB_ALPHA * load_balancing_loss
with:
LB_ALPHA = 0.01
The causal LM loss predicts the next token over the whole fixed-length text block. The load-balancing loss nudges expert usage away from pathological collapse.
For Part 3, I implemented a Gini-style specialization metric:
specialization = (sum_i p_i^2 - 1/E) / (1 - 1/E)
where p_i is the expert load distribution for a layer and E is the number
of experts.
Interpretation:
0% = uniform expert usage
100% = all tokens routed to a single expert
This is not a "higher is always better" metric. It is a compact way to track whether routing is uniform, specialized, or collapsed. In the later Nemotron experiment, specialization is evaluated more directly with domain-by-expert routing matrices and router accuracy.
upcycled_smolMoE/cpt_metrics.png
upcycled_smolMoE/gini_spec.png
upcycled_smolMoE/cpt.txt
upcycled_smolMoE/continued_pretrained_smolMoE.pt
The saved CPT log shows validation CE around 2.136 by step 100 and confirms
that the model still generates coherent text after continued pretraining.
Detailed report:
efficient_training/README.md
This subproject answers:
If only 1000 training examples can be used, can we process the full
[cosmopedia-100k](https://huggingface.co/datasets/HuggingFaceTB/cosmopedia-100k)
dataset and choose those examples better?
The efficient_training/ directory includes:
| File or directory | Purpose |
|---|---|
compute_cosmopedia_metrics.py | Computes token counts, empirical token entropy, dense CE, and PPL over Cosmopedia rows. |
embed_cosmopedia_diversity.py | Embeds rows with Snowflake Arctic Embed, clusters them, and builds a diversity map. |
select_cosmopedia_diverse_ce.py | Builds the sqrt-cluster, moderate-difficulty curated subset. |
select_cosmopedia_extra_ablations.py | Builds the low/mid CE and high-token-entropy ablation subsets. |
select_cosmopedia_random_extra_seeds.py | Builds additional random baselines. |
train_cosmopedia_cpt_cuda.py | DDP CUDA trainer for the selected 1000-row subsets. |
eval_cosmopedia_cpt_cuda.py | Heldout CE/PPL evaluator. |
run_cosmopedia_followups_kaggle.sh | Orchestrates extra random seeds and multi-epoch followups. |
The tested 1000-sample sets were:
The strongest selection idea was the sqrt-diverse set:
quota_c proportional to sqrt(N_c)
where N_c is the size of embedding cluster c.
Within each cluster, rows were filtered to:
token_count in [512, 1536]
dense CE between cluster-local p50 and p85
This protects smaller semantic clusters while avoiding extreme hard examples and extreme sequence lengths.
| Checkpoint | Token-weighted CE | Token-weighted PPL |
|---|---|---|
| CPT baseline | 2.2861 | 9.8368 |
| Best one-epoch random | 1.8749 | 6.5202 |
| Best overall sqrt-diverse epoch 3 | 1.8107 | 6.1147 |
The detailed README includes plots:
efficient_training/report_figures/
and compact result tables:
efficient_training/report_tables/
The biggest lesson is that random in-domain continued training is a strong baseline. The curated selector did not beat random after one epoch, but it did become the best checkpoint after three epochs.
The result is not "hardest rows win." In fact:
This suggests that small-data continued pretraining needs a balance of:
domain relevance + semantic coverage + moderate difficulty + length control
Detailed report:
nemotron_specialization/README.md
This subproject answers:
Can the existing three experts specialize on chat, code, and math SFT data
rather than routing uniformly?
The nemotron_specialization/ directory is a self-contained training pipeline:
| Area | Purpose |
|---|---|
scripts/ | Download and normalize Nemotron chat/code/math SFT data. |
data_selected/ | Balanced 39k/domain selected data. |
data_splits/ | 37k/1k/1k per-domain train/val/test splits. |
modeling/ | Local MoE model copy with controllable routing modes. |
training/ | DDP training, metrics, losses, schedules, checkpoints, evaluation. |
analysis/ | Summary CSVs and plots from the completed run. |
train.py | Main launcher. |
config.py | Central knobs; no argparse-heavy interface. |
The original backbone outside the directory is not edited. The experiment uses a local model copy so the specialization work stays isolated.
Source:
nvidia/Llama-Nemotron-Post-Training-Dataset
Domains:
chat
code
math
Balanced selected data:
| Domain | Samples |
|---|---|
| chat | 39,000 |
| code | 39,000 |
| math | 39,000 |
Split:
| Split | Samples/domain | Total |
|---|---|---|
| train | 37,000 | 111,000 |
| val | 1,000 | 3,000 |
| test | 1,000 | 3,000 |
The completed Kaggle run used smaller disjoint phase slices for time reasons, as documented in the subdirectory README.
The three experts are assigned fixed intended domains:
DOMAIN_TO_EXPERT = {
"chat": 0,
"code": 1,
"math": 2,
}
The specialization method has three phases:
| Phase | Routing | Trainable modules | Purpose |
|---|---|---|---|
| Phase 1 | forced domain routing | experts only | Give each expert direct domain-specific practice. |
| Phase 2 | mixed routing with teacher-forcing anneal | router + experts | Teach router to recover domain assignment. |
| Phase 3 | fully learned routing | router + experts | Remove teacher forcing and regularize specialization. |
Phase 3 includes:
The orthogonality term is behavioral: it pushes average router distributions for chat/code/math away from each other. It does not directly penalize expert weight similarity.
| Phase | Overall CE | Overall PPL | Router accuracy |
|---|---|---|---|
| Baseline | 2.1410 | 8.5080 | 0.3269 |
| After phase 1 | 1.8503 | 6.3614 | 0.3302 |
| After phase 2 | 1.6944 | 5.4431 | 0.8245 |
| After phase 3 | 1.6537 | 5.2263 | 0.8500 |
The important qualitative shift is that phase 1 improves LM loss through forced expert practice, while phase 2 is where the router learns the domain mapping. Phase 3 improves both CE/PPL and router accuracy further under fully learned routing.
The detailed README links the analysis plots:
nemotron_specialization/analysis/
The base MoE lives in:
moe/backbone.py
The Cosmopedia CUDA trainer uses a local patched model copy under:
efficient_training/cuda_modeling/
The Nemotron curriculum uses another local copy under:
nemotron_specialization/modeling/
This is intentional. The open-ended experiments required CUDA dtype fixes, forced routing, router logits, and extra routing metadata. Instead of mutating the base assignment implementation repeatedly, the experimental model copies keep each research track self-contained.
The project uses:
The metrics are simple enough to inspect manually and specific enough to answer the assignment questions.
The project was developed across a local Mac with MPS and Kaggle T4 GPUs.
Important constraints:
The final stable training setup for Cosmopedia used:
2 x T4
block_size = 1024
batch_size_per_gpu = 1
grad_accum_steps = 4
effective_batch = 8
The Nemotron completed run similarly used smaller phase slices than the full planned 37k/domain training split.
The 3000-row heldout set was used for both validation and model selection in the follow-up runs. A stricter final report should split it into:
1000 validation rows
2000 final test rows
and only use the 2000-row test set once at the end.
Model checkpoints, exported model binaries, raw dataset shards, selected training parquets, caches, and large per-row eval files are intentionally not tracked. The no-weights result artifacts are present, including logs, configs, metrics, plots, and compact CSV/JSON summaries. The README reports are based on those downloaded no-weights outputs.
The custom backbone is intentionally direct and readable. It does not use FlashAttention, sequence packing, activation checkpointing, fused kernels, or other production training optimizations.
The project evaluates CE/PPL and router behavior. It does not run broad downstream tasks, human evaluation, or generation-quality benchmarks.
python test_moe_backbone.py
python upcycle.py
This writes:
upcycled_smolMoE/upcycled_smolMoE.pt
python train_router.py
This writes:
upcycled_smolMoE/continued_pretrained_smolMoE.pt
See:
efficient_training/README.md
Key scripts:
python efficient_training/compute_cosmopedia_metrics.py
python efficient_training/embed_cosmopedia_diversity.py
python efficient_training/select_cosmopedia_diverse_ce.py
python efficient_training/train_cosmopedia_cpt_cuda.py
python efficient_training/eval_cosmopedia_cpt_cuda.py
The final report artifacts are:
efficient_training/report_figures/
efficient_training/report_tables/
efficient_training/experimentResults/
See:
nemotron_specialization/README.md
Main launcher:
python nemotron_specialization/train.py
Analysis artifacts:
nemotron_specialization/analysis/
Recommended order:
moe/backbone.py to understand the custom architecture.upcycle.py to see how dense weights become MoE weights.train_router.py for the base CPT loop and MoE metric.efficient_training/README.md for the dataset-intervention research.nemotron_specialization/README.md for the expert-specialization
research.The two subdirectory READMEs are intentionally detailed. This root README is the project map and high-level technical narrative; the sub-READMEs are the experiment logs and analysis reports.
This project builds a small MoE language-model pipeline end to end:
custom architecture
-> dense weight transfer
-> continued pretraining
-> data selection research
-> expert specialization research
The Cosmopedia work shows that data selection matters, but simple random continued training is a serious baseline. The best intervention was not the hardest examples or the highest-entropy examples, but a diversity-aware, moderately hard subset with early stopping.
The Nemotron work shows that expert specialization can be trained directly with a staged curriculum. Forced expert warmup improves the experts, router annealing teaches domain routing, and fully learned routing with light regularization produces strong router/domain alignment.
Together, the two experiments show the same broader lesson: with a small upcycled MoE, the useful question is not only "does the loss go down?" It is "what data or routing signal made the model improve, and can we show that improvement with the right heldout and MoE-specific metrics?"
1 commits
Python
97.1%
Shell
2.9%