From 421db17d5302cf01d6ebae001ea16153a63855e8 Mon Sep 17 00:00:00 2001 From: Starrick Liu <73152103+StarrickLiu@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:09:20 +0800 Subject: [PATCH] feat: Major optimization and robustness improvements (#31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This release introduces significant performance optimizations, memory efficiency improvements, and enhanced system robustness: 🚀 Performance Optimizations: - Add three new fused CUDA kernels (rope_index, rot_pos_emb, get_window_index) for accelerated multimodal preprocessing - Implement FSDP2 support for distributed training with improved memory efficiency - Add Torch.compile integration for additional performance gains - Optimize memory usage: reduce peak allocation from 48GB to 24GB on 8-GPU setup 🔧 System Robustness: - Fix missing token position inputs in prediction pipeline - Add type-robust negation operations in RoPE CUDA kernels (half/bfloat16 support) - Fix dataset root parameter initialization in LeRobot data loader - Enhanced error handling and input validation across fusion operators 📚 Documentation & Usability: - Add comprehensive memory usage benchmarks and hardware recommendations - Update citation format with proper arXiv reference - Improve training configuration documentation with quick start guide - Add detailed API documentation for new fusion operators 🛠️ Technical Details: - Version bump to 1.0.1 - New CUDA kernels: rope_index.cu, rot_pos.cu, window_index.cu - FSDP2 state dict loading with distribute_tensor support - Enhanced multimodal RoPE with 3D position encoding - Window attention optimization for Vision Transformers Breaking Changes: None - all changes are backward compatible --- .gitignore | 2 +- README.md | 37 +- assets/cot_example_frame.png | Bin 0 -> 136402 bytes csrc/README.md | 9 +- csrc/ops.cu | 6 + csrc/rope.cu | 68 ++- csrc/rope_index.cu | 569 ++++++++++++++++++ csrc/rope_index.h | 13 + csrc/rot_pos.cu | 332 ++++++++++ csrc/rot_pos.h | 6 + csrc/window_index.cu | 286 +++++++++ csrc/window_index.h | 9 + scripts/vqa_inference.py | 12 +- setup.py | 5 +- train_qact.py | 24 + wall_x/data/load_lerobot_dataset.py | 3 +- wall_x/fusions/backend.py | 134 +++++ wall_x/fusions/ops.py | 245 ++++++++ .../qwen2_5_based/modeling_qwen2_5_vl.py | 18 +- .../qwen2_5_based/modeling_qwen2_5_vl_act.py | 47 +- wall_x/trainer/qwen_vl_act_trainer.py | 48 +- workspace/README.md | 60 +- workspace/lerobot_example/config_qact.yml | 4 + workspace/lerobot_example/run.sh | 1 - 24 files changed, 1862 insertions(+), 76 deletions(-) create mode 100644 assets/cot_example_frame.png create mode 100644 csrc/rope_index.cu create mode 100644 csrc/rope_index.h create mode 100644 csrc/rot_pos.cu create mode 100644 csrc/rot_pos.h create mode 100644 csrc/window_index.cu create mode 100644 csrc/window_index.h diff --git a/.gitignore b/.gitignore index 142aa08..9c8f49b 100644 --- a/.gitignore +++ b/.gitignore @@ -196,7 +196,6 @@ bin/ # Media files *.jpg *.jpeg -*.png *.gif *.mp4 *.avi @@ -244,6 +243,7 @@ mlruns/ # Cache directories .cache/ cache/ +.ruff_cache/ # ============================================================================= # FONTS AND ASSETS (if not part of the project) diff --git a/README.md b/README.md index 6e6bd17..a482508 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ By creating a direct feedback loop between the model's decisions and the body's This repository provides the training and inference code that supports our WALL series open-source embodied foundation models. It includes end-to-end pipelines for data preparation (LeRobot), model configuration, flow-matching and FAST action branches, and evaluation utilities for real and simulated robots. ## News -- We introduce [**WALL-OSS**](https://x2robot.com/en/research/68bc2cde8497d7f238dde690), an end-to-end embodied foundation model that leverages large-scale multimodal pretraining to achieve (1) embodiment-aware vision–language understanding, (2) strong language–action association, and (3) robust manipulation capability. +- We introduce [**WALL-OSS: Igniting VLMs toward the Embodied Space**](https://x2robot.com/en/research/68bc2cde8497d7f238dde690), an end-to-end embodied foundation model that leverages large-scale multimodal pretraining to achieve (1) embodiment-aware vision–language understanding, (2) strong language–action association, and (3) robust manipulation capability. ## Models - WALL-OSS-FLOW: https://huggingface.co/x-square-robot/wall-oss-flow @@ -83,6 +83,8 @@ bash ./workspace/lerobot_example/run.sh ## Inference +### Basic Action Inference + For model inference, please refer to: ```bash @@ -95,28 +97,47 @@ This script demonstrates how to: - Run inference in validation mode with proper data types (bfloat16) - Validate model outputs and check for numerical stability +### Open-Loop Evaluation + To generate an open-loop comparison plot, please follow: ```bash python ./scripts/draw_openloop_plot.py ``` -To run VQA inference, please follow: +### VQA Inference and Chain-of-Thought Testing + +To run VQA inference and test the model's Chain-of-Thought (COT) reasoning capabilities, please follow: ```bash python ./scripts/vqa_inference.py ``` +This script can be used to test the model's COT reasoning abilities for embodied tasks. Below is an example of COT testing: + +**Input Image:** + +![COT Example Frame](assets/cot_example_frame.png) + +**Input Text:** +``` +To move the red block in the plate with same color, what should you do next? Think step by step. +``` + +**Model Output (COT Reasoning):** +``` +To move the red block in the plate with the same color, you should first locate the red block. It is currently positioned on the table, not in the plate. Then, you should carefully grasp the red block using your fingers. Next, you should use your hand to lift the red block from the table and place it into the plate that is also red in color. Ensure that the red block is securely placed in the plate without slipping or falling. +``` + ## 📚 Cite Us If you find WALL-OSS models useful, please cite: ```bibtex -@misc{walloss_paper_2025, - title = {WALL-OSS: Igniting VLMs toward the Embodied Space}, - author = {X Square Robot}, - year = {2025}, - howpublished = {\url{https://x2robot.cn-wlcb.ufileos.com/wall_oss.pdf}}, - note = {White paper} +@article{zhai2025igniting, + title = {Igniting VLMs Toward the Embodied Space}, + author = {Zhai, Andy and Liu, Brae and Fang, Bruno and Cai, Chalse and Ma, Ellie and Yin, Ethan and Wang, Hao and Zhou, Hugo and Wang, James and Shi, Lights and Liang, Lucy and Wang, Make and Wang, Qian and Gan, Roy and Yu, Ryan and Li, Shalfun and Liu, Starrick and Chen, Sylas and Chen, Vincent and Xu, Zach}, + journal = {arXiv preprint arXiv:2509.11766}, + year = {2025} } ``` diff --git a/assets/cot_example_frame.png b/assets/cot_example_frame.png new file mode 100644 index 0000000000000000000000000000000000000000..02b3450456c3c512019bd95a71f7f4983bc03240 GIT binary patch literal 136402 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

N{* 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ϗ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 @@ -12,4 +15,7 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { m.def("unpermute_bwd", &moe_recover_topK_bwd_op, "Token un-permutation backward kernel"); m.def("rope", &launch_multimodal_rope_forward, "Multimodal RoPE forward kernel"); m.def("rope_bwd", &launch_multimodal_rope_backward, "Multimodal RoPE backward kernel"); + m.def("rope_index", &get_rope_index, "Get RoPE index kernel"); + m.def("rot_pos_emb", &fused_rot_pos_emb_cuda, "Fused Rotary Position Embedding kernel"); + m.def("get_window_index", &get_window_index_cuda, "Get window index kernel"); } diff --git a/csrc/rope.cu b/csrc/rope.cu index 6369f1c..0420ea6 100644 --- a/csrc/rope.cu +++ b/csrc/rope.cu @@ -13,8 +13,6 @@ #include #include - - // Type traits for CUDA types template struct CudaTypeTraits @@ -81,7 +79,8 @@ __global__ void multimodal_rope_forward_kernel( // Process tokens in batches across warps - for (int seq_base = warp_id + seq_paral_idx * warps_per_block; seq_base < seq_len; seq_base += seq_paral_size * warps_per_block) { + for (int seq_base = warp_id + seq_paral_idx * warps_per_block; seq_base < seq_len; seq_base += seq_paral_size * warps_per_block) + { int seq_idx = seq_base; if (seq_idx >= seq_len) break; @@ -119,16 +118,16 @@ __global__ void multimodal_rope_forward_kernel( // Load cos/sin values (coalesced access) int cos_sin_idx = section_idx * batch_size * seq_len * head_dim + - batch_idx * seq_len * head_dim + - seq_idx * head_dim + cos_sin_d; + batch_idx * seq_len * head_dim + + seq_idx * head_dim + cos_sin_d; T cos_val = cos[cos_sin_idx]; T sin_val = sin[cos_sin_idx]; // Calculate tensor indices int tensor_idx = batch_idx * total_heads * seq_len * head_dim + - actual_head_idx * seq_len * head_dim + - seq_idx * head_dim + dim_idx; + actual_head_idx * seq_len * head_dim + + seq_idx * head_dim + dim_idx; // Get input value T input_val = is_q_head ? q[tensor_idx] : k[tensor_idx]; @@ -143,7 +142,22 @@ __global__ void multimodal_rope_forward_kernel( T rotate_val = is_q_head ? q[rotate_tensor_idx] : k[rotate_tensor_idx]; if (dim_idx < half_dim) { - rotate_val = -rotate_val; // First half: negate second half + if constexpr (std::is_same_v) + { + rotate_val = __hneg(rotate_val); + } + else if constexpr (std::is_same_v) + { +#if __CUDA_ARCH__ >= 800 // BFloat16 support requires Ampere or newer + rotate_val = __hneg(rotate_val); // __hneg works for bfloat16 in newer CUDA +#else + rotate_val = __float2bfloat16(-__bfloat162float(rotate_val)); +#endif + } + else + { + rotate_val = -rotate_val; + } } // Apply RoPE: output = input * cos + rotate_half(input) * sin @@ -234,13 +248,13 @@ __global__ void multimodal_rope_backward_kernel( // Global cos/sin index int cos_sin_idx = section_idx * batch_size * seq_len * head_dim + - batch_idx * seq_len * head_dim + - seq_idx * head_dim + cos_sin_d; + batch_idx * seq_len * head_dim + + seq_idx * head_dim + cos_sin_d; // Tensor index for current position int tensor_idx = batch_idx * total_heads * seq_len * head_dim + - actual_head_idx * seq_len * head_dim + - seq_idx * head_dim + dim_idx; + actual_head_idx * seq_len * head_dim + + seq_idx * head_dim + dim_idx; // Load values T cos_val = cos[cos_sin_idx]; @@ -274,8 +288,8 @@ __global__ void multimodal_rope_backward_kernel( } int paired_cos_sin_idx = paired_section_idx * batch_size * seq_len * head_dim + - batch_idx * seq_len * head_dim + - seq_idx * head_dim + paired_cos_sin_d; + batch_idx * seq_len * head_dim + + seq_idx * head_dim + paired_cos_sin_d; T paired_sin_val = sin[paired_cos_sin_idx]; // === Compute input gradients (the only thing we need!) === @@ -441,7 +455,6 @@ void launch_multimodal_rope_forward( torch::Tensor q_out, torch::Tensor k_out, std::vector mrope_section_doubled) { - cudaStream_t stream = at::cuda::getCurrentCUDAStream().stream(); int batch_size = q.size(0); @@ -463,10 +476,14 @@ void launch_multimodal_rope_forward( { data_type = 2; } - int *d_mrope_section_doubled; - cudaMalloc(&d_mrope_section_doubled, 3 * sizeof(int)); - cudaMemcpyAsync(d_mrope_section_doubled, mrope_section_doubled.data(), 3 * sizeof(int), - cudaMemcpyHostToDevice, stream); + + auto mrope_tensor = torch::from_blob( + mrope_section_doubled.data(), + {3}, + torch::TensorOptions().dtype(torch::kInt32) + ).to(q.device(), /*non_blocking=*/true); + + int *d_mrope_section_doubled = static_cast(mrope_tensor.data_ptr()); switch (data_type) { @@ -497,7 +514,6 @@ void launch_multimodal_rope_backward( torch::Tensor grad_q, torch::Tensor grad_k, std::vector mrope_section_doubled) { - cudaStream_t stream = at::cuda::getCurrentCUDAStream().stream(); int batch_size = q.size(0); @@ -519,10 +535,14 @@ void launch_multimodal_rope_backward( { data_type = 2; } - int *d_mrope_section_doubled; - cudaMalloc(&d_mrope_section_doubled, 3 * sizeof(int)); - cudaMemcpyAsync(d_mrope_section_doubled, mrope_section_doubled.data(), 3 * sizeof(int), - cudaMemcpyHostToDevice, stream); + + auto mrope_tensor = torch::from_blob( + mrope_section_doubled.data(), + {3}, + torch::TensorOptions().dtype(torch::kInt32) + ).to(q.device(), /*non_blocking=*/true); + + int *d_mrope_section_doubled = static_cast(mrope_tensor.data_ptr()); switch (data_type) { diff --git a/csrc/rope_index.cu b/csrc/rope_index.cu new file mode 100644 index 0000000..fd0402d --- /dev/null +++ b/csrc/rope_index.cu @@ -0,0 +1,569 @@ +#undef __CUDA_NO_HALF_OPERATORS__ +#undef __CUDA_NO_HALF_CONVERSIONS__ +#undef __CUDA_NO_BFLOAT16_CONVERSIONS__ +#undef __CUDA_NO_HALF2_OPERATORS__ + +#include +#include +#include +#include // for int64_t + +#include +#include +#include +#include +#include +#include +#include + +#define MAX_SEQ_LEN 8192 +#define MAX_VISION_TOKENS 64 +#define WARP_SIZE 32 +#define MAX_THREADS_PER_BLOCK 1024 + +struct VisionDescriptor +{ + int64_t start_pos; + int64_t token_pos; + int64_t patch_count; + int64_t grid_t, grid_h, grid_w; + float time_interval; + int64_t is_video; + int64_t position_offset; +}; + +__device__ __forceinline__ int64_t fast_div(int64_t a, int64_t b) +{ + return __float2ll_rd(__ll2float_rn(a) * __frcp_rn(__ll2float_rn(b))); +} + +__device__ __forceinline__ void get_3d_coords(int64_t patch_idx, int64_t H, int64_t W, + int64_t &t, int64_t &h, int64_t &w) +{ + int64_t hw = H * W; + t = fast_div(patch_idx, hw); + int64_t remaining = patch_idx - t * hw; + h = fast_div(remaining, W); + w = remaining - h * W; +} + +__global__ void compute_vision_counts( + const int64_t *input_ids, // (batch_size, seq_len) + const int64_t *attention_mask, // (batch_size, seq_len) + int64_t *image_counts, // (batch_size,) + int64_t *video_counts, // (batch_size,) + const int64_t batch_size, + const int64_t seq_len, + const int64_t image_token_id, + const int64_t video_token_id, + const int64_t vision_start_token_id) +{ + int64_t batch_idx = blockIdx.x; + int64_t thread_idx = threadIdx.x; + + if (batch_idx >= batch_size) + return; + + __shared__ int64_t shared_image_counts[MAX_THREADS_PER_BLOCK]; + __shared__ int64_t shared_video_counts[MAX_THREADS_PER_BLOCK]; + + int64_t thread_image_count = 0; + int64_t thread_video_count = 0; + + for (int64_t i = thread_idx; i < seq_len - 1; i += blockDim.x) + { + if ((attention_mask != nullptr) && attention_mask[batch_idx * seq_len + i] == 0) + continue; + + int64_t token_id = input_ids[batch_idx * seq_len + i]; + + if (token_id == vision_start_token_id && i + 1 < seq_len) + { + int64_t next_token = input_ids[batch_idx * seq_len + i + 1]; + + if (next_token == image_token_id) + { + thread_image_count++; + } + else if (next_token == video_token_id) + { + thread_video_count++; + } + } + } + + shared_image_counts[thread_idx] = thread_image_count; + shared_video_counts[thread_idx] = thread_video_count; + __syncthreads(); + + for (int64_t stride = blockDim.x / 2; stride > 0; stride /= 2) + { + if (thread_idx < stride) + { + shared_image_counts[thread_idx] += shared_image_counts[thread_idx + stride]; + shared_video_counts[thread_idx] += shared_video_counts[thread_idx + stride]; + } + __syncthreads(); + } + + if (thread_idx == 0) + { + image_counts[batch_idx] = shared_image_counts[0]; + video_counts[batch_idx] = shared_video_counts[0]; + } +} + + +__global__ void preprocess_vision_tokens( + const int64_t *input_ids, // (batch_size, seq_len) + const int64_t *attention_mask, // (batch_size, seq_len) + const int64_t *image_grid_thw, // (max_images, 3) + const int64_t *video_grid_thw, // (max_videos, 3) + const float *second_per_grid_ts, // (max_videos,) + const int64_t *image_counts, // (batch_size,) + const int64_t *video_counts, // (batch_size,) + VisionDescriptor *vision_desc, // (batch_size, MAX_VISION_TOKENS) + int64_t *vision_counts, // (batch_size,) + int64_t *text_lengths, // (batch_size, MAX_VISION_TOKENS+1) + int64_t *position_offsets, // (batch_size, MAX_VISION_TOKENS+1) + const int64_t batch_size, + const int64_t seq_len, + const int64_t spatial_merge_size, + const int64_t image_token_id, + const int64_t video_token_id, + const int64_t vision_start_token_id, + const float tokens_per_second) +{ + int64_t batch_idx = blockIdx.x * blockDim.x + threadIdx.x; + if (batch_idx >= batch_size) + return; + + int64_t image_idx = 0, video_idx = 0; + for (int64_t i = 0; i < batch_idx; i++) + { + image_idx += image_counts[i]; + video_idx += video_counts[i]; + } + + int64_t vision_count = 0; + int64_t current_pos = 0; + int64_t position_offset = 0; + + for (int64_t i = 0; i < seq_len - 1; i++) + { + if ((attention_mask != nullptr) && attention_mask[batch_idx * seq_len + i] == 0) + continue; + + int64_t token_id = input_ids[batch_idx * seq_len + i]; + + if (token_id == vision_start_token_id) + { + int64_t next_token = input_ids[batch_idx * seq_len + i + 1]; + + if (next_token == image_token_id || next_token == video_token_id) + { + int64_t vision_pos = i + 1; + text_lengths[batch_idx * (MAX_VISION_TOKENS + 1) + vision_count] = vision_pos - current_pos; + position_offsets[batch_idx * (MAX_VISION_TOKENS + 1) + vision_count] = position_offset; + position_offset += (vision_pos - current_pos); + + int64_t T, H, W; + float time_interval = 0.0f; + int64_t is_video = (next_token == video_token_id) ? 1 : 0; + + if (is_video == 0) + { + T = image_grid_thw[image_idx * 3 + 0]; + H = image_grid_thw[image_idx * 3 + 1]; + W = image_grid_thw[image_idx * 3 + 2]; + image_idx++; + } + else + { + T = video_grid_thw[video_idx * 3 + 0]; + H = video_grid_thw[video_idx * 3 + 1]; + W = video_grid_thw[video_idx * 3 + 2]; + time_interval = second_per_grid_ts[video_idx]; + video_idx++; + } + + int64_t H_merged = H / spatial_merge_size; + int64_t W_merged = W / spatial_merge_size; + int64_t patch_count = T * H_merged * W_merged; + + if (vision_count < MAX_VISION_TOKENS) + { + VisionDescriptor &desc = vision_desc[batch_idx * MAX_VISION_TOKENS + vision_count]; + desc.start_pos = current_pos; + desc.token_pos = vision_pos; + desc.patch_count = patch_count; + desc.grid_t = T; + desc.grid_h = H_merged; + desc.grid_w = W_merged; + desc.time_interval = time_interval; + desc.is_video = is_video; + desc.position_offset = position_offset; + + if (is_video) + { + position_offset += max(static_cast((T - 1) * time_interval * tokens_per_second) + 1, + static_cast(max(H_merged, W_merged))); + } + else + { + position_offset += max(H_merged, W_merged); + } + + current_pos = vision_pos + patch_count; + vision_count++; + } + + i = vision_pos + patch_count - 1; + } + } + } + + vision_counts[batch_idx] = vision_count; + + int64_t effective_len = seq_len; + if (attention_mask != nullptr) + { + for (int64_t i = seq_len - 1; i >= 0; i--) + { + if (attention_mask[batch_idx * seq_len + i] != 0) + { + effective_len = i + 1; + break; + } + } + } + + text_lengths[batch_idx * (MAX_VISION_TOKENS + 1) + vision_count] = effective_len - current_pos; + position_offsets[batch_idx * (MAX_VISION_TOKENS + 1) + vision_count] = position_offset; +} + + +__global__ void compute_3d_positions( + const int64_t *input_ids, // (batch_size, seq_len) + const int64_t *attention_mask, // (batch_size, seq_len) + const VisionDescriptor *vision_desc, // (batch_size, MAX_VISION_TOKENS) + const int64_t *vision_counts, // (batch_size,) + const int64_t *text_lengths, // (batch_size, MAX_VISION_TOKENS+1) + const int64_t *position_offsets, // (batch_size, MAX_VISION_TOKENS+1) + int64_t *position_ids, // (3, batch_size, seq_len) + int64_t *mrope_deltas, // (batch_size,) + const int64_t batch_size, + const int64_t seq_len, + const float tokens_per_second) +{ + int64_t batch_idx = blockIdx.x; + int64_t thread_idx = threadIdx.x; + + if (batch_idx >= batch_size) + return; + + __shared__ VisionDescriptor shared_visions[MAX_VISION_TOKENS]; + __shared__ int64_t shared_position_offsets[MAX_VISION_TOKENS + 1]; + __shared__ int64_t shared_max_positions[MAX_THREADS_PER_BLOCK]; + + int64_t shared_vision_count = vision_counts[batch_idx]; + + for (int64_t i = thread_idx; i < MAX_VISION_TOKENS; i += blockDim.x) + { + if (i < shared_vision_count) + { + shared_visions[i] = vision_desc[batch_idx * MAX_VISION_TOKENS + i]; + } + } + + for (int64_t i = thread_idx; i < MAX_VISION_TOKENS + 1; i += blockDim.x) + { + shared_position_offsets[i] = position_offsets[batch_idx * (MAX_VISION_TOKENS + 1) + i]; + } + + int64_t thread_max_position = -1; + + __syncthreads(); + + for (int64_t token_idx = thread_idx; token_idx < seq_len; token_idx += blockDim.x) + { + bool is_valid_token = true; + if (attention_mask != nullptr) + { + is_valid_token = attention_mask[batch_idx * seq_len + token_idx] != 0; + } + + if (!is_valid_token) + { + position_ids[0 * batch_size * seq_len + batch_idx * seq_len + token_idx] = 1; + position_ids[1 * batch_size * seq_len + batch_idx * seq_len + token_idx] = 1; + position_ids[2 * batch_size * seq_len + batch_idx * seq_len + token_idx] = 1; + continue; + } + + int64_t segment_idx = -9999999; + int64_t local_pos = token_idx; + + for (int64_t v = 0; v < shared_vision_count; v++) + { + if (token_idx < shared_visions[v].token_pos) + { + segment_idx = v; + local_pos = token_idx - (v > 0 ? shared_visions[v - 1].token_pos + shared_visions[v - 1].patch_count : 0); + break; + } + else if (token_idx < shared_visions[v].token_pos + shared_visions[v].patch_count) + { + segment_idx = -(v + 1); + local_pos = token_idx - shared_visions[v].token_pos; + break; + } + } + + if (segment_idx == -9999999) + { + segment_idx = shared_vision_count; + int64_t last_vision_end = 0; + if (shared_vision_count > 0) + { + last_vision_end = shared_visions[shared_vision_count - 1].token_pos + + shared_visions[shared_vision_count - 1].patch_count; + } + local_pos = token_idx - last_vision_end; + } + + int64_t pos_t, pos_h, pos_w; + + if (segment_idx >= 0) + { + int64_t offset = shared_position_offsets[segment_idx]; + pos_t = pos_h = pos_w = offset + local_pos; + } + else + { + int64_t vision_idx = -(segment_idx + 1); + const VisionDescriptor &desc = shared_visions[vision_idx]; + + int64_t t, h, w; + get_3d_coords(local_pos, desc.grid_h, desc.grid_w, t, h, w); + + pos_t = static_cast(t * desc.time_interval * tokens_per_second) + desc.position_offset; + pos_h = h + desc.position_offset; + pos_w = w + desc.position_offset; + } + + position_ids[0 * batch_size * seq_len + batch_idx * seq_len + token_idx] = pos_t; + position_ids[1 * batch_size * seq_len + batch_idx * seq_len + token_idx] = pos_h; + position_ids[2 * batch_size * seq_len + batch_idx * seq_len + token_idx] = pos_w; + + int64_t max_pos = max(pos_t, max(pos_h, pos_w)); + thread_max_position = max(thread_max_position, max_pos); + } + + shared_max_positions[thread_idx] = thread_max_position; + __syncthreads(); + + for (int64_t stride = blockDim.x / 2; stride > 0; stride /= 2) + { + if (thread_idx < stride) + { + shared_max_positions[thread_idx] = max(shared_max_positions[thread_idx], + shared_max_positions[thread_idx + stride]); + } + __syncthreads(); + } + + if (thread_idx == 0) + { + int64_t global_max_position = shared_max_positions[0]; + mrope_deltas[batch_idx] = global_max_position + 1 - seq_len; + } +} + + +void launch_optimized_3d_rope_kernel( + const int64_t *input_ids, + const int64_t *attention_mask, + const int64_t *image_grid_thw, + const int64_t *video_grid_thw, + const float *second_per_grid_ts, + int64_t *position_ids, + int64_t *mrope_deltas, + int64_t batch_size, + int64_t seq_len, + int64_t spatial_merge_size, + int64_t image_token_id, + int64_t video_token_id, + int64_t vision_start_token_id, + float tokens_per_second) +{ + cudaStream_t stream = at::cuda::getCurrentCUDAStream().stream(); + + torch::Device device(torch::kCUDA, at::cuda::current_device()); + + auto options = torch::TensorOptions().dtype(torch::kInt64).device(device); + + auto vision_desc_tensor = torch::empty( + {batch_size * MAX_VISION_TOKENS * static_cast(sizeof(VisionDescriptor))}, + torch::TensorOptions().dtype(torch::kUInt8).device(device) + ); + VisionDescriptor *d_vision_desc = reinterpret_cast(vision_desc_tensor.data_ptr()); + + auto vision_counts_tensor = torch::empty({batch_size}, options); + auto text_lengths_tensor = torch::empty({batch_size * (MAX_VISION_TOKENS + 1)}, options); + auto position_offsets_tensor = torch::empty({batch_size * (MAX_VISION_TOKENS + 1)}, options); + auto image_counts_tensor = torch::empty({batch_size}, options); + auto video_counts_tensor = torch::empty({batch_size}, options); + + int64_t *d_vision_counts = vision_counts_tensor.data_ptr(); + int64_t *d_text_lengths = text_lengths_tensor.data_ptr(); + int64_t *d_position_offsets = position_offsets_tensor.data_ptr(); + int64_t *d_image_counts = image_counts_tensor.data_ptr(); + int64_t *d_video_counts = video_counts_tensor.data_ptr(); + + dim3 index_grid(static_cast(batch_size)); + dim3 index_block(256); + + compute_vision_counts<<>>( + input_ids, attention_mask, + d_image_counts, d_video_counts, + batch_size, seq_len, image_token_id, video_token_id, vision_start_token_id); + + int64_t threads_per_block = std::min(batch_size, static_cast(256)); + int64_t num_blocks = (batch_size + threads_per_block - 1) / threads_per_block; + + dim3 preprocess_grid(static_cast(num_blocks)); + dim3 preprocess_block(static_cast(threads_per_block)); + + preprocess_vision_tokens<<>>( + input_ids, attention_mask, image_grid_thw, video_grid_thw, + second_per_grid_ts, d_image_counts, d_video_counts, + d_vision_desc, d_vision_counts, d_text_lengths, d_position_offsets, + batch_size, seq_len, spatial_merge_size, + image_token_id, video_token_id, vision_start_token_id, tokens_per_second); + + threads_per_block = std::min(static_cast(seq_len), static_cast(MAX_THREADS_PER_BLOCK)); + if (threads_per_block < 32) threads_per_block = 32; + + int64_t power_of_2 = 1; + while (power_of_2 < threads_per_block) power_of_2 *= 2; + if (power_of_2 > MAX_THREADS_PER_BLOCK) power_of_2 = MAX_THREADS_PER_BLOCK; + threads_per_block = power_of_2; + + dim3 compute_grid(static_cast(batch_size)); + dim3 compute_block(static_cast(threads_per_block)); + + compute_3d_positions<<>>( + input_ids, attention_mask, d_vision_desc, d_vision_counts, + d_text_lengths, d_position_offsets, position_ids, mrope_deltas, + batch_size, seq_len, tokens_per_second); + + AT_CUDA_CHECK(cudaGetLastError()); +} + +std::tuple get_rope_index( + const torch::optional &input_ids, + const torch::optional &image_grid_thw, + const torch::optional &video_grid_thw, + const torch::optional &second_per_grid_ts, + const torch::optional &attention_mask, + int spatial_merge_size, + int image_token_id, + int video_token_id, + int vision_start_token_id, + float tokens_per_second) +{ + TORCH_CHECK(input_ids.has_value(), "input_ids cannot be None"); + TORCH_CHECK(input_ids->dim() == 2, "input_ids must be 2D tensor (batch_size, seq_len)"); + + const auto batch_size = input_ids->size(0); + const auto seq_len = input_ids->size(1); + const auto device = input_ids->device(); + + TORCH_CHECK(device.is_cuda(), "All tensors must be on CUDA device"); + + torch::Tensor input_ids_tensor = input_ids->contiguous(); + const int64_t *input_ids_ptr = input_ids_tensor.data_ptr(); + + torch::Tensor attention_mask_tensor; + const int64_t *attention_mask_ptr = nullptr; + + if (attention_mask.has_value()) { + attention_mask_tensor = attention_mask->contiguous(); + attention_mask_ptr = attention_mask_tensor.data_ptr(); + } + + torch::Tensor image_grid_thw_tensor; + const int64_t *image_grid_thw_ptr = nullptr; + + if (image_grid_thw.has_value()) { + TORCH_CHECK(image_grid_thw->dim() == 2 && image_grid_thw->size(1) == 3, + "image_grid_thw must be shape (num_images, 3)"); + image_grid_thw_tensor = image_grid_thw->contiguous(); + image_grid_thw_ptr = image_grid_thw_tensor.data_ptr(); + } + + torch::Tensor video_grid_thw_tensor; + const int64_t *video_grid_thw_ptr = nullptr; + + if (video_grid_thw.has_value()) { + TORCH_CHECK(video_grid_thw->dim() == 2 && video_grid_thw->size(1) == 3, + "video_grid_thw must be shape (num_videos, 3)"); + video_grid_thw_tensor = video_grid_thw->contiguous(); + video_grid_thw_ptr = video_grid_thw_tensor.data_ptr(); + } + + torch::Tensor second_per_grid_ts_tensor; + const float *second_per_grid_ts_ptr = nullptr; + + if (second_per_grid_ts.has_value()) { + TORCH_CHECK(second_per_grid_ts->dim() == 1, + "second_per_grid_ts must be 1D tensor"); + second_per_grid_ts_tensor = second_per_grid_ts->contiguous(); + second_per_grid_ts_ptr = second_per_grid_ts_tensor.data_ptr(); + } + + if (!image_grid_thw.has_value() && !video_grid_thw.has_value()) { + torch::Tensor position_ids; + torch::Tensor mrope_deltas; + + if (attention_mask.has_value()) { + auto cumsum_result = attention_mask_tensor.to(torch::kInt64).cumsum(-1) - 1; + position_ids = cumsum_result.masked_fill_(attention_mask_tensor.eq(0), 1); + position_ids = position_ids.unsqueeze(0).expand({3, -1, -1}); + + auto max_position_ids = std::get<0>(std::get<0>(position_ids.max(0)).max(-1, true)); + mrope_deltas = max_position_ids + 1 - attention_mask_tensor.size(-1); + mrope_deltas = mrope_deltas.view({batch_size, 1}); + } else { + auto pos_range = torch::arange(seq_len, torch::TensorOptions().dtype(torch::kInt64).device(device)); + position_ids = pos_range.view({1, 1, -1}).expand({3, batch_size, -1}); + mrope_deltas = torch::zeros({batch_size, 1}, torch::TensorOptions().dtype(torch::kInt64).device(device)); + } + + return std::make_tuple(position_ids, mrope_deltas); + } + + auto position_ids = torch::empty({3, batch_size, seq_len}, + torch::TensorOptions().dtype(torch::kInt64).device(device)); + auto mrope_deltas = torch::empty({batch_size, 1}, + torch::TensorOptions().dtype(torch::kInt64).device(device)); + + launch_optimized_3d_rope_kernel( + input_ids_ptr, + attention_mask_ptr, + image_grid_thw_ptr, + video_grid_thw_ptr, + second_per_grid_ts_ptr, + position_ids.data_ptr(), + mrope_deltas.data_ptr(), + static_cast(batch_size), + static_cast(seq_len), + static_cast(spatial_merge_size), + static_cast(image_token_id), + static_cast(video_token_id), + static_cast(vision_start_token_id), + tokens_per_second); + + return std::make_tuple(position_ids, mrope_deltas); +} diff --git a/csrc/rope_index.h b/csrc/rope_index.h new file mode 100644 index 0000000..643d78a --- /dev/null +++ b/csrc/rope_index.h @@ -0,0 +1,13 @@ +#include + +std::tuple get_rope_index( + const torch::optional &input_ids, + const torch::optional &image_grid_thw, + const torch::optional &video_grid_thw, + const torch::optional &second_per_grid_ts, + const torch::optional &attention_mask, + int spatial_merge_size, + int image_token_id, + int video_token_id, + int vision_start_token_id, + float tokens_per_second); diff --git a/csrc/rot_pos.cu b/csrc/rot_pos.cu new file mode 100644 index 0000000..4db08ac --- /dev/null +++ b/csrc/rot_pos.cu @@ -0,0 +1,332 @@ +#include +#include +#include +#include + +// CUDA kernel for fused rotary position embedding computation - int32 version +__global__ void fused_rot_pos_emb_kernel_int32( + const float *__restrict__ inv_freq, // [dim/2] - precomputed inverse frequencies + const int32_t *__restrict__ grid_thw, // [num_grids, 3] - (t, h, w) for each grid + float *__restrict__ output, // [total_tokens, dim] - output rotary embeddings + const int32_t *__restrict__ cumsum_tokens, // [num_grids+1] - cumulative sum of tokens per grid + const int dim_half, // dim/2 (size of inv_freq) + const int spatial_merge_size, // spatial merge size + const int num_grids // number of grids +) +{ + const int32_t tid = blockIdx.x * blockDim.x + threadIdx.x; + const int32_t total_tokens = cumsum_tokens[num_grids]; + + if (tid >= total_tokens * dim_half) + return; + + const int32_t token_idx = tid / dim_half; + const int freq_idx = tid % dim_half; + + // Find which grid this token belongs to + int grid_idx = 0; + int32_t local_token_idx = token_idx; + for (int g = 0; g < num_grids; g++) + { + if (token_idx < cumsum_tokens[g + 1]) + { + grid_idx = g; + local_token_idx = token_idx - cumsum_tokens[g]; + break; + } + } + + // Get grid dimensions + const int32_t h = grid_thw[grid_idx * 3 + 1]; + const int32_t w = grid_thw[grid_idx * 3 + 2]; + + // Calculate spatial dimensions after merging + const int32_t h_merged = h / spatial_merge_size; + const int32_t w_merged = w / spatial_merge_size; + const int32_t spatial_tokens = h_merged * w_merged * spatial_merge_size * spatial_merge_size; + + // Get spatial index + const int32_t spatial_idx = local_token_idx % spatial_tokens; + + // Decompose spatial index to get merged block and position within block + const int32_t tokens_per_block = spatial_merge_size * spatial_merge_size; + const int32_t block_idx = spatial_idx / tokens_per_block; + const int32_t within_block_idx = spatial_idx % tokens_per_block; + + // Get block coordinates in merged grid + const int32_t block_h = block_idx / w_merged; + const int32_t block_w = block_idx % w_merged; + + // Get position within block + const int32_t within_h = within_block_idx / spatial_merge_size; + const int32_t within_w = within_block_idx % spatial_merge_size; + + // Calculate actual h and w positions + const int32_t h_pos = block_h * spatial_merge_size + within_h; + const int32_t w_pos = block_w * spatial_merge_size + within_w; + + // Compute rotary embedding + float freq_val = inv_freq[freq_idx]; + + // Output has shape [total_tokens, dim] where dim = 2 * dim_half + int32_t out_idx = token_idx * dim_half * 2 + freq_idx; + output[out_idx] = h_pos * freq_val; // h_pos frequencies + output[out_idx + dim_half] = w_pos * freq_val; // w_pos frequencies +} + +// CUDA kernel for fused rotary position embedding computation - int64 version +__global__ void fused_rot_pos_emb_kernel_int64( + const float *__restrict__ inv_freq, // [dim/2] - precomputed inverse frequencies + const int64_t *__restrict__ grid_thw, // [num_grids, 3] - (t, h, w) for each grid + float *__restrict__ output, // [total_tokens, dim] - output rotary embeddings + const int64_t *__restrict__ cumsum_tokens, // [num_grids+1] - cumulative sum of tokens per grid + const int dim_half, // dim/2 (size of inv_freq) + const int spatial_merge_size, // spatial merge size + const int num_grids // number of grids +) +{ + const int64_t tid = blockIdx.x * blockDim.x + threadIdx.x; + const int64_t total_tokens = cumsum_tokens[num_grids]; + + if (tid >= total_tokens * dim_half) + return; + + const int64_t token_idx = tid / dim_half; + const int freq_idx = tid % dim_half; + + // Find which grid this token belongs to + int grid_idx = 0; + int64_t local_token_idx = token_idx; + for (int g = 0; g < num_grids; g++) + { + if (token_idx < cumsum_tokens[g + 1]) + { + grid_idx = g; + local_token_idx = token_idx - cumsum_tokens[g]; + break; + } + } + + // Get grid dimensions + const int64_t h = grid_thw[grid_idx * 3 + 1]; + const int64_t w = grid_thw[grid_idx * 3 + 2]; + + // Calculate spatial dimensions after merging + const int64_t h_merged = h / spatial_merge_size; + const int64_t w_merged = w / spatial_merge_size; + const int64_t spatial_tokens = h_merged * w_merged * spatial_merge_size * spatial_merge_size; + + // Get spatial index + const int64_t spatial_idx = local_token_idx % spatial_tokens; + + // Decompose spatial index to get merged block and position within block + const int64_t tokens_per_block = spatial_merge_size * spatial_merge_size; + const int64_t block_idx = spatial_idx / tokens_per_block; + const int64_t within_block_idx = spatial_idx % tokens_per_block; + + // Get block coordinates in merged grid + const int64_t block_h = block_idx / w_merged; + const int64_t block_w = block_idx % w_merged; + + // Get position within block + const int64_t within_h = within_block_idx / spatial_merge_size; + const int64_t within_w = within_block_idx % spatial_merge_size; + + // Calculate actual h and w positions + const int64_t h_pos = block_h * spatial_merge_size + within_h; + const int64_t w_pos = block_w * spatial_merge_size + within_w; + + // Compute rotary embedding + float freq_val = inv_freq[freq_idx]; + + // Output has shape [total_tokens, dim] where dim = 2 * dim_half + int64_t out_idx = token_idx * dim_half * 2 + freq_idx; + output[out_idx] = h_pos * freq_val; // h_pos frequencies + output[out_idx + dim_half] = w_pos * freq_val; // w_pos frequencies +} + +// Parallel computation of token counts per grid - int32 version +__global__ void compute_token_counts_kernel_int32( + const int32_t *__restrict__ grid_thw, + int32_t *__restrict__ token_counts, + const int spatial_merge_size, + const int num_grids) +{ + const int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= num_grids) + return; + + int32_t t = grid_thw[idx * 3 + 0]; + int32_t h = grid_thw[idx * 3 + 1]; + int32_t w = grid_thw[idx * 3 + 2]; + int32_t h_merged = h / spatial_merge_size; + int32_t w_merged = w / spatial_merge_size; + token_counts[idx] = t * h_merged * w_merged * spatial_merge_size * spatial_merge_size; +} + +// Parallel computation of token counts per grid - int64 version +__global__ void compute_token_counts_kernel_int64( + const int64_t *__restrict__ grid_thw, + int64_t *__restrict__ token_counts, + const int spatial_merge_size, + const int num_grids) +{ + const int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= num_grids) + return; + + int64_t t = grid_thw[idx * 3 + 0]; + int64_t h = grid_thw[idx * 3 + 1]; + int64_t w = grid_thw[idx * 3 + 2]; + int64_t h_merged = h / spatial_merge_size; + int64_t w_merged = w / spatial_merge_size; + token_counts[idx] = t * h_merged * w_merged * spatial_merge_size * spatial_merge_size; +} + +// Implementation for int32 +torch::Tensor fused_rot_pos_emb_cuda_int32( + torch::Tensor inv_freq, // [dim/2] + torch::Tensor grid_thw, // [num_grids, 3] + int spatial_merge_size) +{ + TORCH_CHECK(inv_freq.dim() == 1, "inv_freq must be 1-dimensional"); + TORCH_CHECK(inv_freq.is_cuda(), "inv_freq must be a CUDA tensor"); + TORCH_CHECK(inv_freq.scalar_type() == torch::kFloat32, "inv_freq must be float32"); + + TORCH_CHECK(grid_thw.dim() == 2, "grid_thw must be 2-dimensional"); + TORCH_CHECK(grid_thw.size(1) == 3, "grid_thw must have shape [num_grids, 3]"); + TORCH_CHECK(grid_thw.is_cuda(), "grid_thw must be a CUDA tensor"); + TORCH_CHECK(grid_thw.scalar_type() == torch::kInt32, "grid_thw must be int32"); + + TORCH_CHECK(spatial_merge_size > 0, "spatial_merge_size must be positive"); + + const int dim_half = inv_freq.size(0); + const int num_grids = grid_thw.size(0); + + auto token_counts = torch::zeros({num_grids}, torch::TensorOptions().dtype(torch::kInt32).device(grid_thw.device())); + const int threads = 256; + const int blocks = (num_grids + threads - 1) / threads; + + compute_token_counts_kernel_int32<<>>( + grid_thw.data_ptr(), + token_counts.data_ptr(), + spatial_merge_size, + num_grids); + + auto cumsum_tokens = torch::cat({torch::zeros({1}, torch::TensorOptions().dtype(torch::kInt32).device(grid_thw.device())), + token_counts.cumsum(0).to(torch::kInt32)}, + 0); + + cudaDeviceSynchronize(); + + int64_t total_tokens = cumsum_tokens[-1].item(); + TORCH_CHECK(total_tokens > 0, "total_tokens must be positive"); + + auto output = torch::zeros({total_tokens, dim_half * 2}, + torch::TensorOptions().dtype(torch::kFloat32).device(inv_freq.device())); + + const int threads_per_block = 256; + const int64_t num_elements = total_tokens * dim_half; + const int num_blocks = static_cast((num_elements + threads_per_block - 1) / threads_per_block); + + fused_rot_pos_emb_kernel_int32<<>>( + inv_freq.data_ptr(), + grid_thw.data_ptr(), + output.data_ptr(), + cumsum_tokens.data_ptr(), + dim_half, + spatial_merge_size, + num_grids); + + cudaDeviceSynchronize(); + + TORCH_CHECK(output.scalar_type() == torch::kFloat32, "Output must be float32"); + TORCH_CHECK(output.size(0) == total_tokens, "Output token count mismatch"); + TORCH_CHECK(output.size(1) == dim_half * 2, "Output dimension mismatch"); + + return output; +} + +// Implementation for int64 +torch::Tensor fused_rot_pos_emb_cuda_int64( + torch::Tensor inv_freq, // [dim/2] + torch::Tensor grid_thw, // [num_grids, 3] + int spatial_merge_size) +{ + TORCH_CHECK(inv_freq.dim() == 1, "inv_freq must be 1-dimensional"); + TORCH_CHECK(inv_freq.is_cuda(), "inv_freq must be a CUDA tensor"); + TORCH_CHECK(inv_freq.scalar_type() == torch::kFloat32, "inv_freq must be float32"); + + TORCH_CHECK(grid_thw.dim() == 2, "grid_thw must be 2-dimensional"); + TORCH_CHECK(grid_thw.size(1) == 3, "grid_thw must have shape [num_grids, 3]"); + TORCH_CHECK(grid_thw.is_cuda(), "grid_thw must be a CUDA tensor"); + TORCH_CHECK(grid_thw.scalar_type() == torch::kInt64, "grid_thw must be int64"); + + TORCH_CHECK(spatial_merge_size > 0, "spatial_merge_size must be positive"); + + const int dim_half = inv_freq.size(0); + const int num_grids = grid_thw.size(0); + + auto token_counts = torch::zeros({num_grids}, torch::TensorOptions().dtype(torch::kInt64).device(grid_thw.device())); + const int threads = 256; + const int blocks = (num_grids + threads - 1) / threads; + + compute_token_counts_kernel_int64<<>>( + grid_thw.data_ptr(), + token_counts.data_ptr(), + spatial_merge_size, + num_grids); + + auto cumsum_tokens = torch::cat({torch::zeros({1}, torch::TensorOptions().dtype(torch::kInt64).device(grid_thw.device())), + token_counts.cumsum(0).to(torch::kInt64)}, + 0); + + cudaDeviceSynchronize(); + + int64_t total_tokens = cumsum_tokens[-1].item(); + TORCH_CHECK(total_tokens > 0, "total_tokens must be positive"); + + auto output = torch::zeros({total_tokens, dim_half * 2}, + torch::TensorOptions().dtype(torch::kFloat32).device(inv_freq.device())); + + const int threads_per_block = 256; + const int64_t num_elements = total_tokens * dim_half; + const int num_blocks = static_cast((num_elements + threads_per_block - 1) / threads_per_block); + + fused_rot_pos_emb_kernel_int64<<>>( + inv_freq.data_ptr(), + grid_thw.data_ptr(), + output.data_ptr(), + cumsum_tokens.data_ptr(), + dim_half, + spatial_merge_size, + num_grids); + + cudaDeviceSynchronize(); + + TORCH_CHECK(output.scalar_type() == torch::kFloat32, "Output must be float32"); + TORCH_CHECK(output.size(0) == total_tokens, "Output token count mismatch"); + TORCH_CHECK(output.size(1) == dim_half * 2, "Output dimension mismatch"); + + return output; +} + +// Main function that dispatches based on grid_thw scalar type +torch::Tensor fused_rot_pos_emb_cuda( + torch::Tensor inv_freq, + torch::Tensor grid_thw, + int spatial_merge_size) +{ + if (grid_thw.scalar_type() == torch::kInt32) + { + return fused_rot_pos_emb_cuda_int32(inv_freq, grid_thw, spatial_merge_size); + } + else if (grid_thw.scalar_type() == torch::kInt64) + { + return fused_rot_pos_emb_cuda_int64(inv_freq, grid_thw, spatial_merge_size); + } + else + { + TORCH_CHECK(false, "Unsupported grid_thw scalar type: ", grid_thw.scalar_type()); + } +} diff --git a/csrc/rot_pos.h b/csrc/rot_pos.h new file mode 100644 index 0000000..1ed7edd --- /dev/null +++ b/csrc/rot_pos.h @@ -0,0 +1,6 @@ +#include + +torch::Tensor fused_rot_pos_emb_cuda( + torch::Tensor inv_freq, + torch::Tensor grid_thw, + int spatial_merge_size); diff --git a/csrc/window_index.cu b/csrc/window_index.cu new file mode 100644 index 0000000..4a1486b --- /dev/null +++ b/csrc/window_index.cu @@ -0,0 +1,286 @@ +#include +#include +#include +#include +#include + +__global__ void compute_metadata( + const int *grid_thw, // [num_grids, 3] + int *grid_info, // [num_grids, 6]: [grid_elements, grid_windows, llm_h, llm_w, num_windows_h, num_windows_w] + int *global_totals, // [total_elements, total_windows] + int num_grids, + int spatial_merge_size, + int vit_merger_window_size) +{ + int grid_idx = blockIdx.x * blockDim.x + threadIdx.x; + if (grid_idx >= num_grids) + return; + + int grid_t = grid_thw[grid_idx * 3 + 0]; + int grid_h = grid_thw[grid_idx * 3 + 1]; + int grid_w = grid_thw[grid_idx * 3 + 2]; + + int llm_h = grid_h / spatial_merge_size; + int llm_w = grid_w / spatial_merge_size; + + int pad_h = (vit_merger_window_size - llm_h % vit_merger_window_size) % vit_merger_window_size; + int pad_w = (vit_merger_window_size - llm_w % vit_merger_window_size) % vit_merger_window_size; + + int num_windows_h = (llm_h + pad_h) / vit_merger_window_size; + int num_windows_w = (llm_w + pad_w) / vit_merger_window_size; + + int grid_elements = grid_t * llm_h * llm_w; + int grid_windows = grid_t * num_windows_h * num_windows_w; + + grid_info[grid_idx * 6 + 0] = grid_elements; + grid_info[grid_idx * 6 + 1] = grid_windows; + grid_info[grid_idx * 6 + 2] = llm_h; + grid_info[grid_idx * 6 + 3] = llm_w; + grid_info[grid_idx * 6 + 4] = num_windows_h; + grid_info[grid_idx * 6 + 5] = num_windows_w; + + atomicAdd(&global_totals[0], grid_elements); + atomicAdd(&global_totals[1], grid_windows); +} + +__global__ void compute_window_counts( + const int *grid_thw, + const int *grid_info, + int *window_counts, + int vit_merger_window_size, + int spatial_merge_unit, + int num_grids) +{ + int grid_idx = blockIdx.y; + int t_idx = blockIdx.x; + + if (grid_idx >= num_grids) + return; + + int grid_t = grid_thw[grid_idx * 3 + 0]; + if (t_idx >= grid_t) + return; + + int llm_h = grid_info[grid_idx * 6 + 2]; + int llm_w = grid_info[grid_idx * 6 + 3]; + int num_windows_h = grid_info[grid_idx * 6 + 4]; + int num_windows_w = grid_info[grid_idx * 6 + 5]; + + int window_base = 0; + for (int g = 0; g < grid_idx; g++) + { + window_base += grid_info[g * 6 + 1]; + } + + int t_window_base = window_base + t_idx * num_windows_h * num_windows_w; + + int thread_id = threadIdx.x; + int warp_id = thread_id / 32; + int lane_id = thread_id % 32; + + int windows_per_t = num_windows_h * num_windows_w; + int warps_per_block = blockDim.x / 32; + + if (lane_id == 0) + { + for (int window_idx = warp_id; window_idx < windows_per_t; window_idx += warps_per_block) + { + int win_h = window_idx / num_windows_w; + int win_w = window_idx % num_windows_w; + + int start_h = win_h * vit_merger_window_size; + int start_w = win_w * vit_merger_window_size; + + int valid_h = min(vit_merger_window_size, llm_h - start_h); + int valid_w = min(vit_merger_window_size, llm_w - start_w); + + int valid_count = (valid_h > 0 && valid_w > 0) ? valid_h * valid_w : 0; + + window_counts[t_window_base + window_idx] = valid_count; + } + } +} + +__global__ void compute_cu_window_seqlens( + const int *window_counts, + int *cu_window_seqlens, // [total_windows + 1] + int total_windows, + int spatial_merge_unit) +{ + int tid = blockIdx.x * blockDim.x + threadIdx.x; + + if (tid == 0) + { + cu_window_seqlens[0] = 0; + } + + if (tid < total_windows) + { + cu_window_seqlens[tid + 1] = window_counts[tid] * spatial_merge_unit; + } + + __syncthreads(); + + if (tid == 0) + { + for (int i = 1; i <= total_windows; i++) + { + cu_window_seqlens[i] += cu_window_seqlens[i - 1]; + } + } +} + +__global__ void generate_window_indices( + const int *grid_thw, + const int *grid_info, + const int *cu_window_seqlens, + int *window_indices, + int vit_merger_window_size, + int spatial_merge_unit, + int num_grids) +{ + int grid_idx = blockIdx.y; + int t_idx = blockIdx.x; + + if (grid_idx >= num_grids) + return; + + int grid_t = grid_thw[grid_idx * 3 + 0]; + if (t_idx >= grid_t) + return; + + int llm_h = grid_info[grid_idx * 6 + 2]; + int llm_w = grid_info[grid_idx * 6 + 3]; + int num_windows_h = grid_info[grid_idx * 6 + 4]; + int num_windows_w = grid_info[grid_idx * 6 + 5]; + + int element_base = 0; + for (int g = 0; g < grid_idx; g++) + { + element_base += grid_info[g * 6 + 0]; + } + int t_element_base = element_base + t_idx * llm_h * llm_w; + + int window_base = 0; + for (int g = 0; g < grid_idx; g++) + { + window_base += grid_info[g * 6 + 1]; + } + int t_window_base = window_base + t_idx * num_windows_h * num_windows_w; + + int thread_id = threadIdx.x; + int warp_id = thread_id / 32; + int lane_id = thread_id % 32; + + int windows_per_t = num_windows_h * num_windows_w; + int warps_per_block = blockDim.x / 32; + + for (int window_idx = warp_id; window_idx < windows_per_t; window_idx += warps_per_block) + { + int win_h = window_idx / num_windows_w; + int win_w = window_idx % num_windows_w; + + int global_window_idx = t_window_base + window_idx; + int output_offset = cu_window_seqlens[global_window_idx] / spatial_merge_unit; + + int start_h = win_h * vit_merger_window_size; + int start_w = win_w * vit_merger_window_size; + + int valid_h = min(vit_merger_window_size, llm_h - start_h); + int valid_w = min(vit_merger_window_size, llm_w - start_w); + + for (int elem_idx = lane_id; elem_idx < valid_h * valid_w; elem_idx += 32) + { + int local_h = elem_idx / valid_w; + int local_w = elem_idx % valid_w; + + int abs_h = start_h + local_h; + int abs_w = start_w + local_w; + + int value = t_element_base + abs_h * llm_w + abs_w; + + int base_offset = output_offset + elem_idx; + window_indices[base_offset] = value; + } + } +} + +std::tuple get_window_index_cuda( + torch::Tensor grid_thw, + int spatial_merge_size, + int vit_merger_window_size, + int patch_size, + int spatial_merge_unit) +{ + TORCH_CHECK(grid_thw.is_cuda(), "grid_thw must be a CUDA tensor"); + TORCH_CHECK(grid_thw.dim() == 2 && grid_thw.size(1) == 3); + TORCH_CHECK(grid_thw.dtype() == torch::kInt32); + + int num_grids = grid_thw.size(0); + if (num_grids == 0) + { + return std::make_tuple( + torch::empty({0}, grid_thw.options()), + torch::zeros({1}, grid_thw.options())); + } + + const int *d_grid_thw = grid_thw.data_ptr(); + auto options = grid_thw.options(); + + auto grid_thw_cpu = grid_thw.cpu(); + int max_grid_t = 0; + for (int i = 0; i < num_grids; i++) + { + max_grid_t = std::max(max_grid_t, grid_thw_cpu[i][0].item()); + } + + auto grid_info_tensor = torch::empty({num_grids, 6}, options); + auto global_totals_tensor = torch::zeros({2}, options); + + int *d_grid_info = grid_info_tensor.data_ptr(); + int *d_global_totals = global_totals_tensor.data_ptr(); + + int threads1 = 256; + int blocks1 = (num_grids + threads1 - 1) / threads1; + compute_metadata<<>>( + d_grid_thw, d_grid_info, d_global_totals, + num_grids, spatial_merge_size, vit_merger_window_size); + + auto totals_cpu = global_totals_tensor.cpu(); + int total_elements = totals_cpu[0].item(); + int total_windows = totals_cpu[1].item(); + + if (total_elements == 0 || total_windows == 0) + { + return std::make_tuple( + torch::empty({0}, options), + torch::zeros({1}, options)); + } + + torch::Tensor window_indices = torch::empty({total_elements}, options); + torch::Tensor cu_window_seqlens = torch::empty({total_windows + 1}, options); + + int *d_window_indices = window_indices.data_ptr(); + int *d_cu_window_seqlens = cu_window_seqlens.data_ptr(); + + auto window_counts_tensor = torch::empty({total_windows}, options); + int *d_window_counts = window_counts_tensor.data_ptr(); + + dim3 blocks2(max_grid_t, num_grids); + dim3 threads2(256); + + compute_window_counts<<>>( + d_grid_thw, d_grid_info, d_window_counts, + vit_merger_window_size, spatial_merge_unit, num_grids); + + int threads4 = 256; + int blocks4 = (total_windows + threads4 - 1) / threads4; + compute_cu_window_seqlens<<>>( + d_window_counts, d_cu_window_seqlens, total_windows, spatial_merge_unit); + + generate_window_indices<<>>( + d_grid_thw, d_grid_info, d_cu_window_seqlens, d_window_indices, + vit_merger_window_size, spatial_merge_unit, num_grids); + + return std::make_tuple(window_indices, cu_window_seqlens); +} diff --git a/csrc/window_index.h b/csrc/window_index.h new file mode 100644 index 0000000..ea049bf --- /dev/null +++ b/csrc/window_index.h @@ -0,0 +1,9 @@ +#include +#include + +std::tuple get_window_index_cuda( + torch::Tensor grid_thw, + int spatial_merge_size, + int vit_merger_window_size, + int patch_size, + int spatial_merge_unit); diff --git a/scripts/vqa_inference.py b/scripts/vqa_inference.py index c3d2211..b8b27a3 100644 --- a/scripts/vqa_inference.py +++ b/scripts/vqa_inference.py @@ -68,13 +68,15 @@ if __name__ == "__main__": wrapper = VQAWrapper(model_path=MODEL_PATH_FOR_MODULE_TEST) try: - test_image_url = "https://www.ilankelman.org/stopsigns/australia.jpg" - test_question = "What is written on the sign?" + test_question = "To move the red block in the plate with same color, what should you do next? Think step by step." - # img = Image.open("/path/to/your/local/image.jpg").convert("RGB") - import requests + # Local Image + img = Image.open("/path/to/wall-x/assets/cot_example_frame.png").convert("RGB") + # Internet Image + # import requests + # test_image_url = "https://www.ilankelman.org/stopsigns/australia.jpg" + # img = Image.open(requests.get(test_image_url, stream=True).raw).convert("RGB") - img = Image.open(requests.get(test_image_url, stream=True).raw).convert("RGB") answer = wrapper.generate(img, test_question) print("model answer:", answer) diff --git a/setup.py b/setup.py index e13c518..52fe219 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,9 @@ ext_modules = [ "csrc/dual_asym_grouped_gemm.cu", "csrc/permute.cu", "csrc/rope.cu", + "csrc/rope_index.cu", + "csrc/rot_pos.cu", + "csrc/window_index.cu", ], include_dirs=[f"{cwd}/3rdparty/cutlass/include/", f"{cwd}/csrc"], extra_compile_args={ @@ -47,7 +50,7 @@ ext_modules = [ setup( name="wall_x", - version="1.0.0", + version="1.0.1", author="X2Robot Team", classifiers=[ "Programming Language :: Python :: 3", diff --git a/train_qact.py b/train_qact.py index 2906c90..1375f79 100755 --- a/train_qact.py +++ b/train_qact.py @@ -3,6 +3,7 @@ import json import time import yaml import wandb +import accelerate from argparse import ArgumentParser from accelerate import ( Accelerator, @@ -38,9 +39,32 @@ def setup_accelerator(config): ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True) accelerator_dataloader_config = DataLoaderConfiguration(dispatch_batches=False) + if config.get("FSDP2", False): + # Use Fully Sharded Data Parallel (FSDP) version 2 + fsdp_plugin = accelerate.utils.dataclasses.FullyShardedDataParallelPlugin( + fsdp_version=2, reshard_after_forward=True + ) + print("[INFO] Using FSDP version 2 for distributed training") + else: + fsdp_plugin = None + + if config.get("torch_compile", False): + # Use Torch Dynamo for compilation + dynamo_plugin = accelerate.utils.TorchDynamoPlugin( + backend="inductor", + mode="default", + fullgraph=False, + dynamic=False, + ) + print("[INFO] Using Torch Dynamo for compilation") + else: + dynamo_plugin = None + accelerator = Accelerator( kwargs_handlers=[ddp_kwargs], mixed_precision="bf16", + fsdp_plugin=fsdp_plugin, + dynamo_plugin=dynamo_plugin, dataloader_config=accelerator_dataloader_config, gradient_accumulation_steps=config.get("gradient_accumulation_steps", 1), ) diff --git a/wall_x/data/load_lerobot_dataset.py b/wall_x/data/load_lerobot_dataset.py index 99ce239..56ad613 100644 --- a/wall_x/data/load_lerobot_dataset.py +++ b/wall_x/data/load_lerobot_dataset.py @@ -424,8 +424,9 @@ def load_lerobot_data( # repo_id = "lerobot/aloha_mobile_cabinet" repo_id = lerobot_config.get("repo_id", "lerobot/aloha_mobile_cabinet") + root = lerobot_config.get("root", None) dataset = LeRobotDataset( - repo_id, delta_timestamps=delta_timestamps, video_backend="pyav" + repo_id, root=root, delta_timestamps=delta_timestamps, video_backend="pyav" ) if rank == 0: diff --git a/wall_x/fusions/backend.py b/wall_x/fusions/backend.py index 8bcd554..3f9d2e2 100644 --- a/wall_x/fusions/backend.py +++ b/wall_x/fusions/backend.py @@ -297,3 +297,137 @@ def rope_bwd( return backend.rope_bwd( grad_q_out, grad_k_out, q, k, cos, sin, grad_q, grad_k, mrope_section_doubled ) + + +def get_rope_index( + input_ids: torch.Tensor, + image_grid_thw: Optional[torch.Tensor], + video_grid_thw: Optional[torch.Tensor], + second_per_grid_ts: Optional[torch.Tensor], + attention_mask: Optional[torch.Tensor], + spatial_merge_size: int, + image_token_id: int, + video_token_id: int, + vision_start_token_id: int, + tokens_per_second: float, +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Generate position indices for multimodal RoPE (Rotary Position Embedding). + + This function computes 3D position indices for text, image, and video tokens + to enable proper spatial-temporal position encoding in multimodal transformers. + + Args: + input_ids (torch.Tensor): Input token IDs of shape [batch_size, seq_len] + image_grid_thw (torch.Tensor, optional): Image grid specifications of shape [num_images, 3] (T, H, W) + video_grid_thw (torch.Tensor, optional): Video grid specifications of shape [num_videos, 3] (T, H, W) + second_per_grid_ts (torch.Tensor, optional): Temporal scaling per video grid of shape [num_videos] + attention_mask (torch.Tensor, optional): Attention mask of shape [batch_size, seq_len] + spatial_merge_size (int): Spatial dimension merge factor for patch grouping + image_token_id (int): Token ID representing image patches + video_token_id (int): Token ID representing video frames + vision_start_token_id (int): Token ID marking vision sequence start + tokens_per_second (float): Temporal scaling factor for video sequences + + Returns: + Tuple[torch.Tensor, torch.Tensor]: A tuple containing: + - position_ids: 3D position indices of shape [3, batch_size, seq_len] + - mrope_deltas: Position deltas for multimodal RoPE of shape [batch_size, 1] + + Note: + When both image_grid_thw and video_grid_thw are None, returns standard + text-only position indices based on attention_mask or sequence order. + """ + return backend.rope_index( + input_ids, + image_grid_thw, + video_grid_thw, + second_per_grid_ts, + attention_mask, + spatial_merge_size, + image_token_id, + video_token_id, + vision_start_token_id, + tokens_per_second, + ) + + +def rot_pos_emb( + inv_freq: torch.Tensor, + grid_thw: torch.Tensor, + spatial_merge_size: int, +) -> torch.Tensor: + """ + Compute fused rotary position embeddings for multimodal grids. + + This function efficiently computes rotary position embeddings for spatial-temporal + grids using a fused CUDA kernel, supporting both int32 and int64 grid specifications. + + Args: + inv_freq (torch.Tensor): Inverse frequencies for RoPE of shape [dim/2] + Must be float32 dtype on CUDA device + grid_thw (torch.Tensor): Grid specifications of shape [num_grids, 3] (T, H, W) + Supports int32 or int64 dtype on CUDA device + spatial_merge_size (int): Merge factor for spatial dimensions (must be positive) + + Returns: + torch.Tensor: Computed rotary embeddings of shape [total_tokens, dim] + where total_tokens is determined by grid layouts and spatial_merge_size + + Example: + >>> inv_freq = torch.randn(64, device='cuda', dtype=torch.float32) # 128-dim model + >>> grids = torch.tensor([[8, 14, 14], [16, 7, 7]], device='cuda', dtype=torch.int32) + >>> embeddings = rot_pos_emb(inv_freq, grids, spatial_merge_size=2) + >>> print(embeddings.shape) # [computed_tokens, 128] + + Note: + The function automatically dispatches to int32 or int64 implementations + based on the dtype of grid_thw. Output is always float32. + """ + return backend.rot_pos_emb(inv_freq, grid_thw, spatial_merge_size) + + +def get_window_index( + grid_thw: torch.Tensor, + spatial_merge_size: int, + vit_merger_window_size: int, + patch_size: int, + spatial_merge_unit: int, +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Generate window attention indices for Vision Transformer architectures. + + Computes window-based attention indices for hierarchical processing of vision + tokens, enabling efficient sliding window attention patterns in ViT models. + + Args: + grid_thw (torch.Tensor): Grid specifications of shape [num_grids, 3] (T, H, W) + Must be int32 dtype on CUDA device + spatial_merge_size (int): Spatial dimension merge factor + vit_merger_window_size (int): Size of attention windows for ViT processing + patch_size (int): Size of vision patches in pixels + spatial_merge_unit (int): Unit size for spatial merging operations + + Returns: + Tuple[torch.Tensor, torch.Tensor]: A tuple containing: + - window_indices: Flattened window indices of shape [total_elements] + - cu_window_seqlens: Cumulative window sequence lengths of shape [num_windows + 1] + + Example: + >>> grids = torch.tensor([[1, 14, 14]], device='cuda', dtype=torch.int32) + >>> indices, seqlens = get_window_index( + ... grids, spatial_merge_size=2, vit_merger_window_size=7, + ... patch_size=16, spatial_merge_unit=4 + ... ) + + Note: + Returns empty tensors if input grid is empty or no valid windows can be formed. + The cu_window_seqlens tensor enables efficient batched attention computation. + """ + return backend.get_window_index( + grid_thw, + spatial_merge_size, + vit_merger_window_size, + patch_size, + spatial_merge_unit, + ) diff --git a/wall_x/fusions/ops.py b/wall_x/fusions/ops.py index 3beef5f..3f993b8 100644 --- a/wall_x/fusions/ops.py +++ b/wall_x/fusions/ops.py @@ -495,3 +495,248 @@ class MultimodalRoPE(torch.autograd.Function): def multimodal_rope(q, k, cos, sin, mrope_section): return MultimodalRoPE.apply(q, k, cos, sin, mrope_section) + + +################################################################################################ +## +## RoPE Index 3D +## +################################################################################################ + + +def get_rope_index( + input_ids: torch.Tensor, + spatial_merge_size: int, + image_token_id: int, + video_token_id: int, + vision_start_token_id: int, + tokens_per_second: float, + image_grid_thw: torch.Tensor = None, + video_grid_thw: torch.Tensor = None, + second_per_grid_ts: torch.Tensor = None, + attention_mask: torch.Tensor = None, +): + """ + Generate 3D RoPE position indices for multimodal transformer inputs. + + Computes position indices for text, image, and video tokens to enable proper + spatial-temporal position encoding in multimodal transformers with RoPE. + + Args: + input_ids (torch.Tensor): Input token sequence of shape [batch_size, seq_len] + Must be LongTensor on CUDA device + spatial_merge_size (int): Spatial merge size for patch grouping (must be positive) + image_token_id (int): Token ID representing image patches + video_token_id (int): Token ID representing video frames + vision_start_token_id (int): Token ID marking start of vision sequences + tokens_per_second (float): Temporal scaling factor for video sequences (must be positive) + image_grid_thw (torch.Tensor, optional): Image grid dimensions of shape [num_images, 3] (T, H, W) + video_grid_thw (torch.Tensor, optional): Video grid dimensions of shape [num_videos, 3] (T, H, W) + second_per_grid_ts (torch.Tensor, optional): Video time intervals of shape [num_videos] + attention_mask (torch.Tensor, optional): Attention mask of shape [batch_size, seq_len] + + Returns: + Tuple[torch.Tensor, torch.Tensor]: A tuple containing: + - position_ids: 3D position indices of shape [3, batch_size, seq_len] + - mrope_position_deltas: mRoPE position deltas of shape [batch_size, 1] + + Raises: + TypeError: If input_ids is not a torch.Tensor + ValueError: If input dimensions are incorrect or tensors not on CUDA + """ + # Input validation + if not isinstance(input_ids, torch.Tensor): + raise TypeError("input_ids must be a torch.Tensor") + + if input_ids.dim() != 2: + raise ValueError("input_ids must be 2D tensor (batch_size, seq_len)") + + if not input_ids.is_cuda: + raise ValueError("input_ids must be on CUDA device") + + # Parameter validation + if not isinstance(spatial_merge_size, int) or spatial_merge_size <= 0: + raise ValueError( + f"spatial_merge_size must be positive integer, got {spatial_merge_size}" + ) + + if not isinstance(tokens_per_second, (int, float)) or tokens_per_second <= 0: + raise ValueError( + f"tokens_per_second must be positive number, got {tokens_per_second}" + ) + + return backend.get_rope_index( + input_ids, + image_grid_thw, + video_grid_thw, + second_per_grid_ts, + attention_mask, + spatial_merge_size, + image_token_id, + video_token_id, + vision_start_token_id, + float(tokens_per_second), + ) + + +################################################################################################ +## +## Fused Rotary Position Embedding +## +################################################################################################ + + +def rot_pos_emb( + inv_freq: torch.Tensor, + grid_thw: torch.Tensor, + spatial_merge_size: int, +) -> torch.Tensor: + """ + Compute fused rotary position embeddings using optimized CUDA kernel. + + This function fuses all rotary position embedding computations into a single + CUDA kernel for improved performance with spatial-temporal grids. + + Args: + inv_freq (torch.Tensor): Inverse frequencies tensor of shape [dim/2] + Contains precomputed 1.0 / (theta ** (torch.arange(0, dim, 2) / dim)) + Must be float32 on CUDA device + grid_thw (torch.Tensor): Grid dimensions tensor of shape [num_grids, 3] + Each row contains (T, H, W) for temporal, height, width dimensions + Supports int32 or int64 on CUDA device + spatial_merge_size (int): Spatial merge size for token grouping (must be positive) + + Returns: + torch.Tensor: Rotary position embeddings of shape [total_tokens, dim] + where dim = 2 * len(inv_freq) + First half contains h_pos frequencies, second half contains w_pos frequencies + + Raises: + TypeError: If inputs are not torch.Tensor or spatial_merge_size not int + ValueError: If tensor dimensions incorrect, not on CUDA, or devices mismatch + RuntimeError: If CUDA kernel execution fails + """ + # Type checking + if not isinstance(inv_freq, torch.Tensor): + raise TypeError(f"inv_freq must be a torch.Tensor, got {type(inv_freq)}") + + if not isinstance(grid_thw, torch.Tensor): + raise TypeError(f"grid_thw must be a torch.Tensor, got {type(grid_thw)}") + + # Dimension checking + if inv_freq.dim() != 1: + raise ValueError( + f"inv_freq must be 1-dimensional, got {inv_freq.dim()}D tensor" + ) + + if grid_thw.dim() != 2: + raise ValueError( + f"grid_thw must be 2-dimensional, got {grid_thw.dim()}D tensor" + ) + + if grid_thw.size(1) != 3: + raise ValueError( + f"grid_thw must have shape [num_grids, 3], got shape {list(grid_thw.shape)}" + ) + + # Device checking + if not inv_freq.is_cuda: + raise ValueError("inv_freq must be on CUDA device") + + if not grid_thw.is_cuda: + raise ValueError("grid_thw must be on CUDA device") + + # Ensure both tensors are on the same device + if inv_freq.device != grid_thw.device: + raise ValueError( + f"inv_freq and grid_thw must be on the same device, " + f"got {inv_freq.device} and {grid_thw.device}" + ) + + # Parameter validation + if not isinstance(spatial_merge_size, int): + raise TypeError( + f"spatial_merge_size must be an integer, got {type(spatial_merge_size)}" + ) + + if spatial_merge_size <= 0: + raise ValueError( + f"spatial_merge_size must be positive, got {spatial_merge_size}" + ) + + # Ensure inv_freq is float32 (the kernel expects float) + if inv_freq.dtype != torch.float32: + inv_freq = inv_freq.to(torch.float32) + + # Call the CUDA backend + try: + return backend.rot_pos_emb(inv_freq, grid_thw, spatial_merge_size) + except RuntimeError as e: + raise RuntimeError(f"CUDA kernel execution failed: {str(e)}") + + +################################################################################################ +## +## Fused Window Index Generation +## +################################################################################################ + + +def get_window_index( + grid_thw: torch.Tensor, + window_size: int, + spatial_merge_size: int, + patch_size: int, + spatial_merge_unit: int = 1, +): + """ + Generate window attention indices for Vision Transformer architectures. + + Computes window-based attention indices for hierarchical processing of vision + tokens, enabling efficient sliding window attention patterns in ViT models. + + Args: + grid_thw (torch.Tensor): Grid specifications of shape [num_grids, 3] (T, H, W) + Must be or will be converted to int32 on CUDA device + window_size (int): Window size for attention computation + spatial_merge_size (int): Spatial merge size for patch grouping + patch_size (int): Size of vision patches in pixels + spatial_merge_unit (int, optional): Spatial merging unit size. Defaults to 1. + + Returns: + Tuple[torch.Tensor, torch.Tensor]: A tuple containing: + - window_index: Window indices tensor of shape [total_elements] + - cu_window_seqlens: Cumulative window sequence lengths of shape [num_windows + 1] + + Raises: + AssertionError: If grid_thw dimensions are incorrect + + Note: + Returns empty tensors if input grid is empty or no valid windows can be formed. + The function automatically converts input to CUDA int32 if needed. + """ + # Input validation + assert ( + grid_thw.dim() == 2 and grid_thw.size(1) == 3 + ), f"grid_thw must have shape (num_grids, 3), got {grid_thw.shape}" + + # Ensure input is on CUDA and int32 type + if not grid_thw.is_cuda: + grid_thw = grid_thw.cuda() + + if grid_thw.dtype != torch.int32: + grid_thw = grid_thw.to(torch.int32) + + # Calculate vit_merger_window_size + vit_merger_window_size = window_size // spatial_merge_size // patch_size + + # Call CUDA backend + window_index, cu_window_seqlens = backend.get_window_index( + grid_thw, + spatial_merge_size, + vit_merger_window_size, + patch_size, + spatial_merge_unit, + ) + + return window_index, cu_window_seqlens diff --git a/wall_x/model/qwen2_5_based/modeling_qwen2_5_vl.py b/wall_x/model/qwen2_5_based/modeling_qwen2_5_vl.py index 92f8f95..d09ad1b 100644 --- a/wall_x/model/qwen2_5_based/modeling_qwen2_5_vl.py +++ b/wall_x/model/qwen2_5_based/modeling_qwen2_5_vl.py @@ -560,15 +560,17 @@ class Qwen2_5_VisionTransformerPretrainedModel(Qwen2_5_VLPreTrainedModel): `torch.Tensor`: hidden_states. """ hidden_states = self.patch_embed(hidden_states) - rotary_pos_emb = self.rot_pos_emb(grid_thw) - window_index, cu_window_seqlens = self.get_window_index(grid_thw) - window_index = window_index.to(hidden_states.device) - cu_window_seqlens = torch.tensor( - cu_window_seqlens, - device=hidden_states.device, - dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32, + rotary_pos_emb = ops.rot_pos_emb( + self.rotary_pos_emb.inv_freq, grid_thw, self.spatial_merge_size + ) + + window_index, cu_window_seqlens = ops.get_window_index( + grid_thw=grid_thw, + window_size=self.window_size, + spatial_merge_size=self.spatial_merge_size, + patch_size=self.patch_size, + spatial_merge_unit=self.spatial_merge_unit, ) - cu_window_seqlens = torch.unique_consecutive(cu_window_seqlens) seq_len, _ = hidden_states.size() hidden_states = hidden_states.reshape( diff --git a/wall_x/model/qwen2_5_based/modeling_qwen2_5_vl_act.py b/wall_x/model/qwen2_5_based/modeling_qwen2_5_vl_act.py index e5a4f85..8ba3626 100644 --- a/wall_x/model/qwen2_5_based/modeling_qwen2_5_vl_act.py +++ b/wall_x/model/qwen2_5_based/modeling_qwen2_5_vl_act.py @@ -1244,12 +1244,17 @@ class Qwen2_5_VLMoEForAction(Qwen2_5_VLForConditionalGeneration): or self.rope_deltas is None or (past_key_values is None or past_key_values.get_seq_length() == 0) ): - position_ids, rope_deltas = self.get_rope_index( - input_ids, - image_grid_thw, - video_grid_thw, - second_per_grid_ts, - attention_mask, + position_ids, rope_deltas = ops.get_rope_index( + input_ids=input_ids, + image_grid_thw=image_grid_thw, + video_grid_thw=video_grid_thw, + second_per_grid_ts=second_per_grid_ts, + attention_mask=attention_mask, + spatial_merge_size=self.config.vision_config.spatial_merge_size, + image_token_id=self.config.image_token_id, + video_token_id=self.config.video_token_id, + vision_start_token_id=self.config.vision_start_token_id, + tokens_per_second=self.config.vision_config.tokens_per_second, ) self.rope_deltas = rope_deltas # Use previously calculated rope deltas to get correct position IDs @@ -1720,12 +1725,17 @@ class Qwen2_5_VLMoEForAction(Qwen2_5_VLForConditionalGeneration): or self.rope_deltas is None or (past_key_values is None or past_key_values.get_seq_length() == 0) ): - position_ids, rope_deltas = self.get_rope_index( - input_ids, - image_grid_thw, - video_grid_thw, - second_per_grid_ts, - attention_mask, + position_ids, rope_deltas = ops.get_rope_index( + input_ids=input_ids, + image_grid_thw=image_grid_thw, + video_grid_thw=video_grid_thw, + second_per_grid_ts=second_per_grid_ts, + attention_mask=attention_mask, + spatial_merge_size=self.config.vision_config.spatial_merge_size, + image_token_id=self.config.image_token_id, + video_token_id=self.config.video_token_id, + vision_start_token_id=self.config.vision_start_token_id, + tokens_per_second=self.config.vision_config.tokens_per_second, ) self.rope_deltas = rope_deltas # Use previously calculated rope deltas to get correct position IDs @@ -1882,6 +1892,17 @@ class Qwen2_5_VLMoEForAction(Qwen2_5_VLForConditionalGeneration): ) dof_mask = dof_mask.to(inputs_embeds.device).to(inputs_embeds.dtype) + # Calculate token distribution across MoE expert groups + group_size = torch.zeros( + self.config.num_experts, dtype=torch.long, device="cpu" + ) + for i in range(self.config.num_experts): + group_size[i] = (moe_token_types == i).sum() + + # Calculate start and end indices for each expert group + start_indices = torch.cumsum(group_size, dim=0) - group_size + end_indices = torch.cumsum(group_size, dim=0) + def step(timestep, noisy_action): """ Single denoising step for diffusion process. @@ -1915,6 +1936,8 @@ class Qwen2_5_VLMoEForAction(Qwen2_5_VLForConditionalGeneration): past_key_values=past_key_values, inputs_embeds=temp_inputs_embeds, moe_token_types=moe_token_types, + start_indices=start_indices, + end_indices=end_indices, use_cache=True, output_attentions=False, output_hidden_states=False, diff --git a/wall_x/trainer/qwen_vl_act_trainer.py b/wall_x/trainer/qwen_vl_act_trainer.py index 736b936..92e4a2a 100644 --- a/wall_x/trainer/qwen_vl_act_trainer.py +++ b/wall_x/trainer/qwen_vl_act_trainer.py @@ -5,11 +5,13 @@ import torch import random import numpy as np import torch.nn as nn +import torch.distributed as dist from tqdm import tqdm from functools import wraps from datetime import datetime from torch.optim import AdamW +from torch.distributed.tensor import distribute_tensor from accelerate import Accelerator from safetensors.torch import load_file from transformers.optimization import get_cosine_with_min_lr_schedule_with_warmup @@ -769,7 +771,9 @@ class QwenVlAct_Trainer: """ checkpoint_path = self.config["resume"]["ckpt"] - if self.config.get("resume", {}).get("load_ckpt_only", False): + if self.config.get("FSDP2", False): + self._load_fsdp_state_dict_with_distribute_tensor() + elif self.config.get("resume", {}).get("load_ckpt_only", False): # Load only model weights ckpt_path = self.config["resume"]["ckpt"] + "/model.safetensors" state_dict = load_file(ckpt_path, device="cpu") @@ -788,6 +792,48 @@ class QwenVlAct_Trainer: self.print_rank0(f"Resumed from checkpoint: {checkpoint_path}") + def _load_fsdp_state_dict_with_distribute_tensor(self): + + rank = dist.get_rank() if dist.is_initialized() else 0 + + full_sd = load_file( + self.config["resume"]["ckpt"] + "/model.safetensors", device="cpu" + ) + meta_sharded_sd = self.model.state_dict() + sharded_sd = {} + + def find_matching_key(target_key, available_keys): + if target_key in available_keys: + return target_key + prefixed_key = f"_orig_mod.{target_key}" + if prefixed_key in available_keys: + return prefixed_key + if target_key.startswith("_orig_mod."): + unprefixed_key = target_key[len("_orig_mod.") :] + if unprefixed_key in available_keys: + return unprefixed_key + return None + + for param_name, full_tensor in full_sd.items(): + matching_key = find_matching_key(param_name, meta_sharded_sd.keys()) + if matching_key is None: + if rank == 0: + print( + f"[Rank {rank}] Warning: Parameter not found:", + param_name, + flush=True, + ) + continue + sharded_meta_param = meta_sharded_sd[matching_key] + sharded_tensor = distribute_tensor( + full_tensor, + sharded_meta_param.device_mesh, + sharded_meta_param.placements, + ) + sharded_sd[matching_key] = nn.Parameter(sharded_tensor) + + self.model.load_state_dict(sharded_sd, assign=True, strict=False) + def log_l1_details(self, all_label, all_pred, all_task, all_dof_mask): """ Log detailed L1 loss metrics by degrees of freedom. diff --git a/workspace/README.md b/workspace/README.md index b9dbc14..a1acfe5 100644 --- a/workspace/README.md +++ b/workspace/README.md @@ -1,15 +1,28 @@ -# Training Configuration Guide +# Training Guide -This document explains the key configuration parameters that can be modified for Wall-X training. +This document explains the key configuration parameters and memory requirements for Wall-X training. ## Quick Start Checklist -1. **Update run.sh**: Set `code_dir` and `config_path` to your actual paths -2. **Configure GPUs**: Set `CUDA_VISIBLE_DEVICES` for your available GPUs -3. **Update config paths**: Replace all `/path/to/` placeholders in `config_qact.yml` with actual paths -4. **Configure robot**: Set `dof_config` and `agent_pos_config` for your robot -5. **Set dataset**: Choose appropriate `repo_id` for your dataset -6. **Adjust batch size**: Set `batch_size_per_gpu` based on GPU memory -7. **Run training**: Execute `bash ./workspace/lerobot_example/run.sh` + +### 🚀 **Step 1: Download Pre-trained Model** +Choose one of the available models: +- **WALL-OSS-FLOW**: https://huggingface.co/x-square-robot/wall-oss-flow +- **WALL-OSS-FAST**: https://huggingface.co/x-square-robot/wall-oss-fast + +### ⚙️ **Step 2: Configure Environment** +- Update `run.sh`: Set `code_dir` and `config_path` to your actual paths +- Set `CUDA_VISIBLE_DEVICES` for your available GPUs + +### 📝 **Step 3: Update Configuration Files** +- Replace all `/path/to/` placeholders in `config_qact.yml` with actual paths +- Configure robot settings: `dof_config` and `agent_pos_config` +- Set dataset: Choose appropriate `repo_id` +- Adjust `batch_size_per_gpu` based on your GPU memory + +### ▶️ **Step 4: Start Training** +```bash +bash ./workspace/lerobot_example/run.sh +``` ## Enable FAST tokenizer To fine-tune using the FAST tokenizer, please download the repository and update the `action_tokenizer_path`. Make sure to set `use_fast_tokenizer` to `true`: @@ -38,6 +51,15 @@ action_tokenizer_path: "/path/to/fast/" # Must set if use_fast_token - `num_training_steps`: Total training steps - `num_epoch`: Number of training epochs +### Training Optimization Settings +- `FSDP2`: Enable FSDP2 for distributed training (default: True) - **Recommended for multi-GPU** +- `torch_compile`: Enable PyTorch compilation optimization (default: False) + +**⚠️ Important Note on torch_compile:** +- **Benefits**: Enabling `torch_compile` can significantly improve training efficiency +- **Requirements**: Requires that the data input shape is always consistent throughout training +- **Caution**: If you don't have sufficient understanding of torch compile, please **DO NOT** enable it as it may cause unexpected issues with dynamic input shapes + ## Robot Configuration (Modify for Your Robot) ### DOF Configuration @@ -67,6 +89,20 @@ Keep `agent_pos_config` consistent with `dof_config`. - `resume.ckpt`: Path to checkpoint for resuming training - `resume.load_ckpt_only`: Only load model weights, not optimizer state -## Performance Settings (Optional) -- `profile`: Enable PyTorch profiling (true/false) -- `padding_side`: Token padding side (left/right) +## Memory Usage + +Below are the memory consumption benchmarks for different training configurations using the `lerobot/aloha_mobile_cabinet` dataset: + +| Dataset | Batch Size | FSDP2 | Torch Compile | Num GPUs | Max Allocated Memory | +|---------|------------|--------|---------------|----------|---------------------| +| lerobot/aloha_mobile_cabinet | 1 | ❌ | ❌ | 1 | 40.11G | +| lerobot/aloha_mobile_cabinet | 1 | ❌ | ❌ | 8 | 48.02G | +| lerobot/aloha_mobile_cabinet | 1 | âś… | ❌ | 2 | 43.70G | +| lerobot/aloha_mobile_cabinet | 1 | âś… | ❌ | 8 | 24.96G | +| lerobot/aloha_mobile_cabinet | 1 | âś… | âś… | 8 | 24.21G | + + +**Hardware Recommendations:** + +- For single GPU training: Ensure at least 48GB VRAM (e.g., RTX 6000 Ada, A6000) +- For multi-GPU training: Enable FSDP2 for optimal memory distribution diff --git a/workspace/lerobot_example/config_qact.yml b/workspace/lerobot_example/config_qact.yml index 7f72500..6f167ba 100644 --- a/workspace/lerobot_example/config_qact.yml +++ b/workspace/lerobot_example/config_qact.yml @@ -28,6 +28,10 @@ batch_size_per_gpu: 8 padding_side: left epoch_save_interval: 10 +# Training optimization settings +FSDP2: True +torch_compile: False + # Robot configuration - Define degrees of freedom for each component dof_config: follow_left_ee_cartesian_pos: 3 # Left end-effector Cartesian position diff --git a/workspace/lerobot_example/run.sh b/workspace/lerobot_example/run.sh index 380b65c..7d18106 100644 --- a/workspace/lerobot_example/run.sh +++ b/workspace/lerobot_example/run.sh @@ -1,5 +1,4 @@ #!/bin/bash -# export CUDA_VISIBLE_DEVICES=4,5,6,7 export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 NUM_GPUS=$(echo $CUDA_VISIBLE_DEVICES | tr ',' '\n' | wc -l)