Download repos/MotionCache/MotionCache4MAGI-1/sample_video.py from Cccccz/comparison: direct link, hf CLI and curl.
- Browser
- Download file 23.8 kB
-
https://huggingface.co/Cccccz/comparison/resolve/main/repos/MotionCache/MotionCache4MAGI-1/sample_video.py
- Command line
-
hf download hf://Cccccz/comparison/repos/MotionCache/MotionCache4MAGI-1/sample_video.py
-
curl -L -o sample_video.py https://huggingface.co/Cccccz/comparison/resolve/main/repos/MotionCache/MotionCache4MAGI-1/sample_video.py
23.8 kB
| import os | |
| import sys | |
| import argparse | |
| import json | |
| import subprocess | |
| import torch | |
| import multiprocessing as mp | |
| import csv | |
| import re | |
| from pathlib import Path | |
| import logging | |
| from datetime import datetime | |
| import yaml | |
| def load_config(config_path): | |
| _, ext = os.path.splitext(config_path) | |
| with open(config_path, "r") as f: | |
| if ext == ".json": | |
| return json.load(f) | |
| if ext in [".yaml", ".yml"]: | |
| return yaml.safe_load(f) | |
| raise ValueError(f"Unsupported config file extension: {ext}") | |
| def setup_gpu_logger(gpu_id: int, base_log_dir: str, run_timestamp: str): | |
| """Set up independent log file for each GPU""" | |
| # If base_log_dir contains /videos/ subpath, extract its parent directory | |
| if "videos/" in base_log_dir or "/videos/" in base_log_dir: | |
| # e.g.: path/output/videos/overall_consistency -> path/output | |
| if "/videos/" in base_log_dir: | |
| base_log_dir = base_log_dir.split("/videos/")[0] | |
| else: | |
| base_log_dir = base_log_dir.split("videos/")[0] | |
| # Create subfolder by run time | |
| log_dir = os.path.join(base_log_dir, "log", run_timestamp) | |
| os.makedirs(log_dir, exist_ok=True) | |
| # Simplified log file name | |
| log_file = os.path.join(log_dir, f"gpu_{gpu_id}.log") | |
| # Create logger | |
| logger = logging.getLogger(f"GPU_{gpu_id}") | |
| logger.setLevel(logging.INFO) | |
| # Clear existing handlers | |
| logger.handlers.clear() | |
| # File handler | |
| file_handler = logging.FileHandler(log_file) | |
| file_handler.setLevel(logging.INFO) | |
| # Console handler (optional, if you still want to see output in terminal) | |
| # console_handler = logging.StreamHandler() | |
| # console_handler.setLevel(logging.INFO) | |
| # Format | |
| formatter = logging.Formatter( | |
| f'[GPU {gpu_id}] %(asctime)s - %(levelname)s - %(message)s', | |
| datefmt='%Y-%m-%d %H:%M:%S' | |
| ) | |
| file_handler.setFormatter(formatter) | |
| # console_handler.setFormatter(formatter) | |
| logger.addHandler(file_handler) | |
| # logger.addHandler(console_handler) | |
| # Also redirect print to log file | |
| sys.stdout = LoggerWriter(logger, logging.INFO) | |
| sys.stderr = LoggerWriter(logger, logging.ERROR) | |
| return logger, log_file | |
| class LoggerWriter: | |
| """Redirect print output to logger""" | |
| def __init__(self, logger, level): | |
| self.logger = logger | |
| self.level = level | |
| def write(self, message): | |
| if message.strip(): | |
| self.logger.log(self.level, message.rstrip()) | |
| def flush(self): | |
| pass | |
| def worker_process(gpu_id: int, rank: int, args: argparse.Namespace, all_samples: list, run_timestamp: str): | |
| """Independent worker running on each GPU""" | |
| # ========== For pdb debugging in terminal ========== | |
| sys.stdin = open(0) | |
| # ================================================== | |
| os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu_id) | |
| os.environ["WORLD_SIZE"] = "1" | |
| os.environ["RANK"] = "0" | |
| os.environ["LOCAL_RANK"] = "0" | |
| os.environ["MASTER_ADDR"] = "localhost" | |
| os.environ["MASTER_PORT"] = str(29510 + gpu_id) | |
| # ========== Set up independent log file for each GPU ========== | |
| logger, log_file = setup_gpu_logger(gpu_id, args.save_path, run_timestamp) | |
| logger.info(f"GPU {gpu_id} logger initialized. Log file: {log_file}") | |
| if getattr(args, "print_peak_memory", False) and torch.cuda.is_available(): | |
| torch.cuda.reset_peak_memory_stats() | |
| device = torch.cuda.current_device() | |
| print(f"[GPU {gpu_id}] GPU Memory before pipeline: {torch.cuda.memory_allocated(device) / 1024**3:.2f} GB allocated") | |
| from inference.pipeline.video_generate import SampleTransport | |
| if args.reuse_strategy == "original": | |
| pass | |
| elif args.reuse_strategy == "all": | |
| from inference.pipeline.teacache_all import teacache_forward_velocity, teacache_integrate_velocity | |
| # TeaCache | |
| SampleTransport.rel_l1_thresh = args.rel_l1_thresh | |
| SampleTransport.accumulated_rel_l1_distance = 0 | |
| SampleTransport.previous_modulated_input = None | |
| SampleTransport.previous_residual = None | |
| SampleTransport.cnt = 0 | |
| SampleTransport.forward_velocity = teacache_forward_velocity | |
| SampleTransport.integrate_velocity = teacache_integrate_velocity | |
| SampleTransport.reuse_times = 0 | |
| SampleTransport.no_reuse_first_n_steps = args.no_reuse_first_n_steps | |
| SampleTransport.previous_output = None | |
| SampleTransport.whole_calc_when_cross = args.whole_calc_when_cross | |
| SampleTransport.log = args.log | |
| elif args.reuse_strategy == "chunkwise": | |
| from inference.pipeline.flowcache import teacache_forward_velocity, teacache_integrate_velocity | |
| if args.compress_kv_cache: | |
| print("KV cache compression is enabled.") | |
| # KV cache compression | |
| SampleTransport.compress_kv_cache = True | |
| assert args.total_cache_chunk_nums is not None | |
| compression_config = { | |
| "method_config": { | |
| "compress_strategy": args.compress_strategy, | |
| "mix_lambda": args.mix_lambda, | |
| "query_granularity": args.query_granularity, | |
| "score_weighting_method": args.score_weighting_method, | |
| "power": args.power, | |
| }, | |
| } | |
| from inference.rkv import replace_magi | |
| from inference.rkv.utils import ChunkKVRangeTracker | |
| replace_magi(compression_config) | |
| else: | |
| SampleTransport.compress_kv_cache = False | |
| SampleTransport.rel_l1_thresh = args.rel_l1_thresh | |
| SampleTransport.chunk_accumulated_rel_l1 = 0 | |
| SampleTransport.previous_modulated_input = None | |
| SampleTransport.previous_residual = None | |
| SampleTransport.cnt = 0 | |
| SampleTransport.forward_velocity = teacache_forward_velocity | |
| SampleTransport.integrate_velocity = teacache_integrate_velocity | |
| SampleTransport.reuse_times = 0 | |
| SampleTransport.no_reuse_first_n_steps = args.no_reuse_first_n_steps | |
| SampleTransport.previous_output = None | |
| SampleTransport.discard_nearly_clean_chunk = args.discard_nearly_clean_chunk | |
| SampleTransport.whole_calc_when_cross = args.whole_calc_when_cross | |
| SampleTransport.no_reuse_mode = args.no_reuse_mode | |
| # Token-wise reuse parameters | |
| SampleTransport.token_wise_reuse = args.token_wise_reuse | |
| SampleTransport.token_rel_l1_thresh = args.token_rel_l1_thresh if args.token_rel_l1_thresh is not None else args.rel_l1_thresh | |
| SampleTransport.tokenwise_l1_mode = args.tokenwise_l1_mode | |
| SampleTransport.compute_l1_weights_once = getattr(args, "compute_l1_weights_once", False) | |
| # Three-phase control parameters | |
| SampleTransport.warmup_steps = args.warmup_steps | |
| SampleTransport.chunk_wise_only_steps = args.chunk_wise_only_steps | |
| # Token reuse ratio control | |
| SampleTransport.max_token_reuse_ratio = args.max_token_reuse_ratio | |
| SampleTransport.initial_token_reuse_ratio = args.initial_token_reuse_ratio | |
| SampleTransport.final_token_reuse_ratio = args.final_token_reuse_ratio | |
| # Temporal weight parameters | |
| SampleTransport.temporal_weight_floor = args.temporal_weight_floor | |
| SampleTransport.temporal_weight_power = args.temporal_weight_power | |
| SampleTransport.enable_temporal_voting = args.enable_temporal_voting | |
| # Debug and visualization parameters | |
| SampleTransport.print_token_stats = args.print_token_stats | |
| SampleTransport.visualize_reuse_mask = args.visualize_reuse_mask | |
| SampleTransport.debug = getattr(args, "debug", False) | |
| # --- Per-chunk state --- | |
| SampleTransport.chunk_accumulated_rel_l1 = None # List[float]: Accumulated rel L1 for each chunk | |
| SampleTransport.prev_chunk_features = None # List[Tensor]: Features of each chunk from previous step | |
| SampleTransport.chunk_reuse_flags = None # Whether each chunk is reused in current step | |
| # KV cache compression | |
| SampleTransport.total_cache_chunk_nums = args.total_cache_chunk_nums | |
| # Check mutual exclusivity of token_wise_reuse and compress_kv_cache | |
| if SampleTransport.token_wise_reuse and SampleTransport.compress_kv_cache: | |
| raise ValueError( | |
| "token_wise_reuse and compress_kv_cache cannot be enabled simultaneously. " | |
| "Token-level reuse requires full chunk query states for KV cache reassembly, " | |
| "which is incompatible with KV cache compression. " | |
| "Please set only one of these options to True." | |
| ) | |
| # log | |
| SampleTransport.log = args.log | |
| else: | |
| raise ValueError(f"Unknown reuse strategy: {args.reuse_strategy}") | |
| try: | |
| magi_root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel']).decode().strip() | |
| os.environ["MAGI_ROOT"] = magi_root | |
| os.environ["PYTHONPATH"] = f"{magi_root}:{os.environ.get('PYTHONPATH', '')}" | |
| except Exception as e: | |
| print(f"[GPU {gpu_id}] Failed to set MAGI_ROOT: {e}") | |
| return | |
| filtered_samples = [] | |
| for sample in all_samples: | |
| if args.benchmark == 'vbench': | |
| prompt = sample | |
| safe_prompt = prompt | |
| seed = 0 | |
| cur_save_path = os.path.join(args.save_path, f"{safe_prompt}-{seed}.mp4") | |
| else: # physicsiq | |
| cur_save_path = sample['output_path'] | |
| cur_save_path = os.path.abspath(cur_save_path) | |
| if not os.path.exists(cur_save_path): | |
| filtered_samples.append(sample) | |
| else: | |
| print(f"[β SKIP] Already exists: {cur_save_path}") | |
| if not filtered_samples: | |
| print(f"[GPU {gpu_id}] No samples need to be generated.") | |
| return | |
| print(f"Totally Processing {len(filtered_samples)} samples.") | |
| # === Split samples === | |
| world_size = args.num_gpus | |
| samples_per_gpu = (len(filtered_samples) + world_size - 1) // world_size | |
| start_idx = rank * samples_per_gpu | |
| end_idx = min(start_idx + samples_per_gpu, len(filtered_samples)) | |
| my_samples = filtered_samples[start_idx:end_idx] | |
| if not my_samples: | |
| print(f"[GPU {gpu_id}] No samples assigned.") | |
| return | |
| print(f"[GPU {gpu_id}] Processing {len(my_samples)} samples: {start_idx} ~ {end_idx-1}") | |
| from inference.pipeline.entry import MagiPipeline | |
| from inference.common import set_random_seed | |
| print(f"[GPU {gpu_id}] Loading model...") | |
| pipeline = MagiPipeline(args.config_file) | |
| print(f"[GPU {gpu_id}] Model loaded successfully.") | |
| for i, sample in enumerate(my_samples): | |
| if args.benchmark == 'vbench': | |
| prompt = sample | |
| seed = 0 | |
| safe_prompt = prompt | |
| output_path = os.path.join(args.save_path, f"{safe_prompt}-{seed}.mp4") | |
| output_path = os.path.abspath(output_path) | |
| if os.path.exists(output_path): | |
| print(f"[β GPU {gpu_id}] Already exists: {output_path}") | |
| continue | |
| # Reset seed before each prompt to ensure reproducibility | |
| set_random_seed(pipeline.config.runtime_config.seed) | |
| print(f"[π GPU {gpu_id}] Generating T2V: '{prompt}' -> {output_path}") | |
| pipeline.run_text_to_video(prompt=prompt, output_path=output_path) | |
| print(f"[β GPU {gpu_id}] Saved: {output_path}") | |
| else: # physicsiq | |
| prompt = sample['description'] | |
| prefix_video_path = sample['prefix_video_path'] | |
| output_path = sample['output_path'] | |
| if not os.path.exists(prefix_video_path): | |
| print(f"[β οΈ GPU {gpu_id}] Conditioning video not found: {prefix_video_path}") | |
| continue | |
| if os.path.exists(output_path): | |
| print(f"[β GPU {gpu_id}] Already exists: {output_path}") | |
| continue | |
| print(f"[π GPU {gpu_id}] Generating V2V: '{prompt}'") | |
| print(f" β Input: {prefix_video_path}") | |
| print(f" β Output: {output_path}") | |
| # Reset seed before each prompt to ensure reproducibility | |
| set_random_seed(pipeline.config.runtime_config.seed) | |
| pipeline.run_video_to_video( | |
| prompt=prompt, | |
| prefix_video_path=prefix_video_path, | |
| output_path=output_path | |
| ) | |
| print(f"[β GPU {gpu_id}] Saved: {output_path}") | |
| if getattr(args, "print_peak_memory", False) and torch.cuda.is_available(): | |
| device = torch.cuda.current_device() | |
| print(f"[GPU {gpu_id}] Peak GPU memory: {torch.cuda.max_memory_allocated(device) / 1024**3:.2f} GB") | |
| print(f"[GPU {gpu_id}] Done.") | |
| def load_physicsiq_samples(args): | |
| """Load sample list for PhysicsIQ dataset""" | |
| DATA_ROOT = args.physicsiq_data_dir | |
| DESCRIPTIONS_CSV = os.path.join(DATA_ROOT, "descriptions", "descriptions.csv") | |
| OUTPUT_DIR = args.save_path | |
| FPS = 24 # MAGI uses 24 FPS | |
| if not os.path.exists(DESCRIPTIONS_CSV): | |
| raise FileNotFoundError(f"descriptions.csv not found at {DESCRIPTIONS_CSV}") | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| samples = [] | |
| with open(DESCRIPTIONS_CSV, mode='r') as f: | |
| reader = csv.DictReader(f) | |
| for row in reader: | |
| scenario = row['scenario'].strip() | |
| match = re.match(r'^(\d+)_', scenario) | |
| if not match: | |
| print(f"Cannot extract ID from scenario: {scenario}") | |
| continue | |
| vid_id = match.group(1).zfill(4) | |
| description = row['description'] | |
| generated_video_name = row['generated_video_name'] | |
| # Construct conditioning video path | |
| conditioning_dir = os.path.join(DATA_ROOT, "physics-IQ-benchmark", "split-videos", "conditioning", f"{FPS}FPS") | |
| match_suffix = re.search(r'_(.*)', scenario) | |
| prefix_video_name = match_suffix.group(1) if match_suffix else "" | |
| filename = f"{vid_id}_conditioning-videos_{FPS}FPS_{prefix_video_name}" | |
| prefix_video_path = os.path.join(conditioning_dir, filename) | |
| output_path = os.path.join(OUTPUT_DIR, generated_video_name) | |
| os.makedirs(os.path.dirname(output_path), exist_ok=True) | |
| samples.append({ | |
| 'vid_id': vid_id, | |
| 'scenario': scenario, | |
| 'description': description, | |
| 'generated_video_name': generated_video_name, | |
| 'prefix_video_path': prefix_video_path, | |
| 'output_path': output_path | |
| }) | |
| total_samples_num = len(samples) | |
| unique_samples_num = total_samples_num // 2 | |
| print(f"[β ] Loaded {unique_samples_num} samples.") | |
| # physicsiq samples twice, we only take the first half | |
| filtered_samples = samples[:unique_samples_num] | |
| # Apply start/end slice | |
| if args.start is not None or args.end is not None: | |
| start = args.start if args.start is not None else 0 | |
| end = args.end if args.end is not None else len(filtered_samples) | |
| # Ensure index is within valid range | |
| start = max(0, min(start, len(filtered_samples))) | |
| end = max(start, min(end, len(filtered_samples))) | |
| filtered_samples = filtered_samples[start:end] | |
| print(f"[βοΈ] Sliced to samples {start} to {end-1}, total {len(filtered_samples)} samples.") | |
| return filtered_samples | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--benchmark", type=str, required=True, choices=['vbench', 'physicsiq'], help="Benchmark name") | |
| parser.add_argument("--vbench_prompt_dir", type=str, help="Directory containing prompt files") | |
| parser.add_argument("--dimension", type=str, help="Benchmark dimension") | |
| parser.add_argument('--physicsiq_data_dir', type=str, default='physics-iq-benchmark', help='Root path of the dataset') | |
| parser.add_argument("--save_path", type=str, default="./benchmark_videos", help="Output directory") | |
| parser.add_argument("--config_file", type=str, default="example/4.5B/4.5B_distill_config.json", help="Model config file") | |
| parser.add_argument("--additional_config", type=str, help="Additional MotionCache config file") | |
| parser.add_argument("--gpus", type=str, default="0", help="Comma-separated GPU IDs to use") | |
| parser.add_argument("--reuse_strategy", type=str, choices=['all', 'chunkwise', 'original'], required=True, help="Reuse strategy for video generation") | |
| parser.add_argument('--rel_l1_thresh', type=float, default=0, help='Relative L1 distance threshold for TeaCache.') | |
| parser.add_argument('--no_reuse_first_n_steps', type=int, default=0, help='Number of steps to not reuse output.') | |
| # chunkwise reuse | |
| parser.add_argument('--discard_nearly_clean_chunk', action='store_true', help='Whether to discard nearly clean chunks.') | |
| parser.add_argument('--whole_calc_when_cross', action='store_true', help='Whether to perform whole calculation when crossing stage which number of chunks increases.') | |
| parser.add_argument( | |
| '--no_reuse_mode', type=str, choices=['first', 'mid', 'none'], | |
| help='Detemine which part of steps to not reuse output. first: first n steps; mid: mid n steps; none: not set .' | |
| ) | |
| # KV cache compression | |
| parser.add_argument('--compress_kv_cache', action='store_true', help='Whether to compress kv cache.') | |
| parser.add_argument('--total_cache_chunk_nums', type=int, default=None, help='Total length of kv cache.') | |
| parser.add_argument('--compress_strategy', type=str, default='token', choices=['token', 'frame', 'chunk'], help='KV cache compression strategy.') | |
| parser.add_argument('--mix_lambda', type=float, default=0.07, help='Mix lambda for token compression.') | |
| parser.add_argument('--query_granularity', type=str, default='chunk', choices=['chunk', 'frame', 'token'], help='Query granularity for token compression.') | |
| parser.add_argument('--score_weighting_method', type=str, default='no_weight', choices=['no_weight','hard_code', 'exponential', 'polynomial', 'gaussian', "upper_convex_polynomial"], help='Score weighting method for KV compression.') | |
| parser.add_argument('--power', type=float, default=3, help='Power parameter for polynomial and upper_convex_polynomial weighting methods.') | |
| # log | |
| parser.add_argument('--log', action='store_true', help='Whether to log the TeaCache information.') | |
| parser.add_argument('--print_peak_memory', action='store_true', help='Print peak GPU memory usage.') | |
| # Token-wise reuse parameters | |
| parser.add_argument('--token_wise_reuse', action='store_true', help='Enable token-wise reuse strategy.') | |
| parser.add_argument('--token_rel_l1_thresh', type=float, default=None, help='Token-level relative L1 distance threshold (defaults to rel_l1_thresh if not set).') | |
| parser.add_argument('--tokenwise_l1_mode', type=str, default='chunk', choices=['chunk', 'token'], help='Mode for token-level rel_l1 calculation: "chunk" uses chunk-level rel_l1 for all tokens, "token" computes per-token rel_l1 independently.') | |
| # Three-phase control parameters | |
| parser.add_argument('--warmup_steps', type=int, default=5, help='Number of warmup steps where no caching is used.') | |
| parser.add_argument('--chunk_wise_only_steps', type=int, default=0, help='Number of steps for chunk-wise only phase (before token-wise phase).') | |
| # Token reuse ratio control | |
| parser.add_argument('--max_token_reuse_ratio', type=float, default=1.0, help='Fixed maximum ratio of tokens that can be reused (0~1). Set to 1.0 to allow all tokens to be reused.') | |
| parser.add_argument('--initial_token_reuse_ratio', type=float, default=None, help='Initial token reuse ratio at the start of hierarchical phase (for dynamic ratio mode).') | |
| parser.add_argument('--final_token_reuse_ratio', type=float, default=None, help='Final token reuse ratio at the end of hierarchical phase (for dynamic ratio mode).') | |
| # Temporal weight parameters | |
| parser.add_argument('--temporal_weight_floor', type=float, default=0.0, help='Floor for temporal weight normalization, maps weights to [floor, 1] range.') | |
| parser.add_argument('--temporal_weight_power', type=float, default=None, help='Power for nonlinear temporal weight normalization (default: None=linear). Values < 1 make more tokens closer to 1 (convex curve), > 1 make more tokens closer to floor.') | |
| parser.add_argument('--enable_temporal_voting', action='store_true', help='Enable temporal voting: force tokens at the same spatial position across frames to have the same reuse decision via majority voting.') | |
| # Debug and visualization | |
| parser.add_argument('--print_token_stats', action='store_true', help='Print token-wise reuse statistics.') | |
| parser.add_argument('--visualize_reuse_mask', action='store_true', help='Visualize token reuse mask on the output video.') | |
| # sampling range control | |
| parser.add_argument('--start', type=int, default=None, help='Start index of samples to process (inclusive)') | |
| parser.add_argument('--end', type=int, default=None, help='End index of samples to process (exclusive)') | |
| args = parser.parse_args() | |
| if args.additional_config: | |
| additional_config = load_config(args.additional_config) | |
| print(f"Loading additional config: {additional_config}") | |
| for key, value in additional_config.items(): | |
| setattr(args, key, value) | |
| print(f"[β ] Parsed arguments: {args}") | |
| if args.benchmark == 'vbench': | |
| assert os.path.exists(args.vbench_prompt_dir), f"Prompt directory not found: {args.vbench_prompt_dir}" | |
| prompt_file = os.path.join(args.vbench_prompt_dir, f"{args.dimension}.txt") | |
| if not os.path.exists(prompt_file): | |
| raise FileNotFoundError(f"Prompt file not found: {prompt_file}") | |
| with open(prompt_file, 'r') as f: | |
| all_samples = [line.strip() for line in f if line.strip()] | |
| # Apply start/end slice | |
| if args.start is not None or args.end is not None: | |
| start = args.start if args.start is not None else 0 | |
| end = args.end if args.end is not None else len(all_samples) | |
| # Ensure index is within valid range | |
| start = max(0, min(start, len(all_samples))) | |
| end = max(start, min(end, len(all_samples))) | |
| all_samples = all_samples[start:end] | |
| print(f"[βοΈ] Sliced to prompts {start} to {end-1}, total {len(all_samples)} prompts.") | |
| elif args.benchmark == 'physicsiq': | |
| assert os.path.exists(args.physicsiq_data_dir), f"Data root directory not found: {args.physicsiq_data_dir}" | |
| all_samples = load_physicsiq_samples(args) | |
| else: | |
| raise ValueError(f"Invalid benchmark: {args.benchmark}") | |
| gpu_ids = list(map(int, args.gpus.split(','))) | |
| args.num_gpus = len(gpu_ids) | |
| os.makedirs(args.save_path, exist_ok=True) | |
| print(f"Total prompts: {len(all_samples)}") | |
| print(f"GPUs: {gpu_ids}") | |
| print(f"Output: {args.save_path}") | |
| print(f"Config: {args.config_file}") | |
| # Notify user of log file location | |
| run_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| log_run_dir = os.path.join(args.save_path, "log", run_timestamp) | |
| print(f"GPU logs will be saved to: {log_run_dir}/") | |
| print(f" Each GPU will have its own log file: gpu_<ID>.log") | |
| print("") | |
| processes = [] | |
| for rank, gpu_id in enumerate(gpu_ids): | |
| print(f"[Main] Starting worker for GPU {gpu_id} (rank {rank})...") | |
| p = mp.Process(target=worker_process, args=(gpu_id, rank, args, all_samples, run_timestamp)) | |
| p.start() | |
| processes.append(p) | |
| for p in processes: | |
| p.join() | |
| if __name__ == "__main__": | |
| main() | |