Files
wehub-resource-sync 85742ab165
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:44:17 +08:00

355 lines
12 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
"""RL training main loop that threads Tinker into Agent-lightning.
This module closely follows the Tinker Cookbook's
[`rl/train.py`](https://github.com/thinking-machines-lab/tinker-cookbook/blob/9b2af83cb62b9c4e8325a0efab71429e5aedf289/tinker_cookbook/rl/train.py)
but swaps the rollout collection strategy: instead of stepping environments,
we enqueue Agent-lightning tasks and reconstruct trajectories from spans. The
user-defined agent therefore owns the environment logic.
"""
from __future__ import annotations
import asyncio
import logging
import os
import time
from typing import Any, Literal
import chz
import tinker
from tinker_cookbook import checkpoint_utils
from tinker_cookbook.renderers import get_renderer
from tinker_cookbook.rl.train import (
do_train_step_and_get_sampling_client,
save_checkpoint_and_get_sampling_client,
)
from tinker_cookbook.tokenizer_utils import Tokenizer
from tinker_cookbook.utils import ml_log
from tinker_cookbook.utils.misc_utils import timed
from tinker_cookbook.utils.trace import scope, trace_init
from agentlightning import (
LightningStore,
LightningStoreClient,
LLMProxy,
LlmProxyTraceToTriplet,
TracerTraceToTriplet,
TraceToTripletBase,
)
from .env import AGLDataset, AGLDatasetBuilder
from .llm import TinkerLLM
from .rollout import AGLTestSetEvaluator, do_group_of_group_rollouts
logger = logging.getLogger(__name__)
@chz.chz
class Config:
"""Configuration for Tinker RL training with Agent-lightning.
Compared with `tinker_cookbook.rl.train.Config` this dataclass:
* Pins `dataset_builder` to `AGLDatasetBuilder` so minibatches emit
Agent-lightning tasks rather than real Tinker environments.
* Adds Agent-lightning specific knobs (`store_address`, `adapter_from_llm_proxy`,
`llm_proxy_retry_attempts`) that drive how rollouts are queued and traced.
"""
learning_rate: float
dataset_builder: AGLDatasetBuilder[Any] # also determines batch size
model_name: str
renderer_name: str
compute_post_kl: bool = False
lora_rank: int = 32
llm_proxy_port: int = 12306
kl_penalty_coef: float = 0.0
kl_discount_factor: float = 0.0
# Sampling parameters
max_tokens: int = 2048
train_temperature: float = 1.0
eval_temperature: float = 1.0
top_k: int = -1
top_p: float = 1.0
# Agent-lightning parameters (only used in when running standalone)
store_address: str = "http://localhost:4747"
# Using tracing data from LLM proxy instead of client-side tracer
# When this is true, adapter_agent_match is ignored
adapter_from_llm_proxy: bool = False
adapter_agent_match: str | None = None
llm_proxy_retry_attempts: int = 0
# Concurrency parameters (mainly for controlling max queue length)
concurrency: int = 16
# Loss function to use for training: "importance_sampling" or "ppo"
loss_fn: Literal["importance_sampling", "ppo"] = "importance_sampling"
# Number of optimizer steps per training iteration.
# Useful for very large batch sizes.
num_substeps: int = 1
wandb_project: str | None = None
wandb_name: str | None = None
log_path: str = chz.field(munger=lambda _, s: os.path.expanduser(s))
base_url: str | None = None
enable_trace: bool = False
remove_constant_reward_groups: bool = False
eval_every: int = 20
save_every: int = 20
load_checkpoint_path: str | None = None
@scope
async def do_sync_training(
*,
start_batch: int,
end_batch: int,
num_batches: int,
cfg: Config,
training_client: tinker.TrainingClient,
service_client: tinker.ServiceClient,
evaluators: list[AGLTestSetEvaluator[Any]],
dataset: AGLDataset[Any],
ml_logger: ml_log.Logger,
tokenizer: Tokenizer,
store: LightningStore,
adapter: TraceToTripletBase,
llm_proxy: LLMProxy,
):
"""Implements fully synchronous on-policy training.
See `tinker_cookbook.rl.train.do_sync_training` for the original flow. The
Agent-lightning adaptation diverges in a few places:
* A LiteLLM proxy is restarted every batch so refreshed player checkpoints
are immediately visible to rollout workers.
* Trajectories are gathered via `do_group_of_group_rollouts`, which in turn
dequeues tasks from the Agent-lightning store and rebuilds transitions from
trace triplets.
* Evaluation hooks call `AGLTestSetEvaluator` so validation samples reuse the
same CrewAI-based agent rather than invoking a raw token completer.
"""
# Initial sampling client
logger.info(f"Creating sampling client with training client {training_client} and start batch {start_batch}")
sampling_client, _ = await save_checkpoint_and_get_sampling_client(
training_client, start_batch, cfg.log_path, cfg.save_every
)
logger.info(f"Creating renderer with name {cfg.renderer_name}")
renderer = get_renderer(cfg.renderer_name, tokenizer)
tinker_llm = TinkerLLM(
model_name=cfg.model_name,
sampling_client=sampling_client,
renderer=renderer,
tokenizer=tokenizer,
max_tokens=cfg.max_tokens,
temperature=cfg.train_temperature,
top_k=cfg.top_k,
top_p=cfg.top_p,
).rewrite_litellm_custom_providers()
logger.info(f"Starting training from batch {start_batch} to {end_batch}")
for i_batch in range(start_batch, end_batch):
metrics = {
"progress/batch": i_batch,
"optim/lr": cfg.learning_rate,
"progress/done_frac": (i_batch + 1) / num_batches,
}
logger.info(f"[Batch {i_batch}] Starting training step. Learning rate: {cfg.learning_rate}")
t_start = time.time()
llm_proxy.update_model_list(tinker_llm.as_model_list())
await llm_proxy.restart()
logger.info(f"[Batch {i_batch}] LiteLLM model list: {llm_proxy.model_list}")
llm_resource = llm_proxy.as_resource()
resources_update = await store.add_resources({"main_llm": llm_resource})
# Run evaluations
if cfg.eval_every > 0 and i_batch % cfg.eval_every == 0:
logger.info(f"[Batch {i_batch}] Running evaluations")
tinker_llm.temperature = cfg.eval_temperature
with timed("run_evals", metrics):
for evaluator in evaluators:
eval_metrics = await evaluator(resources_update.resources_id, store, adapter, "val", i_batch)
metrics.update({f"test/{k}": v for k, v in eval_metrics.items()})
tinker_llm.temperature = cfg.train_temperature
# Get batch and sample trajectories
logger.info(f"[Batch {i_batch}] Getting batch data from dataset")
env_group_builders_P = dataset.get_batch(i_batch)
with timed("sample", metrics):
logger.info(f"[Batch {i_batch}] Sampling trajectories...")
trajectory_groups_P = await do_group_of_group_rollouts(
env_group_builders_P,
resources_update.resources_id,
i_batch,
store=store,
adapter=adapter,
mode="train",
do_remove_constant_reward_groups=cfg.remove_constant_reward_groups,
concurrency=cfg.concurrency,
)
logger.info(f"[Batch {i_batch}] Trajectories sampled: {len(trajectory_groups_P)}")
# Train step
logger.info(f"[Batch {i_batch}] Starting training step...")
sampling_client, train_step_metrics = await do_train_step_and_get_sampling_client(
cfg,
i_batch,
training_client,
service_client,
tokenizer,
env_group_builders_P,
trajectory_groups_P,
)
# Point Tinker LLM to a new model
tinker_llm.update_sampling_client(sampling_client)
# Log metrics
metrics.update(train_step_metrics)
metrics["time/total"] = time.time() - t_start
ml_logger.log_metrics(metrics, step=i_batch)
logger.info(f"[Batch {i_batch}] Sampling and training completed")
await llm_proxy.stop()
@scope
async def main_training_loop(
cfg: Config,
store: LightningStore,
adapter: TraceToTripletBase,
llm_proxy: LLMProxy,
):
"""Main training loop for MDP RL."""
ml_logger = ml_log.setup_logging(
log_dir=cfg.log_path,
wandb_project=cfg.wandb_project,
config=cfg,
wandb_name=cfg.wandb_name,
)
if cfg.enable_trace:
# Get and rename the current (main) task
current_task = asyncio.current_task()
if current_task is not None:
current_task.set_name("main")
trace_events_path = os.path.join(cfg.log_path, "trace_events.jsonl")
logger.info(f"Tracing is enabled. Trace events will be saved to {trace_events_path}")
logger.info(
f"Run `python tinker_cookbook/utils/trace.py {trace_events_path} trace.json` and visualize in chrome://tracing or https://ui.perfetto.dev/"
)
trace_init(output_file=trace_events_path)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("pylatexenc").setLevel(logging.WARNING)
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
logging.getLogger("LiteLLM Router").setLevel(logging.WARNING)
resume_info = checkpoint_utils.get_last_checkpoint(cfg.log_path)
if resume_info:
start_batch = resume_info["batch"]
else:
start_batch = 0
logger.info(f"Creating service client with base URL {cfg.base_url}")
service_client = tinker.ServiceClient(base_url=cfg.base_url)
logger.info(f"Creating training client with model name {cfg.model_name} and rank {cfg.lora_rank}")
training_client = await service_client.create_lora_training_client_async(cfg.model_name, rank=cfg.lora_rank)
load_state_path: str | None = resume_info["state_path"] if resume_info else cfg.load_checkpoint_path
if load_state_path:
future = await training_client.load_state_async(load_state_path)
_ = await future.result_async()
logger.info(f"Loaded state from {load_state_path}")
else:
logger.info("No checkpoint found, starting from scratch")
# Get tokenizer from training client
tokenizer = training_client.get_tokenizer()
logger.info(f"Tokenizer created: {tokenizer}")
# Create dataset from thunk
dataset, test_dataset = await cfg.dataset_builder()
evaluators = [AGLTestSetEvaluator(test_dataset)]
num_batches = len(dataset)
logger.info(f"Will train on {num_batches} batches and test on {len(test_dataset)} batches")
# Training loop
await do_sync_training(
start_batch=start_batch,
end_batch=num_batches,
num_batches=num_batches,
cfg=cfg,
training_client=training_client,
service_client=service_client,
evaluators=evaluators,
dataset=dataset,
ml_logger=ml_logger,
tokenizer=tokenizer,
store=store,
adapter=adapter,
llm_proxy=llm_proxy,
)
# Save final checkpoint
if start_batch < num_batches:
logger.info(f"Saving final checkpoint to {cfg.log_path}/final.pt")
_ = await checkpoint_utils.save_checkpoint_async(
training_client=training_client,
name="final",
log_path=cfg.log_path,
kind="both",
loop_state={"batch": num_batches},
)
else:
logger.info("Training was already complete; nothing to do")
# Cleanup
ml_logger.close()
logger.info("Training completed successfully")
@scope
async def main(config: Config) -> None:
"""Entry point for the training script.
Sets up the store, adapter, and LLM proxy, then launches the main training loop.
Args:
config: Training configuration.
"""
store = LightningStoreClient(config.store_address)
if config.adapter_from_llm_proxy:
# This is still under development
if config.adapter_agent_match is not None:
raise ValueError("adapter_agent_match is not supported when adapter_from_llm_proxy is True")
adapter = LlmProxyTraceToTriplet()
else:
# This is the tested path
adapter = TracerTraceToTriplet(agent_match=config.adapter_agent_match, _skip_empty_token_spans=True)
llm_proxy = LLMProxy(
port=config.llm_proxy_port,
model_list=[],
store=store,
num_retries=config.llm_proxy_retry_attempts,
# Must use thread mode here because otherwise the Tinker sampling client will hang.
launch_mode="thread",
)
await main_training_loop(config, store, adapter, llm_proxy)
if __name__ == "__main__":
chz.nested_entrypoint(main)