mirror of
https://github.com/hpcaitech/ColossalAI.git
synced 2026-07-12 19:06:17 +00:00
348 lines
18 KiB
Python
348 lines
18 KiB
Python
import os
|
|
import threading
|
|
import time
|
|
from typing import Any, Dict, Optional
|
|
|
|
import ray
|
|
import ray.util.collective as cc
|
|
import torch
|
|
import torch.distributed as dist
|
|
from coati.distributed.comm import SharedVariableActor, ray_broadcast_tensor_dict
|
|
from coati.distributed.profiling_utils import CustomProfiler
|
|
from coati.distributed.utils import bind_batch, post_recv, unbind_batch
|
|
from tqdm import tqdm
|
|
|
|
from colossalai.booster import Booster
|
|
from colossalai.booster.plugin import HybridParallelPlugin
|
|
from colossalai.initialize import launch
|
|
from colossalai.utils import get_current_device
|
|
|
|
|
|
class BaseConsumer:
|
|
def __init__(
|
|
self,
|
|
shared_sync_data_actor: SharedVariableActor,
|
|
shared_signal_actor: SharedVariableActor,
|
|
num_producers: int,
|
|
num_episodes: int,
|
|
rank: int,
|
|
world_size: int,
|
|
master_addr: str,
|
|
master_port: int,
|
|
train_dataset_size: int,
|
|
batch_size: int,
|
|
model_config: Dict[str, Any],
|
|
plugin_config: Dict[str, Any],
|
|
minibatch_size: int = 1,
|
|
save_interval: int = 100,
|
|
save_dir: str = "./model",
|
|
enable_profiling: bool = False,
|
|
):
|
|
self.num_producers = num_producers
|
|
self.num_episodes = num_episodes
|
|
self.rank = rank
|
|
self.world_size = world_size
|
|
self.master_addr = master_addr
|
|
self.master_port = master_port
|
|
self.train_dataset_size = train_dataset_size
|
|
self.received_prompts = 0
|
|
self.batch_size = batch_size
|
|
self.minibatch_size = minibatch_size
|
|
self.save_interval = save_interval
|
|
self.save_dir = save_dir
|
|
self.enable_profiling = enable_profiling
|
|
assert batch_size % minibatch_size == 0, "batch_size should be divisible by microbatch_size"
|
|
self.num_microbatches = batch_size // minibatch_size
|
|
self.data_uid = 0
|
|
self.sync_model_thread_started = False
|
|
|
|
self.model_config = model_config
|
|
self.plugin_config = plugin_config
|
|
|
|
self.device = get_current_device()
|
|
self.lr_scheduler = None
|
|
|
|
self.shared_sync_data_actor = shared_sync_data_actor
|
|
self.shared_signal_actor = shared_signal_actor
|
|
self.state_dict_cpu = {}
|
|
|
|
def setup(self) -> None:
|
|
launch(self.rank, self.world_size, self.master_addr, self.master_port, local_rank=0)
|
|
|
|
plugin_config = dict(tp_size=1, pp_size=1, precision="bf16", zero_stage=2)
|
|
if (
|
|
self.plugin_config.get("pp_size", 1) > 1
|
|
and "num_microbatches" not in self.plugin_config
|
|
and "microbatch_size" not in self.plugin_config
|
|
):
|
|
plugin_config["microbatch_size"] = max(1, self.minibatch_size // plugin_config.get("pp_size", 1))
|
|
plugin_config.update(self.plugin_config)
|
|
self.plugin = HybridParallelPlugin(**plugin_config)
|
|
self.booster = Booster(plugin=self.plugin)
|
|
self.dp_rank = dist.get_rank(self.plugin.dp_group)
|
|
self.tp_rank = dist.get_rank(self.plugin.tp_group)
|
|
self.pp_rank = dist.get_rank(self.plugin.pp_group)
|
|
|
|
self.dp_size = dist.get_world_size(self.plugin.dp_group)
|
|
self.tp_size = dist.get_world_size(self.plugin.tp_group)
|
|
self.pp_size = dist.get_world_size(self.plugin.pp_group)
|
|
|
|
self.buffer = []
|
|
self.recv_cnt = 0
|
|
self.profiler = CustomProfiler(f"C{self.rank}", disabled=not self.enable_profiling)
|
|
|
|
def get_ddp_config(self) -> Dict[str, Any]:
|
|
"""
|
|
Get the DDP configuration for the consumer.
|
|
This method is used to get the DDP configuration for the consumer.
|
|
"""
|
|
return {
|
|
"dp_size": self.dp_size,
|
|
"tp_size": self.tp_size,
|
|
"pp_size": self.pp_size,
|
|
"dp_rank": self.dp_rank,
|
|
"tp_rank": self.tp_rank,
|
|
"pp_rank": self.pp_rank,
|
|
"world_size": self.world_size,
|
|
"rank": self.rank,
|
|
}
|
|
|
|
def init_collective_group(
|
|
self,
|
|
world_size: int,
|
|
rank: int,
|
|
backend: str = "nccl",
|
|
group_name: str = "default",
|
|
gloo_timeout: int = 3000000,
|
|
):
|
|
cc.init_collective_group(
|
|
world_size=world_size, rank=rank, backend=backend, group_name=group_name, gloo_timeout=gloo_timeout
|
|
)
|
|
print(f"[C{self.rank}] Initialized {group_name} collective group", flush=True)
|
|
|
|
def state_dict(self) -> Dict[str, torch.Tensor]:
|
|
raise NotImplementedError
|
|
|
|
def step(self, **kwargs) -> Optional[float]:
|
|
raise NotImplementedError
|
|
|
|
def prepare_mini_batch(self, effective_group_to_raw_group_mapping: Dict[int, int]) -> Dict[str, torch.Tensor]:
|
|
"""
|
|
Prepare a mini-batch from the effective group to raw group mapping.
|
|
This method is used to create a mini-batch for training.
|
|
"""
|
|
batches = [
|
|
self.buffer[effective_group_to_raw_group_mapping[i]]
|
|
for i in range(self.dp_rank * self.minibatch_size, (self.dp_rank + 1) * self.minibatch_size)
|
|
]
|
|
# every dp_rank will receive a complete mini-batch, no need to sync within step() later
|
|
# each mini-batch use the first self.dp_size * minibatch_size effective samples
|
|
raw_mini_batches = self.buffer[
|
|
: effective_group_to_raw_group_mapping[self.dp_size * self.minibatch_size - 1] + 1
|
|
] # include the last effective sample
|
|
raw_mini_batches_metric_dict = {
|
|
"raw_train_mini_batch_reward": [t[1] for t in raw_mini_batches],
|
|
"raw_train_mini_batch_format_acc": [t[2] for t in raw_mini_batches],
|
|
"raw_train_mini_batch_ans_acc": [t[3] for t in raw_mini_batches],
|
|
"raw_train_mini_batch_response_len": [t[4] for t in raw_mini_batches],
|
|
}
|
|
batch = bind_batch([t[0] for t in batches])
|
|
batch = post_recv(batch)
|
|
return batch, raw_mini_batches_metric_dict
|
|
|
|
def calculate_effective_group_to_raw_group_mapping(self):
|
|
effective_group_to_raw_group_mapping = {}
|
|
for buffer_idx in range(len(self.buffer)):
|
|
if self.buffer[buffer_idx][0] is not None:
|
|
effective_group_to_raw_group_mapping[len(effective_group_to_raw_group_mapping)] = buffer_idx
|
|
return effective_group_to_raw_group_mapping
|
|
|
|
def loop(self) -> None:
|
|
print(f"Consumer{self.rank}, nmb: {self.num_microbatches}")
|
|
for episode in range(self.num_episodes):
|
|
with tqdm(
|
|
range(self.train_dataset_size),
|
|
desc=f"Episode {episode} with rollout step(s)",
|
|
disable=self.rank != 0,
|
|
) as pbar:
|
|
while self.received_prompts < self.train_dataset_size:
|
|
torch.cuda.reset_peak_memory_stats()
|
|
effective_group_to_raw_group_mapping = {}
|
|
self.profiler.enter(f"recv_data")
|
|
while len(effective_group_to_raw_group_mapping) < self.dp_size * self.minibatch_size:
|
|
# receive data from producers
|
|
raw_batch = ray.get(
|
|
self.shared_sync_data_actor.get_data.remote(self.data_uid)
|
|
) # get the first queued data
|
|
self.profiler.log(f"enter sleep")
|
|
while raw_batch is None:
|
|
print(
|
|
f"[T{dist.get_rank()}] No data received by consumer {self.rank}, skipping. Consider increasing the data actor buffer limit"
|
|
)
|
|
time.sleep(1)
|
|
raw_batch = ray.get(self.shared_sync_data_actor.get_data.remote(self.data_uid))
|
|
continue
|
|
self.profiler.log(f"exit sleep")
|
|
self.data_uid += 1
|
|
raw_batch = {k: v.to(self.device) for k, v in raw_batch.items()}
|
|
# calculate group reward et al. filtering. As only the filtered group will be used for training (which is incomplete),
|
|
# we need to calculate the metrics before filtering here for logging
|
|
# [batch_size, num_generations] -> [batch_size]
|
|
reward = raw_batch["reward"][:, :, 0]
|
|
format_acc = raw_batch["format_acc"][:, :, 0]
|
|
ans_acc = raw_batch["ans_acc"][:, :, 0]
|
|
response_len = (
|
|
raw_batch["response_idx"][:, :, 1] - raw_batch["response_idx"][:, :, 0] + 1
|
|
).type(torch.float32)
|
|
effective_group_mask = None
|
|
if self.filter_range is not None and self.grpo_config.get("dynamic_batching", True):
|
|
# filter the group based on the reward and accuracy
|
|
group_ans_acc_mean = ans_acc.mean(dim=1)
|
|
effective_group_mask = torch.logical_and(
|
|
group_ans_acc_mean > self.filter_range[0], group_ans_acc_mean < self.filter_range[1]
|
|
)
|
|
|
|
raw_batch = unbind_batch(raw_batch) # List[Dict[str, torch.Tensor]]
|
|
self.received_prompts += len(raw_batch)
|
|
pbar.update(len(raw_batch))
|
|
for group_idx, group_with_reward in enumerate(raw_batch):
|
|
self.buffer.append(
|
|
[
|
|
(
|
|
group_with_reward
|
|
if effective_group_mask is None or effective_group_mask[group_idx]
|
|
else None
|
|
),
|
|
reward[group_idx],
|
|
format_acc[group_idx],
|
|
ans_acc[group_idx],
|
|
response_len[group_idx],
|
|
]
|
|
)
|
|
if effective_group_mask is not None:
|
|
print(
|
|
f"[T{dist.get_rank()}] Filter recv data: {len(raw_batch)} -> {torch.sum(effective_group_mask).cpu().item()} effective groups"
|
|
)
|
|
# mapping the effective group to the raw group for indexing
|
|
effective_group_to_raw_group_mapping = self.calculate_effective_group_to_raw_group_mapping()
|
|
print(
|
|
f"[T{dist.get_rank()}] Collect Effective Prompt: {len(effective_group_to_raw_group_mapping)}/{self.dp_size * self.minibatch_size}"
|
|
)
|
|
self.profiler.exit(f"recv_data")
|
|
need_sync_model = False
|
|
while len(effective_group_to_raw_group_mapping) >= self.dp_size * self.minibatch_size:
|
|
# after we have enough effective groups, we can start training
|
|
# on each dp_rank, we use minibatch_size effective samples to form a batch
|
|
batch, raw_mini_batches_metric_dict = self.prepare_mini_batch(
|
|
effective_group_to_raw_group_mapping
|
|
)
|
|
self.profiler.enter("step")
|
|
loss = self.step(pbar, **batch, **raw_mini_batches_metric_dict)
|
|
self.profiler.exit("step")
|
|
self.buffer = self.buffer[
|
|
effective_group_to_raw_group_mapping[self.dp_size * self.minibatch_size - 1] + 1 :
|
|
]
|
|
# recalculate the effective group to raw group mapping
|
|
effective_group_to_raw_group_mapping_size_before = len(effective_group_to_raw_group_mapping)
|
|
effective_group_to_raw_group_mapping = self.calculate_effective_group_to_raw_group_mapping()
|
|
assert (
|
|
len(effective_group_to_raw_group_mapping)
|
|
== effective_group_to_raw_group_mapping_size_before - self.dp_size * self.minibatch_size
|
|
)
|
|
# cc.barrier(group_name="consumer_pg")
|
|
if loss is not None:
|
|
pbar.set_postfix({"loss": loss})
|
|
need_sync_model = True
|
|
ray.get(self.shared_signal_actor.set_signal.remote("global_step", self.global_step + 1))
|
|
if need_sync_model and (
|
|
(self.global_step + 1) % self.save_interval == 0
|
|
or self.received_prompts >= self.train_dataset_size
|
|
):
|
|
if self.rank == 0:
|
|
print(f"Start saving policy model at step {self.global_step + 1}.")
|
|
save_path = os.path.join(
|
|
self.save_dir, f"modeling-episode-{episode}-step-{self.global_step + 1}"
|
|
)
|
|
self.booster.save_model(self.policy_model, save_path, shard=True)
|
|
if self.rank == 0:
|
|
print(f"Saved model checkpoint at step {self.global_step + 1} in folder {save_path}")
|
|
|
|
if need_sync_model and (
|
|
episode != self.num_episodes - 1 or self.received_prompts != self.train_dataset_size
|
|
):
|
|
|
|
def sync_model_thread():
|
|
# sync model weights to all producers, if no model update or it is the last training step, skip syncing
|
|
if self.pp_size > 1:
|
|
print(
|
|
f"[T{dist.get_rank()}] Sync model PP stage {self.pp_rank} episode {episode} step {self.global_step}"
|
|
)
|
|
else:
|
|
print(f"[T{dist.get_rank()}] Sync model episode {episode} step {self.global_step}")
|
|
torch.cuda.empty_cache()
|
|
if self.pp_size > 1:
|
|
if self.tp_rank == 0 and self.dp_rank == 0:
|
|
self.profiler.enter("sync_model")
|
|
ray.get(
|
|
self.shared_signal_actor.set_signal.remote(
|
|
f"consumer_pp_{self.pp_rank}", "ready_sync_model"
|
|
)
|
|
)
|
|
print(
|
|
f"[T{dist.get_rank()}] Sync model PP stage {self.pp_rank} episode {episode} step {self.global_step}"
|
|
)
|
|
ray_broadcast_tensor_dict(
|
|
self.state_dict_cpu,
|
|
src=0,
|
|
device=torch.device("cpu"),
|
|
group_name=f"sync_model_consumer_pp_{self.pp_rank}",
|
|
backend="gloo",
|
|
)
|
|
self.profiler.exit("sync_model")
|
|
else:
|
|
if self.rank == 0:
|
|
self.profiler.enter("sync_model")
|
|
ray.get(self.shared_signal_actor.set_signal.remote("consumer", "ready_sync_model"))
|
|
print(f"[T{dist.get_rank()}] Sync model episode {episode} step {self.global_step}")
|
|
ray_broadcast_tensor_dict(
|
|
self.state_dict_cpu,
|
|
src=0,
|
|
device=torch.device("cpu"),
|
|
group_name="sync_model_consumer",
|
|
backend="gloo",
|
|
)
|
|
self.profiler.exit("sync_model")
|
|
|
|
if not self.sync_model_thread_started:
|
|
# only sync model when the thread is not started and no other thread is broadcasting
|
|
self.sync_model_thread_started = True
|
|
state_dict_ = self.state_dict()
|
|
if (self.pp_size > 1 and self.tp_rank == 0 and self.dp_rank == 0) or (
|
|
self.pp_size == 1 and self.rank == 0
|
|
):
|
|
if len(self.state_dict_cpu) == 0:
|
|
# use pinned memory to speed up the transfer
|
|
self.state_dict_cpu = {k: v.cpu().pin_memory() for k, v in state_dict_.items()}
|
|
torch.cuda.synchronize()
|
|
for k, v in state_dict_.items():
|
|
self.state_dict_cpu[k].copy_(v, non_blocking=True)
|
|
torch.cuda.synchronize()
|
|
cc.barrier(
|
|
group_name="consumer_pg"
|
|
) # to make sure all ranks have state dict offloaded to CPU before starting the thread
|
|
time_before_starting_thread = time.time()
|
|
threading.Thread(target=sync_model_thread).start()
|
|
# sync_model_thread()
|
|
self.profiler.log(
|
|
f"Sync model, took {time.time() - time_before_starting_thread:.2f} seconds"
|
|
)
|
|
self.sync_model_thread_started = False
|
|
# ray.get(self.shared_signal_actor.release_process_lock.remote("broadcasting_lock"))
|
|
self.profiler.log(f"Peak memory usage: {torch.cuda.max_memory_allocated() / 1024 ** 2:.2f} MB")
|
|
self.received_prompts = 0
|
|
ray.get(self.shared_signal_actor.set_signal.remote("consumer", "terminate"))
|
|
|
|
def __del__(self):
|
|
if hasattr(self, "profiler"):
|
|
self.profiler.close()
|