T2J-Bench is a benchmark for semantic-preserving codebase conversion. It tests whether coding agents can convert LLaMA-Factory-style PyTorch training repositories into JAX/Flax repositories while preserving the training behavior that downstream users rely on.
The benchmark accompanies the paper "Converted, Not Equivalent": Benchmarking Codebase Conversion via Observational Equivalence. Instead of only checking whether a converted repository runs, T2J-Bench evaluates each conversion against a fixed equivalence contract derived from the original PyTorch codebase.
- Task: convert
torch_orig/PyTorch training repos into candidatejax_repo/implementations. - Scale: 45 datapoints = 15 model families x 3 training methods
(
SFT,DPO,PPO). - Model families:
bloom,falcon,gemma3,internvl,llama3,llava_next,mistral,paligemma,phi4,qwen2,qwen2_omni,qwen2_vl,qwen3,qwen3_vl, andstarcoder2. - Evaluator: ordered
reference,spec,numeric, andbehavioralstages for repo-to-repo observational equivalence. - Main launcher:
bash ./run_interactive_attempt.sh. - Attempt outputs:
dataset/.attempt_docker/.
The benchmark focuses on full training-pipeline conversion, not isolated function translation. Each task gives an agent a PyTorch source repository and a minimum-constraint prompt. The agent must produce a JAX repository that can be compared against the source through the fixed external evaluator.
| Path | Purpose |
|---|---|
dataset/{sft,dpo,ppo}/<model>/torch_orig/ |
PyTorch source repos used as conversion references. |
dataset/instructions/ |
Frozen minimum-constraint prompts for SFT, DPO, and PPO tasks. |
dataset/eval/ |
Staged PyTorch-vs-JAX equivalence evaluator. |
coding-agent/ |
ADK-based coding-agent scaffold, tools, Docker image files, runner scripts, and tests. |
run_interactive_attempt.sh |
Interactive launcher for benchmark attempts and optional evaluation. |
call_agent.py |
In-container CLI entry point for ADK-based agents. |
requirements.txt |
Host lf environment requirements, including CUDA PyTorch/JAX wheels. |
Generated artifacts are intentionally kept out of the source tree:
dataset/.attempt_docker/The benchmark was developed and tested on the system below. Other configurations may work, but this is the environment used for the reported results.
| Component | Specification |
|---|---|
| OS | Debian GNU/Linux 11 (bullseye) |
| Kernel / Arch | 5.10.0 (cloud) / x86_64 |
| CPU | 2x Intel Xeon @ 2.20GHz (96 vCPUs) |
| Memory | ~1.3 TiB |
| GPU | 8x NVIDIA A100-SXM4-80GB (80 GB each) |
| NVIDIA driver | 550.90.07 (supports CUDA 12.4) |
| Docker | 20.10.17 |
Python (lf env) |
3.11.15 |
| PyTorch | 2.12.0+cu126 |
| JAX | 0.10.1 (CUDA 12) |
Each attempt requests 2 GPUs by default.
The attempt workflow is designed for a Linux or WSL shell with GPU-enabled Docker. Before running attempts, make sure you have:
- Docker daemon access through either
dockerorsudo -n docker. - NVIDIA Container Toolkit if using GPUs.
- A local conda environment named
lf. conda-packavailable in that environment.- Editable source trees used by the Docker image build:
adk-python/srcfrom this repo's vendored custom ADK fork.$HOME/LlamaFactory/srcfrom a separately cloned LlamaFactory checkout.
- Provider credentials for whichever agent you run.
- Hugging Face access for any gated model weights you evaluate.
Run the following from the repository root.
LlamaFactory's current main requires Python >=3.11, so the reference setup
uses Python 3.11.
conda create -n lf python=3.11 -y
conda activate lf
git clone https://github.com/hiyouga/LlamaFactory.git "$HOME/LlamaFactory"
pip install -e "$HOME/LlamaFactory"
pip install -e ./adk-python
# Install after the editable packages so CUDA 12 torch overrides any default
# torch wheel pulled by LlamaFactory.
pip install -r requirements.txtThe evaluator and helper code in run_interactive_attempt.sh run on the host,
not inside the attempt container. The launcher automatically prefers an lf
environment found under .conda, miniconda3, or /opt/conda. To point it at
a specific interpreter:
CODING_AGENT_PYTHON_BIN="$HOME/.conda/envs/lf/bin/python" bash ./run_interactive_attempt.shCreate the default agent environment file if you plan to use client_agent or
iterative_agent:
cp coding-agent/coding_agent/.env.example coding-agent/coding_agent/.envThen fill in provider settings such as GOOGLE_CLOUD_PROJECT and
CODING_AGENT_MODEL. For codex, gemini, and claude, the interactive
launcher can also use your shell environment or a custom env file.
python -c "import torch; print(torch.cuda.is_available())"
python -c "import jax; print(jax.devices())"
pip checktorch.cuda.is_available() should print True, jax.devices() should show
CUDA devices, and pip check should report no broken requirements.
requirements.txt pins CUDA 12.6 PyTorch wheels (torch==2.12.0+cu126,
torchvision==0.27.0+cu126, torchaudio==2.11.0+cu126) and installs
jax[cuda12]. If your NVIDIA driver needs a different CUDA build, adjust the
PyTorch index URL and +cuXXX pins before installing.
bash coding-agent/docker/coding_agent_lf/build_image.sh coding-agent-lf:latestThe build script packages the host lf conda environment, copies editable ADK
and LlamaFactory sources into the Docker build context, and builds
coding-agent-lf:latest.
If LlamaFactory or conda-pack lives somewhere else:
CODING_AGENT_LLAMAFACTORY_SRC=/path/to/LlamaFactory/src \
CODING_AGENT_CONDA_PACK_BIN=/path/to/conda-pack \
bash coding-agent/docker/coding_agent_lf/build_image.sh coding-agent-lf:latestQuick import check:
sudo -n docker run --rm \
--mount type=bind,src="$(pwd)",dst=/opt/t2j_bench,readonly \
coding-agent-lf:latest \
bash -lc 'PYTHONPATH=/opt/t2j_bench/coding-agent python -c "import coding_agent, tools; print(\"ok\")"'GPU/JAX check:
sudo -n docker run --rm --gpus all coding-agent-lf:latest \
bash -lc 'python - <<'"'"'PY'"'"'
import jax
print(jax.__version__)
print(jax.devices())
PY'Use plain docker instead of sudo -n docker if your user already has Docker
daemon access.
bash ./run_interactive_attempt.shFor a small smoke run, choose:
- training method:
sft - model:
bloom - agent:
client_agent - Docker image:
coding-agent-lf:latest - pytest evaluation: disabled
The launcher prompts for task/model, agent, env profile, model override, Docker image, GPU mapping, verbose/yolo mode, optional pytest evaluation, and optional batch resume settings.
The evaluator downloads model weights from Hugging Face. Gated models, for
example google/gemma-3-4b-it used by sft/gemma3, require a token whose
account has accepted the model license.
Attempt containers bind-mount the host ~/.config/gcloud and
~/.cache/huggingface directories, so host authentication is visible inside the
container.
- Accept the model license on its Hugging Face page while logged in.
- Log in on the host:
hf auth login
# or, if the CLI is not on PATH:
conda run -n lf hf auth loginVerify access before running an attempt:
conda run -n lf python -c "from huggingface_hub import whoami; print(whoami()['name'])"Ungated models such as sft/bloom do not need a token.
Most references download weights on demand, but llava_next uses a cache-only
loader (snapshot_download(..., local_files_only=True)), so its model must be
present in the Hugging Face cache beforehand or its reference fails with
Tokenizer file not found: .../tokenizer.json. Pre-download it once (it is
ungated; the cache is shared with attempt containers):
hf download llava-hf/llava-v1.6-vicuna-7b-hf
# or, if the CLI is not on PATH:
conda run -n lf hf download llava-hf/llava-v1.6-vicuna-7b-hfThe recommended entry point is:
bash ./run_interactive_attempt.shSupported training-method choices are sft, dpo, ppo, and all.
Supported top-level agents are client_agent, iterative_agent, codex,
gemini, and claude.
For client_agent, codex, gemini, and claude, the launcher stages an
isolated candidate repository and archives the final candidate as:
dataset/.attempt_docker/<model>_<task>_<timestamp>/jax_repo/Batch runs write logs under:
dataset/.attempt_docker/batch_runs/For custom automation, call the lower-level runner directly:
python coding-agent/scripts/run_attempt_in_docker.py \
--image coding-agent-lf:latest \
--workspace dataset \
--mount sft/gemma3:sft/gemma3 \
--prompt-file dataset/instructions/sft.md \
--agent client_agent \
--attempt-root dataset/.attempt_docker/gemma3_sft_manual \
--gpus device=0,1 \
--verboseUseful options:
--mount RELPATHmounts selected workspace paths into the container.--promptor--prompt-filesupplies the conversion instruction.--agentselectsclient_agent,iterative_agent,codex,gemini, orclaude.--gpus nonedisables GPU passthrough.--dry-runprints the Docker command without executing it.
Prebuilt wrappers for selected tasks live in:
coding-agent/docker/coding_agent_lf/attempt_scripts/Example:
bash coding-agent/docker/coding_agent_lf/attempt_scripts/run_sft_gemma3.shThe interactive launcher can generate the evaluator config and run pytest for
you. To run evaluation manually, use dataset/eval.
Example for an SFT candidate:
cd dataset/eval
XLA_PYTHON_CLIENT_PREALLOCATE=false CUDA_VISIBLE_DEVICES=0,1 \
python -m lib.generate_config \
sft \
../sft/gemma3/torch_orig \
../.attempt_docker/gemma3_sft_manual/jax_repo \
--config-out configs/local_sft_gemma3.yaml \
--output-dir outputs/local_sft_gemma3 \
--no-run
XLA_PYTHON_CLIENT_PREALLOCATE=false CUDA_VISIBLE_DEVICES=0,1 \
python -m pytest -q tests/test_eval_pipeline.py \
--eval-config configs/local_sft_gemma3.yamlEach evaluation writes:
new_eval_report.json: full staged report with compared artifacts.new_eval_summary.json: compact pass/fail summary.
Interpret stage failures in order:
- Fix
referencebefore comparing repositories. - Fix
specbefore tuning numeric behavior. - Treat skipped or missing
behavioralartifacts as real failures, not success.
For evaluator design details, see dataset/eval/README.md.
An "agent" is the system under test. It receives a frozen instruction prompt, writes a converted JAX repo, and is evaluated externally.
Inside the container, every agent must honor the same contract:
- Prompt: the rendered instruction is available under
/attempt_meta/. Treat it as the only task guidance. - Workspace: the working directory is
/attempt_workspace. - Source repo: the PyTorch reference is mounted at
/attempt_workspace/<task>/<model>. - Candidate repo: the converted repository must be written to
/attempt_workspace/jax_repo. - Credentials: model-provider and Hugging Face credentials are mounted from the host environment/profile selected by the launcher.
- Done condition: success means a populated
jax_repo/; the agent does not need to return a special value.
There are two common integration paths.
Use this pattern for an agent that reads a prompt on stdin and edits files in
the current directory, like the existing codex, gemini, and claude
integrations.
- Install the CLI in
coding-agent/docker/coding_agent_lf/Dockerfile, then rebuild the image. - Add
_build_<name>_exec_command(...)incoding-agent/scripts/run_attempt_in_docker.py, modeled on the existing Codex/Gemini/Claude builders. - Add
<name>to the runner's--agentchoices and dispatch branch. - Add home-directory, settings, or auth mounts if your CLI needs them.
Use this pattern for a Python agent built on the vendored Google ADK fork, like
client_agent and iterative_agent.
- Create a package under
coding-agent/with anagent.pyexposing module-levelroot_agent. - Reuse tools from
coding-agent/tools/when possible:fs_tools,bash_exec,lsp_tool,todo_tools, and related helpers. - Register the agent in
call_agent.py. - Add the agent to
coding-agent/scripts/run_attempt_in_docker.py.
Pure-Python agent changes usually do not require a Docker rebuild because
coding-agent/ is mounted into the container on PYTHONPATH. Rebuild only when
you add dependencies to the lf environment.
For either integration path, update run_interactive_attempt.sh:
- Add
<name>to theChoose top-level agentmenu. - Add an env-profile/default-model branch if needed.
- Add
<name>toagent_uses_staged_jax_reposo the producedjax_repo/is archived for evaluation.
Then run it with:
bash ./run_interactive_attempt.shadk-python/ is a vendored fork of
google/adk-python, based on
google-adk 1.27.2. The coding agent depends on local modifications that are
not part of an official ADK release, so the fork travels with this repo.
Important local changes include:
- Tool-result formatting for model-readable
read_file,search,patch_file,lsp, evaluator, and related tool outputs. - Large tool-output offloading to workspace files with compact head/tail summaries.
- Anthropic/Claude request handling for output tokens, thinking configuration, and long streaming final responses.
- Thought-signature preservation gated by
ADK_PRESERVE_THOUGHT_SIGNATURES. - Gemini/Vertex client selection from environment/model name.
- Verbose tool-call, result, and usage rendering for attempt logs.
- Minor tweaks to planning, LiteLLM handling, and GCP IAM connector removal.
Check shell and Python syntax:
bash -n run_interactive_attempt.sh
bash -n coding-agent/docker/coding_agent_lf/build_image.sh
python -m py_compile call_agent.py coding-agent/scripts/run_attempt_in_docker.pyInspect the current Docker image:
sudo -n docker image inspect coding-agent-lf:latestValidate evaluator configs:
cd dataset/eval
python -m lib.validate_configs- Existing attempt outputs can be resumed or skipped when running
allor all models for one task. dataset/evalis reference-first:torch_origdefines the contract, and the JAX candidate must match that contract through repo-owned entry points.- The benchmark verifier is fixed and external to the agent. Agent self-reports are diagnostic logs, not benchmark pass/fail labels.
@article{song2026converted,
title={Converted, Not Equivalent: Benchmarking Codebase Conversion via Observational Equivalence},
author={Song, Linxin and Chen, Jiefeng and Huang, Yue and Mishra, Bhavana Dalvi, and Wang, Chi and Zhao, Jieyu and Yoon, Jinsung and Pfister, Tomas},
journal={arXiv preprint arXiv:2605.29054},
year={2026}
}Apache 2.0 License. See LICENSE for details.
This is not an officially supported Google product. This project is not eligible for the Google Open Source Software Vulnerability Rewards Program.
This project is intended for demonstration purposes only. It is not intended for use in a production environment.
