/*! * \file multi_gpu_loader.cc * \brief Implementation of a multi-GPU loader with loading-time sharding. */ #ifndef MLC_SINGLE_GPU_ONLY #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../metadata/model.h" #include "../support/progress_bar.h" namespace mlc { namespace llm { namespace multi_gpu { using tvm::Device; using tvm::runtime::vm::TensorCacheMetadata; using namespace tvm::runtime; using tvm::ffi::Array; using tvm::ffi::Function; using tvm::ffi::Object; using tvm::ffi::Optional; using tvm::ffi::Shape; using tvm::ffi::TypedFunction; using DurationType = std::chrono::microseconds; class RangeTimer { public: explicit RangeTimer(DurationType* result) : start(std::chrono::high_resolution_clock::now()), result(result) {} ~RangeTimer() { std::chrono::time_point end = std::chrono::high_resolution_clock::now(); // auto duration = end - start; (*result) += std::chrono::duration_cast(end - start); } private: std::chrono::time_point start; DurationType* result; }; class PreprocessorPool { public: explicit PreprocessorPool(const ModelMetadata& model_metadata, Module vm_module) { for (const ModelMetadata::Param& param : model_metadata.params) { for (const ModelMetadata::Param::Preproc& preproc : param.preprocs) { const std::string& func_name = preproc.func_name; if (Function f = vm_module.defined() ? vm_module->GetFunction(func_name, true).value_or(Function(nullptr)) : nullptr; f != nullptr) { preproc_funcs[func_name] = f; } else if (const auto f = Function::GetGlobal(func_name); f.has_value()) { preproc_funcs[func_name] = *f; } else { LOG(FATAL) << "ValueError: Undefined function: " << func_name; } } } } Tensor Apply(Tensor param, const ModelMetadata::Param& param_info) const { for (const ModelMetadata::Param::Preproc& preproc : param_info.preprocs) { const std::string& func_name = preproc.func_name; Tensor param_in = param; param = Tensor::Empty(preproc.out_shape, preproc.out_dtype, param->device); TVM_FFI_ICHECK(preproc_funcs.count(func_name)); DLTensor dl_param_in = *param_in.operator->(); DLTensor dl_param = *param.operator->(); preproc_funcs.at(func_name)(&dl_param_in, &dl_param); } return param; } private: std::unordered_map> preproc_funcs; }; struct ParamInfo { const TensorCacheMetadata::FileRecord* file; const TensorCacheMetadata::FileRecord::ParamRecord* param; }; Tensor RecvFromGlobalWorker0(Device device, const ModelMetadata::Param& param_info) { Shape shape = param_info.preprocs.empty() ? param_info.shape : param_info.preprocs[0].in_shape; Tensor result = Tensor::Empty(shape, param_info.dtype, device); RecvFromWorker0(result); return result; } Tensor BroadcastOrShardAndScatter(Tensor param, const ModelMetadata::Param& param_info, int num_shards, const PreprocessorPool& preprocs) { bool needs_sharding = !param_info.preprocs.empty(); if (!needs_sharding) { BroadcastFromWorker0(param, /*in_group=*/true, param); return param; } Device device = param->device; Shape shape = param_info.preprocs.back().out_shape; DLDataType dtype = param_info.preprocs.back().out_dtype; TVM_FFI_ICHECK(shape.size() >= 1 && shape[0] == num_shards) << "ValueError: The first dimension of the output shape must be equal to the " << "number of shards, but got: " << shape << " and num_shards = " << num_shards; param = preprocs.Apply(param, param_info); Tensor result = Tensor::Empty(Shape(shape.begin() + 1, shape.end()), dtype, device); ScatterFromWorker0(param, /*in_group=*/true, result); return result; } Tensor ReceiveBroadcastedOrSharded(Device device, const ModelMetadata::Param& param_info, int num_shards) { bool needs_sharding = !param_info.preprocs.empty(); Tensor result; if (needs_sharding) { Shape shape = param_info.preprocs.back().out_shape; DLDataType dtype = param_info.preprocs.back().out_dtype; result = Tensor::Empty(Shape(shape.begin() + 1, shape.end()), dtype, device); ScatterFromWorker0(std::nullopt, /*in_group=*/true, result); } else { result = Tensor::Empty(param_info.shape, param_info.dtype, device); BroadcastFromWorker0(result, /*in_group=*/true, result); } return result; } std::string FormatDuration(DurationType duration) { std::ostringstream os; auto float_seconds = std::chrono::duration_cast>(duration).count(); os << std::fixed << std::setprecision(3) << float_seconds << " s"; return os.str(); } Array> LoadMultiGPU(const std::string& model_path, Module vm_module, const std::string& model_config_str) { DiscoWorker* worker = DiscoWorker::ThreadLocal(); Device device = worker->default_device; int worker_id = worker->worker_id; int group_size = worker->num_workers / worker->num_groups; int num_shards = group_size; int group_id = worker_id / group_size; LOG(INFO) << "[Worker #" << worker_id << "] Loading model to device: " << device; // Step 0. Initialize metadata and paths TensorCacheMetadata tensor_cache_metadata = TensorCacheMetadata::Load(model_path); tvm::ffi::json::Value model_config = tvm::ffi::json::Parse(model_config_str); ModelMetadata model_metadata = ModelMetadata::FromModule(vm_module, model_config.cast()); TVM_FFI_ICHECK_EQ(model_metadata.tensor_parallel_shards, num_shards) << "ValueError: The model is compiled using `--tensor-parallel-shards=" << model_metadata.tensor_parallel_shards << "`, but mlc-chat-config.json is configured to use " << num_shards << " GPUs. " << "Please set \"tensor_parallel_shards\" in mlc-chat-config.json to " << model_metadata.tensor_parallel_shards; // Step 1. Extract auxiliary information PreprocessorPool preprocs(model_metadata, vm_module); std::unordered_map param_name2info; for (const ModelMetadata::Param& param : model_metadata.params) { param_name2info[param.name] = param; } // Step 2. Load, preprocess and shard all the parameters std::unordered_map sharded_params; if (worker_id == 0) { DurationType time_loading(0); DurationType time_preproc(0); ProgressBar progress_bar(model_metadata.params.size()); LOG(INFO) << "Loading parameters..."; for (const TensorCacheMetadata::FileRecord& record : tensor_cache_metadata.records) { Array loaded_params; { RangeTimer _(&time_loading); std::string raw_data_buffer; loaded_params = record.Load(device, model_path, &raw_data_buffer); DeviceAPI::Get(device)->StreamSync(device, nullptr); } // For each parameter in the shard file, preprocess and shard it for (size_t i = 0; i < record.records.size(); ++i, progress_bar.Progress()) { RangeTimer _(&time_preproc); const std::string& param_name = record.records[i].name; const ModelMetadata::Param& param_info = param_name2info.at(param_name); for (int group_id : param_info.pipeline_stages) { if (group_id == 0) { // Broadcast or shard-scatter this parameter to all workers in worker group 0. sharded_params[param_name] = BroadcastOrShardAndScatter(loaded_params[i], param_info, num_shards, preprocs); } else { // Send this parameter to the first worker of the worker group of "group_id", // and let that first worker to process this parameter. SendToWorker(loaded_params[i], /*receiver_id=*/group_id * group_size); } } DeviceAPI::Get(device)->StreamSync(device, nullptr); } } LOG(INFO) << "Loading done. Time used:" << std::fixed << std::setprecision(3) // << " Loading " << FormatDuration(time_loading) << " Preprocessing " << FormatDuration(time_preproc) << "."; } else { for (const TensorCacheMetadata::FileRecord& record : tensor_cache_metadata.records) { for (size_t i = 0; i < record.records.size(); ++i) { const std::string& param_name = record.records[i].name; const ModelMetadata::Param& param_info = param_name2info.at(param_name); if (std::find(param_info.pipeline_stages.begin(), param_info.pipeline_stages.end(), group_id) == param_info.pipeline_stages.end()) { // This worker group doesn't need to hold a copy of this parameter. continue; } if (worker_id % group_size == 0) { // The worker is the first worker of its worker group (while not the first worker group). // Receive the full parameter from the global worker 0. Tensor full_param = RecvFromGlobalWorker0(device, param_info); // Broadcast or shard-scatter this parameter to all workers in its worker group. sharded_params[param_name] = BroadcastOrShardAndScatter(full_param, param_info, num_shards, preprocs); } else { // The worker is not the first worker of its worker group. // Receive from the first worker in the its worker group. sharded_params[param_name] = ReceiveBroadcastedOrSharded(device, param_info, num_shards); } } } } // Step 3. Reorder the sharded parameters according to the order in model_metadata Array> shards; shards.reserve(model_metadata.params.size()); for (const ModelMetadata::Param& param : model_metadata.params) { const auto& it = sharded_params.find(param.name); shards.push_back(it == sharded_params.end() ? Optional() : it->second); } return shards; } Array> LoadMultiGPUPresharded(const std::string& model_path, Module vm_module, const std::string& model_config_str) { DiscoWorker* worker = DiscoWorker::ThreadLocal(); Device device = worker->default_device; int worker_id = worker->worker_id; int group_size = worker->num_workers / worker->num_groups; int num_shards = group_size; int group_id = worker_id / group_size; int local_worker_id = worker_id % group_size; LOG(INFO) << "[Worker #" << worker_id << "] Loading model to device: " << device; // Step 0. Initialize metadata and paths TensorCacheMetadata tensor_cache_metadata = TensorCacheMetadata::Load(model_path); tvm::ffi::json::Value model_config = tvm::ffi::json::Parse(model_config_str); ModelMetadata model_metadata = ModelMetadata::FromModule(vm_module, model_config.cast()); std::unordered_map param_info_map; for (const TensorCacheMetadata::FileRecord& file_record : tensor_cache_metadata.records) { for (const TensorCacheMetadata::FileRecord::ParamRecord& param_record : file_record.records) { const std::string& param_name = param_record.name; param_info_map[param_name] = ParamInfo{&file_record, ¶m_record}; } } Array> params; const TensorCacheMetadata::FileRecord* current_file_; std::string current_file_stream_; params.reserve(model_metadata.params.size()); DurationType time_loading(0); for (const ModelMetadata::Param& param : model_metadata.params) { RangeTimer _(&time_loading); if (std::find(param.pipeline_stages.begin(), param.pipeline_stages.end(), group_id) == param.pipeline_stages.end()) { // This worker group doesn't need to hold a copy of this parameter. params.push_back(Optional()); continue; } bool needs_sharding = !param.preprocs.empty(); std::string param_name = needs_sharding ? static_cast( std::stringstream() << param.name << "_shard-" << local_worker_id) .str() : std::string(param.name); auto it = param_info_map.find(param_name); TVM_FFI_ICHECK(it != param_info_map.end()) << "ValueError: Cannot find parameter: " << param_name; const ParamInfo& param_info = (*it).second; const TensorCacheMetadata::FileRecord::ParamRecord* param_record = param_info.param; const TensorCacheMetadata::FileRecord* file_record = param_info.file; if (file_record != current_file_) { current_file_ = file_record; file_record->Load(device, model_path, ¤t_file_stream_); } params.push_back(param_record->Load(device, ¤t_file_stream_)); } SyncWorker(); if (worker_id == 0) { LOG(INFO) << "Loading done. Time used: " << FormatDuration(time_loading) << "."; } return params; } TVM_FFI_STATIC_INIT_BLOCK() { namespace refl = tvm::ffi::reflection; refl::GlobalDef() .def("mlc.multi_gpu.LoadMultiGPU", LoadMultiGPU) .def("mlc.multi_gpu.LoadMultiGPUPresharded", LoadMultiGPUPresharded); } } // namespace multi_gpu } // namespace llm } // namespace mlc #endif // MLC_SINGLE_GPU_ONLY