This project was developed as a technical proposal for the JetBrains Internship: Post-Training Coding LLMs with Frontier Alignment Methods. It demonstrates a complete post-training pipeline that takes a strong base coding model and aligns it to produce clean, maintainable, working code — not just code that passes tests.
Standard alignment approaches for code (RLHF, basic DPO) typically use binary execution feedback: does the code pass the tests? This creates a perverse incentive — the model learns to produce complex, hard-to-read "spaghetti code" that technically works but violates software engineering best practices.
CodeAlign introduces a Composite IDE Reward Signal that combines:
| Component | Signal | Purpose |
|---|---|---|
| Binary (execution success) | Functional correctness | |
| Cyclomatic complexity (continuous) | Code maintainability | |
| Lint violations (per-language linters) | Style adherence |
This aligns the model's objective with the human objective: clean, maintainable, working code.
"Identified a real problem in the literature (reward hacking toward complex code), designed a solution (composite reward), and validated it empirically."
CodeAlign explores execution-grounded alignment for code generation. Instead of relying on static, human-labeled preference data, DPO preference pairs are generated automatically: candidate completions are run in a sandbox and scored on whether they compile and pass tests, with ties broken by static-analysis metrics (cyclomatic complexity, lint score).
The core question this project is testing: does combining execution feedback with static-analysis metrics avoid a known failure mode of execution-only rewards — models that learn to produce code which passes tests but is needlessly complex or unidiomatic ("spaghetti code")?
Pipeline: curated data (syntax + complexity + lint validated) → SFT → DPO preference generation (execution + static analysis) → DPO training → evaluation (Base vs. SFT vs. DPO, including an ablation of execution-only vs. composite reward).
Phases 0–6 are implemented. See the Roadmap below.
- Languages: Python, Java, C++, C#, JavaScript, TypeScript, Go, Rust. These map onto JetBrains' own primary IDE lineup (PyCharm, IntelliJ IDEA, CLion, Rider, WebStorm) — a deliberate choice, not an arbitrary list, and it spans genuinely different paradigms (dynamic scripting, statically-typed OOP, systems-level manual memory, gradually-typed web) rather than eight near-identical languages. Earlier drafts of this project scoped v1 to Python only; that changed once the dataset (see below) turned out to be multi-language by construction and multi-language support became an explicit goal rather than an accident of the data.
- Validation approach: three signals apply across all eight languages — syntax validity (via
tree-sittergrammars, which parse without needing a full compiler toolchain or resolvable imports — important since these are code snippets/diffs, not complete buildable projects; note this is a Concrete Syntax Tree, not an AST, so it's called syntax validation throughout rather than "AST validation"), cyclomatic complexity (vializard, which supports all eight languages out of the box), and lint scoring via per-language tools:ruff(Python),cpplint(C++),eslint(JavaScript and TypeScript),PMD(Java),clippy(Rust),golangci-lint(Go), anddotnet buildwarnings (C#). Each linter is configured to flag semantic bugs and code quality issues rather than pure style violations — per-language thresholds account for differences in tool granularity (seecuration_config.py). The native toolchains (Node, JDK, .NET SDK, Rust, Go) are bundled indocker/Dockerfile.executor. - Duplication: two distinct checks, not one. An internal-duplication / code-smell check flags copy-pasted blocks within a single sample (a DRY violation — is this one piece of code repeating itself). A separate MinHash/LSH near-duplicate index flags samples that are near-identical to another sample already accepted (dataset-level redundancy — protects against wasting training signal on repeated examples). They catch different problems; both are active.
- Base model:
Qwen2.5-Coder-7B-Instruct. Chosen because it is already code-pretrained, dense (single-GPU feasible), and well supported by the Hugging Face / TRL / PEFT stack. A general-purpose instruct model (e.g. Llama-3.1-8B-Instruct) may be added later purely as an ablation baseline, to measure how much starting from a code-specialized model actually matters.
Documented up front so the project's resource footprint is explicit, not an afterthought.
- Hardware target: 1x GPU with 24 GB VRAM — RTX 3090 / 4090 locally, or L4 / RTX A5000 / RTX 3090 on a rental provider (RunPod Community Cloud pricing, ≈$0.30–$0.70/hr depending on GPU model, as of July 2026).
- Training method: QLoRA (4-bit) for both SFT and DPO, via TRL + PEFT + bitsandbytes. Full fine-tuning of a 7B model is out of scope for a single-GPU portfolio budget.
- Estimated compute: ~25–40 GPU-hours total, covering SFT, DPO preference-pair generation (sandboxed execution + scoring), DPO training, and evaluation of all three checkpoints. Data curation (Phase 1) is CPU-bound and not part of this GPU-hour estimate. The range is wide on purpose — it accounts for debugging and re-runs, not just a single idealized pass.
- Estimated cost: ~$10–$30 total.
- These are planning estimates. They will be replaced with measured numbers once Phase 2 (SFT) produces real wall-clock data.
-
Dataset:
bigcode/commitpackft. Real commits from permissively-licensed GitHub repositories, filtered by the BigCode team to keep only commit messages that read as natural-language instructions — used to train OctoCoder in the OctoPack paper. 702K samples across 277 languages, ~1.6 GB. Each sample carries an explicit per-samplelicensefield, and content is real developer code (bug fixes, feature additions), not LLM-synthesized — this sidesteps the OpenAI-terms-of-use ambiguity that came up with the previously-consideredMagicoder-OSS-Instruct-75K(GPT-3.5-generated) and is arguably a better fit for an IDE-assistant use case than isolated algorithmic puzzles. -
License filter: restricted to samples tagged
mit,apache-2.0,bsd-2-clause,bsd-3-clause,isc,unlicense, orcc0-1.0. The dataset's own license list also includesagpl-3.0,lgpl-2.1,epl-1.0,mpl-2.0andunknown— those are copyleft or ambiguous, not permissive in the strict sense, and are excluded even though the dataset card describes the whole set as "permissively licensed." -
Language filter: restricted to the eight languages in scope (see above). Everything else (
yaml,json,markdown,html,css, and 270+ others) is dropped — most of it isn't code in the sense this project cares about anyway. -
Format — two prompt types, not one, both derived from CommitPackFT itself: samples where
old_contentsis empty or trivial (≤3 meaningful lines — a new file) are framed as write-from-spec: instruction only, no code context, target isnew_contents. Samples with substantialold_contentsare framed as in-context edit: existing file + instruction, target isnew_contents. The instruction itself prefersmessage(full commit body) oversubject(short line) whenever it adds real detail.Why both, not just one: in-context editing is realistic IDE-assistant behavior ("fix/refactor this"), but Phase 5's evaluation benchmarks (HumanEval, MBPP) are entirely write-from-spec in shape — training exclusively on in-context edits would leave a real mismatch between the SFT/DPO training distribution and what the project's own evaluation measures. Splitting on
old_contentssize gets both formats from the one dataset already vetted for license and language quality, rather than reintroducing a second dataset (with Magicoder's OpenAI-terms and language-labeling problems) or hand-writing prompts. The split point (NEW_FILE_LINE_THRESHOLDincuration_config.py) is a starting heuristic — Phase 1's report notebook tracks the resultingedit/new_filemix so it can be checked empirically rather than assumed.This replaces the markdown-fence extraction used for the previous dataset candidate —
new_contentsis already raw source, and each language's own config file (not a self-reported per-row label) is what determines its language, so it doesn't need the same defensive handling Magicoder'slangcolumn did. -
Validation: every
new_contentssample passes through the syntax + complexity + lint checks above before entering the SFT set. Samples that fail are discarded — see Phase 1.
- Phase 0 — Setup & scope
- Phase 1 — Data curation (syntax validation + complexity + linting)
- Phase 2 — Supervised fine-tuning (SFT) — see below
- Phase 3 — DPO preference-pair generation (execution feedback + static analysis) — see below
- Phase 4 — DPO training — see below
- Phase 5 — Evaluation (Base vs. SFT vs. DPO + reward-ablation) — see below
- Phase 6 — Interactive demo (side-by-side model comparison) — see below
- Phase 7 (stretch, not blocking) — see below
Training a pre-trained base model on a curated set of (instruction, code) pairs to specialize it for coding — teaching output format and basic coding patterns, turning a general language model into a coding assistant. This is necessary, not optional: a pristine, well-validated dataset (Phase 1's syntax + complexity + lint gates) doesn't mean a base model will code well if simply prompted — instruction tuning is what actually adapts its behavior to the task.
What SFT is not meant to do here: it's not where this project's core research question gets answered. SFT establishes a reasonably competent starting point; DPO with the composite execution + static-analysis reward (Phase 3–4) is where the actual alignment experiment happens. Overinvesting in SFT hyperparameter search would spend the compute budget on the less interesting phase — see the golden-recipe decision below.
- Trainer:
trl.SFTTrainer+ QLoRA (4-bit NF4) onQwen2.5-Coder-7B-Instruct, training only on the Phase 1 "pristine" subset. - Hyperparameters — a fixed "golden recipe", not a sweep. Bayesian search (Optuna) was ruled out on purpose: a single 24GB GPU makes iterating multiple full SFT runs on a 7B model impractical, and that compute is better spent on DPO preference-pair generation, which is where the project's actual contribution lives. Standard-practice LoRA values are used instead — see
src/training/sft_config.pyfor the exact numbers (rank, alpha, dropout, learning rate, optimizer, epochs, seed) and the reasoning behind each. - VRAM budget: QLoRA's 4-bit base (~4.5GB vs. ~14GB in fp16) + gradient checkpointing + a
per_device_train_batch_size=2/gradient_accumulation_steps=8split (effective batch size 16) keeps this inside 24GB — seesrc/notebooks/02_sft_experiments_log.ipynbfor the measured VRAM footprint and W&B loss curves once a run completes. - Evaluation:
bigcode-evaluation-harness— HumanEval (164 hand-written problems;pass@1is the fraction solved on the first attempt) — rather than a hand-rolled eval script. Deliberately run from the terminal against a saved checkpoint, not from insidesft_trainer.py: training and evaluation stay decoupled, and the same harness invocation gets reused for the DPO checkpoint in Phase 5 for a like-for-like comparison. - Checkpoint:
checkpoints/sft/final_model(LoRA adapter + tokenizer).
After SFT training completes, the LoRA adapter is merged back into the base model via src/training/merge_sft.py. This produces checkpoints/sft/merged_model — a full-weight model that DPO (Phase 4) uses as its starting point. This is necessary because DPO needs a frozen reference policy (the SFT model), and applying a second LoRA adapter on top of an existing adapter is not supported — the SFT adapter must be baked into the base weights first.
Execution-Based Iterative DPO: you don't need humans to label "Chosen" vs "Rejected." Using automated execution (sandbox) and statistical analysis (linters) to automatically generate preference pairs for DPO. Execution-based feedback provides a deterministic ground truth, eliminating the subjectivity and high cost of human annotation.
The pipeline loops over every prompt in the Phase 1 pristine dataset — generating two candidate completions from the SFT checkpoint (at temperature=0.7 with top_p=0.95, via num_return_sequences=2), executing both inside a resource-limited Docker container (network_disabled, mem_limit=128m), and labelling the pair according to three cases:
- Model generates two variations of code for a problem
- Run both in a sandbox
- The labelling decision:
- Case A — One passes, one fails. Chosen: the passing one. Rejected: the failing one. DPO update: train the model to prefer the passing logic.
- Case B — Both fail: discard the pair to prevent noisy gradients.
- Case C — Both pass. Rank them by quality metrics: cyclomatic complexity and lint score (the composite reward signal). The superior code becomes chosen.
Moving beyond binary compilation checks to a multi-dimensional reward system. A binary reward (=1 if execution succeeds without runtime errors, 0 otherwise) encourages "spaghetti code" and doesn't guide the model toward good engineering practices. The solution is a Composite Reward:
| Component | Signal | Weight | Purpose |
|---|---|---|---|
| Binary (execution success) | w₁ = 1.0 |
Functional correctness | |
| Negative, proportional to cyclomatic complexity (continuous) | w₂ = 0.1 |
Code quality metrics — force simplicity and adherence to best practices | |
| Penalty for lint violations (per-language linters) | w₃ = 0.2 |
Style adherence |
It aligns the model's objective with the human objective: clean, maintainable, working code. Introducing a continuous penalty for high cyclomatic complexity prevents reward hacking and ensures the model produces maintainable, idiomatic code.
Phase 3 writes two files, mirroring Phase 1's pristine_dataset.jsonl / report_dataset.jsonl split:
data/preferences/dpo_dataset.jsonl— clean pairs (prompt/chosen/rejectedonly), ready forDPOTrainerin Phase 4.data/preferences/dpo_report.jsonl— every prompt processed (including discarded pairs), with full metadata: case classification (A/B/C), composite scores, cyclomatic complexity, and lint errors for both chosen and rejected candidates, plus language. This is whatsrc/notebooks/03_preference_generation_report.ipynbreads to produce the evidence graphs.
W&B logging follows the same wandb.init / wandb.log / wandb.finish pattern used in Phases 2 and 4. Logged metrics:
- Case distribution:
case_a,case_b,case_c,case_c_tied - Pair counts:
pairs_generated,pairs_discarded - Quality gap:
avg_chosen_scorevs.avg_rejected_score,avg_chosen_ccvs.avg_rejected_cc,avg_chosen_lint_errorsvs.avg_rejected_lint_errors
The Case C percentage is the single most important metric of the project — if it's >15%, the composite reward is actively separating quality among working code, which is the core contribution beyond binary pass/fail.
- Sandbox:
DockerSandbox— communicates via gRPC with the Rust execution daemon (see below). Ephemeral containers withnetwork_disabled=True,mem_limit=128m,timeout=10s. Multi-language: Python, JavaScript, TypeScript, Java, C++, C#, Go, Rust (matching Phase 1's language scope). Image:docker/Dockerfile.executor(ubuntu:24.04, non-root user, all eight language toolchains installed). - Candidate generation:
CandidateGenerator— loads the SFT checkpoint (checkpoints/sft/merged_model) with QLoRA (4-bit NF4), generatesN_CANDIDATES=2completions per prompt. Code is extracted from markdown fences if present (parse_code). Supports batched generation (generate_candidates_batch): multiple prompts are left-padded and processed in a single GPU forward pass, giving ~4× throughput vs. single-prompt inference. - Orchestrator:
PreferenceOrchestrator— wires generation → execution → scoring → labelling. Reuses Phase 1'slinter_check()andget_cyclomatic_complexity()for the static-analysis components of the composite reward. The batch path (create_preference_pairs_batch) runs sandbox evaluations concurrently viaThreadPoolExecutor(up to 8 workers), overlapping I/O-bound gRPC calls with CPU-bound linting. - Checkpoint / resume: progress is saved to
.checkpoint_{reward_mode}.jsonafter every batch. If the process is interrupted, relaunching the same command resumes from the last completed batch — output files are opened in append mode, already-processed prompts are skipped. The checkpoint is deleted automatically on successful completion. - Batch size: controlled by
BATCH_SIZEinpreference_generation_config.py(default: 8). Tune down if GPU runs out of memory, up if VRAM allows. - Report notebook:
src/notebooks/03_preference_generation_report.ipynb— case distribution (bar + pie), composite score histograms (chosen vs. rejected), cyclomatic complexity boxplots, lint error boxplots, per-language breakdown, qualitative samples.
The LLM implicitly learns a reward function by maximizing the log-probability of Chosen responses and minimizing it for Rejected responses. Instead of using RLHF, no separate reward model is needed. No unstable PPO loop. It has much faster and more stable training. DPO transforms alignment into a binary classification problem, leveraging the LLM's own implicit reward structure.
The DPO loss:
Where
- Trainer:
trl.DPOTrainer+ QLoRA (4-bit NF4) on the merged SFT model (checkpoints/sft/merged_model— produced bymerge_sft.pyafter Phase 2), training on Phase 3's clean preference pairs (data/preferences/dpo_dataset.jsonl). - Hyperparameters — same golden recipe as SFT. Identical LoRA configuration (
r=16,alpha=32,dropout=0.1) and training config (lr=2e-4,paged_adamw_8bit, effective batch size 16,bf16, gradient checkpointing,seed=42). The only DPO-specific parameter isbeta=0.1— the standard value from the DPO paper. Rationale: the compute budget is better spent on Phase 5's evaluation ablation (composite vs. execution-only reward) than on DPO hyperparameter sweeps. - VRAM budget: same as SFT — QLoRA (4-bit base ~4.5GB) + gradient checkpointing +
batch_size=2/gradient_accumulation=8keeps this inside 24GB. - Checkpoint:
checkpoints/dpo/final_model(adapter + tokenizer). - Report notebook:
src/notebooks/04_dpo_training_report.ipynb— DPO loss curve analysis, three-way comparison (Base vs. SFT vs. DPO on HumanEval pass@1), W&B observations.
Comparison of four models: Base, SFT, DPO (composite reward), and DPO (ablation — execution-only reward). Three metric dimensions:
| Dimension | Metric | What it measures |
|---|---|---|
| Functional correctness | HumanEval pass@1 | Does the code work? |
| Code simplicity | Mean cyclomatic complexity | Is the code maintainable? |
| Style adherence | Mean lint errors | Is the code idiomatic? |
The star graph of the project: DPO-execution-only vs. DPO-composite-reward, showing in numbers that the composite reward avoids the "spaghetti code" failure mode predicted in Phase 0. This is what transforms the project from "I did SFT+DPO" to "I identified a known failure mode in the literature and demonstrated empirically that my solution works" — the type of contribution an applied research team looks for.
The ablation checkpoint (checkpoints/dpo_ablation/final_model) is trained by re-running Phases 3–4 with the composite reward reduced to execution-only. Both the preference generator and the DPO trainer accept a --reward_mode CLI flag that handles this automatically — no manual config editing required:
# Generate execution-only preference pairs (W_COMPLEXITY=0, W_LINT=0)
uv run python -m src.preference_generation.main --reward_mode execution_only
# Train the ablation DPO model on those pairs
uv run python -m src.training.dpo_trainer --reward_mode execution_onlyOr run the full pipeline (both composite and ablation) in one shot:
bash scripts/03_04_run_dpo_pipeline.shThis produces a DPO model trained on binary pass/fail preferences only (no quality ranking for Case C). Comparing it against the full composite model isolates the contribution of the
5–10 side-by-side samples where both models solve the problem, but the composite model produces measurably simpler code (lower cyclomatic complexity, fewer lint errors). Generated by src/evaluation/extract_qualitative.py and rendered inline in the notebook. These let a reviewer judge "by eye" whether the quantitative improvement translates to visibly better code.
- Evaluation harness:
scripts/05_run_eval_harness.sh— runsbigcode-evaluation-harnesson all four checkpoints (Base, SFT, DPO composite, DPO ablation), saves generations and pass@1 metrics todata/evaluation/. Supports multi-language evaluation viabash scripts/05_run_eval_harness.sh <lang>(defaults topython; other languages use MultiPL-E). - Static analysis:
src/evaluation/static_analysis.py— computes cyclomatic complexity and lint errors on every generated sample using Phase 1'slinter_check(), writesdata/evaluation/static_analysis_results_{lang}.jsonl. - Qualitative extraction:
src/evaluation/extract_qualitative.py— finds problems where the ablation model produces more complex code than the composite model, writes side-by-side markdown todata/evaluation/qualitative_samples_{lang}.md. - Report notebook:
src/notebooks/05_evaluation_report.ipynb— the most important notebook of the project. Contains: pass@1 bar chart, static-analysis aggregate table, the ablation boxplots (the star graph), four-way comparison, inline qualitative samples, summary table, and key findings template.
A Gradio app where a recruiter can paste a coding prompt and see three outputs (Base / SFT / DPO) side by side with their static-analysis metrics in 30 seconds — the return on "impression per effort" is very high compared to reading a README.
Why Gradio, not FastAPI + frontend? Gradio already runs its own Uvicorn server internally, provides gr.Code() with syntax highlighting, and generates a polished UI with zero HTML/CSS. Adding a separate FastAPI backend + a frontend framework would double the complexity without adding anything a recruiter-facing demo needs. FastAPI and Uvicorn remain in pyproject.toml because they're Gradio transitive dependencies and available for Phase 7 if the project evolves into a production service.
- Three columns, live metrics. Each column shows the generated code (
gr.Codewith Python highlighting) and two numbers: cyclomatic complexity and lint errors — computed on the fly using Phase 1'slinter_check(). This makes the composite-reward thesis visible in real time: the DPO column should consistently show lower complexity and fewer lint errors than the Base column. - Preset examples. Five prompts (palindrome check, LCS, binary search, flatten nested list, thread-safe LRU cache) so the demo works with a single click — no typing needed.
- Same QLoRA loading pattern. All three models load in 4-bit NF4 with the same
BitsAndBytesConfigused during training. SFT and DPO are loaded asPeftModeladapters on separate base instances. Three instances fit in ~12 GB VRAM total.
uv run python demo/app.py
# Opens at http://localhost:7860None of these block Phases 1–6. Listed here so the scope decisions behind them are explicit rather than assumed. These address key risks and limitations of the current pipeline:
- Lightweight RL comparison (GRPO vs DPO). A
GRPOTrainerrun (via TRL) using the same composite reward built for DPO, compared against the DPO result. For a complete research portfolio, comparing direct differentiation methods (DPO/ORPO) against online reinforcement learning (PPO/GRPO) is the standard. This covers the "reinforcement learning" requirement from the original JetBrains internship posting. Implementation scaffolding is already in place (src/training/grpo_config.py,src/training/grpo_trainer.py). - Multi-Language Evaluation (MultiPL-E). Currently,
bigcode-evaluation-harnessis configured for Python (HumanEval). To prove the generalizability of our composite reward, evaluation must be extended to Rust and Go using MultiPL-E. The training dataset, sandbox, and evaluation scripts already support multi-language via--language/{lang}patterns, but the final benchmarking step is currently Python-centric.
Not listed here because it's already addressed elsewhere: Unsloth (TRL's SFT/DPO speed-up) is an optional dependency group for Phases 2/4, not a Phase 7 item — see Compute budget and pyproject.toml.
Why QLoRA? Full fine-tuning of a 7B model requires ~56GB VRAM. QLoRA reduces this to ~12GB while maintaining 95%+ of full fine-tuning quality, making the project reproducible on a single consumer GPU.
Why not RLHF? Code provides deterministic ground truth (compile/test), making RLHF's human labelling unnecessary and expensive. Execution-based DPO achieves the same goal at a fraction of the cost.
codealign/
├── README.md
├── LICENSE
├── pyproject.toml
├── uv.lock
├── .env.example
├── .gitignore
├── checkpoints/ # trained adapters (gitignored)
│ ├── sft/
│ │ ├── final_model/ # LoRA adapter + tokenizer
│ │ └── merged_model/ # base + SFT merged (used by DPO)
│ ├── dpo/ # Phase 4 DPO adapter (composite reward)
│ └── dpo_ablation/ # Phase 5 DPO ablation (execution-only reward)
├── data/
│ ├── raw/ # untouched source data
│ ├── curated/ # syntax + complexity + lint validated SFT data (Phase 1)
│ │ ├── pristine_dataset.jsonl # accepted samples (SFT training data)
│ │ ├── rejected_dataset.jsonl # filtered-out samples
│ │ └── report_dataset.jsonl # all samples with full metadata (notebook)
│ ├── preferences/ # chosen/rejected pairs for DPO (Phase 3)
│ │ ├── dpo_dataset.jsonl # clean pairs for DPOTrainer
│ │ └── dpo_report.jsonl # all results with full metadata (notebook)
│ └── evaluation/ # Phase 5 results
│ ├── *_generations_{lang}.json # raw code from each model
│ ├── *_metrics_{lang}.json # pass@1 from harness
│ ├── static_analysis_results_{lang}.jsonl # CC + lint per sample
│ └── qualitative_samples_{lang}.md # side-by-side ablation examples
├── src/
│ ├── __init__.py
│ ├── data_curation/ # Phase 1
│ │ ├── curation_config.py # language/license whitelists, per-language lint thresholds
│ │ ├── dataset.py # loads CommitPackFT per-language via HF
│ │ ├── prompts.py # builds new_file/edit prompts + ChatML conversion
│ │ ├── validators.py # orchestrates checks → check_code()
│ │ ├── linters.py # ruff / cpplint / eslint / PMD / clippy / golangci-lint / dotnet
│ │ ├── code_smells.py # internal (within-sample) duplication
│ │ ├── minhash.py # cross-sample near-duplicate index
│ │ └── main.py # loads CommitPackFT, runs the pipeline
│ ├── preference_generation/ # Phase 3
│ │ ├── preference_generation_config.py # model, sandbox, reward weights, W&B
│ │ ├── candidate_generator.py # dual-temperature generation from SFT
│ │ ├── docker_sandbox.py # gRPC client → Rust executor daemon
│ │ ├── preference_orchestrator.py # A/B/C labelling + composite reward
│ │ ├── executor_pb2.py # generated protobuf (do not edit)
│ │ ├── executor_pb2_grpc.py # generated gRPC stubs (do not edit)
│ │ └── main.py # pipeline entry point, W&B tracking
│ ├── training/ # Phase 2, 4 & 7
│ │ ├── sft_config.py # SFT hyperparameters, model paths, seed, W&B
│ │ ├── sft_trainer.py # SFT training script
│ │ ├── merge_sft.py # merges LoRA adapter into base for DPO
│ │ ├── dpo_config.py # DPO hyperparameters (β, model paths, seed, W&B)
│ │ ├── dpo_trainer.py # DPO training script (--reward_mode for ablation)
│ │ ├── grpo_config.py # GRPO hyperparameters (Phase 7)
│ │ └── grpo_trainer.py # GRPO training with composite reward func (Phase 7)
│ ├── evaluation/ # Phase 5
│ │ ├── evaluation_config.py # paths, model checkpoints, thresholds
│ │ ├── static_analysis.py # CC + lint on all generated code
│ │ └── extract_qualitative.py # side-by-side ablation examples
│ └── notebooks/ # per-phase Jupyter reports
│ ├── 01_curation_report.ipynb
│ ├── 02_sft_experiments_log.ipynb
│ ├── 03_preference_generation_report.ipynb
│ ├── 04_dpo_training_report.ipynb
│ ├── 05_evaluation_report.ipynb # ⭐ the most important notebook
│ └── 06_grpo_rl_report.ipynb # Phase 7 (GRPO vs DPO & Multi-language)
├── demo/ # Phase 6 — interactive Gradio app
│ ├── __init__.py
│ ├── app.py # three-column side-by-side comparison UI
│ └── demo_config.py # model paths, generation params, server port
├── scripts/ # thin CLI wrappers per phase
│ ├── 01_curate_data.sh
│ ├── 02_run_sft.sh
│ ├── 03_04_run_dpo_pipeline.sh
│ └── 05_run_eval_harness.sh
├── tests/ # unit tests
├── docker/
│ └── Dockerfile.executor # multi-language sandbox (ubuntu:24.04, non-root)
└── rust-daemon/ # gRPC execution daemon (tonic + tokio)
├── Cargo.toml
├── build.rs # tonic-build for executor.proto
├── proto/
│ └── executor.proto # gRPC service definition
└── src/main.rs
git clone <repo-url>
cd codealign
uv sync
cp .env.example .env # fill in WANDB_API_KEY and HF_TOKEN
# --- PRE-REQUISITE: Build the Sandbox Environment ---
# We build the Docker image first because it contains all the native toolchains
# (PMD, ESLint, Go, Rust, .NET, etc.) needed for both Phase 1 and Phase 3.
docker build -f docker/Dockerfile.executor -t codealign-executor:latest .
# Phase 1 — curate the dataset (runs inside Docker for native linter toolchains)
docker run --rm \
-u "$(id -u):$(id -g)" \
-e HOME=/tmp \
-e DOTNET_NOLOGO=1 \
-e DOTNET_CLI_TELEMETRY_OPTOUT=1 \
-v "$(pwd):/app" \
-w /app \
-e UV_CACHE_DIR=/tmp \
codealign-executor:latest \
/opt/venv/bin/uv run python -m src.data_curation.main
# Phase 2 — SFT training + adapter merge
bash scripts/02_run_sft.shPhase 3 requires the Rust gRPC daemon running in a separate terminal (see below).
# Phase 3 — generate DPO preference pairs (composite reward)
# Requires the Rust daemon running in another terminal.
# Safe to interrupt and resume — checkpoints are saved after every batch.
uv run python -m src.preference_generation.main --reward_mode composite
# Phase 3 (ablation) — generate execution-only preference pairs
uv run python -m src.preference_generation.main --reward_mode execution_only
# Phase 4 — DPO training (composite reward)
uv run python -m src.training.dpo_trainer --reward_mode composite
# Phase 4 (ablation) — DPO training (execution-only baseline)
uv run python -m src.training.dpo_trainer --reward_mode execution_only
# Or run Phases 3 & 4 together in one shot (both modes sequentially):
# bash scripts/03_04_run_dpo_pipeline.sh
# Phase 5 — evaluation (all 4 checkpoints)
bash scripts/05_run_eval_harness.sh
# Phase 6 — interactive demo
uv run python demo/app.pyPhase 3 (Preference Generation) executes candidate code inside Docker containers via gRPC calls to a Rust daemon built with tonic and tokio. The daemon must be running before launching Phase 3.
# Protocol Buffers compiler — required by tonic-build to compile executor.proto
sudo apt update && sudo apt install -y protobuf-compiler
protoc --version # verifycd rust-daemon
cargo build --release
cargo run --release
# ✓ gRPC Server listening on [::1]:3000Keep this terminal open — the daemon must stay running throughout Phase 3.
# Composite reward (the main experiment)
uv run python -m src.preference_generation.main --reward_mode composite
# Execution-only reward (ablation baseline)
uv run python -m src.preference_generation.main --reward_mode execution_onlyBoth commands are checkpoint-safe: if interrupted (Ctrl+C, crash, reboot), re-running the same command resumes from the last completed batch. Progress is saved to data/preferences/.checkpoint_{mode}.json.
Once preference pairs are generated, you can stop the Rust daemon (Ctrl+C in Terminal 1). DPO training runs entirely on the GPU and does not call the sandbox:
# Train composite DPO model
uv run python -m src.training.dpo_trainer --reward_mode composite
# Train ablation DPO model
uv run python -m src.training.dpo_trainer --reward_mode execution_onlyBuilt as a technical demonstration for the JetBrains Post-Training and Alignment for Coding LLMs Internship program. Inspired by:
- DeepSeek-V4 Technical Report — On-Policy Distillation & GRM
- TRL Library — SFT/DPO/GRPO trainers
- BigCode Evaluation Harness — HumanEval/MBPP benchmarks
Code in this repository is released under the MIT License (see LICENSE). This is independent of the training-data license, which is documented separately above.