feat(sana): in-tree SANA-Streaming V2V DiT (GDN linear-attention) + C-API - #21
feat(sana): in-tree SANA-Streaming V2V DiT (GDN linear-attention) + C-API#21jcelerier wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces an in-tree C++/CUDA implementation of the SANA-Streaming DiT architecture, including the Gated-Delta-Net (GDN) linear-attention recurrence, softmax/cross-attention, and GLU-MBConv FFN, along with a C-API wrapper. The review feedback highlights several critical performance and safety improvements. First, performance can be significantly optimized by replacing expensive synchronous device-to-host round-trips with custom GPU kernels for splitting QKV and CKV tensors, and by utilizing shared memory in the gdn_bidi kernel to avoid redundant global memory accesses. Second, resource management should be improved by adding a reference counter to properly manage and destroy the static cublasHandle_t. Finally, exception safety issues should be addressed by implementing a destructor for the Arena struct to prevent GPU memory leaks, and by reordering allocations in librediffusion_sana_create to avoid leaking the wrapper handle if the SanaDiT constructor throws an exception.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| #define SCKV(x) do{ cudaError_t e=(x); if(e){ printf("[sana] CUDA err %s @ %d\n", cudaGetErrorString(e), __LINE__);} }while(0) | ||
| #define SCB(x) do{ cublasStatus_t s=(x); if(s){ printf("[sana] cuBLAS err %d @ %d\n",(int)s,__LINE__); return -1;} }while(0) | ||
|
|
||
| static cublasHandle_t HBL = nullptr; |
| } | ||
| // ================= GDN kernels ================= | ||
| struct Bufs{ const float *q,*k,*v,*qrot,*krot,*beta,*decay,*init_kv,*init_z; float *num,*den,*out_kv,*out_z,*state_kv,*state_z,*dv,*dz; int H,D,T,S,N; float eps; }; | ||
| __global__ void gdn_bidi(Bufs b){ |
There was a problem hiding this comment.
The PR description states that the GDN linear-attention recurrence kernel uses a 'shared-mem state' to achieve an ~8x speedup. However, the gdn_bidi kernel currently accesses the recurrent states skv and sz directly from global memory (b.state_kv and b.state_z) on every step of the loop. This results in highly redundant global memory reads and writes across all threads. Consider loading the recurrent states into shared memory at the start of the kernel, performing the loop updates in shared memory, and writing the final states back to global memory at the end.
|
|
||
| static const int Tt=4, Hs=15, Ws=26, S=Hs*Ws, Nt=Tt*S, Lk=300, CH=13440, HALF=6720; | ||
|
|
||
| struct Arena{ std::vector<float*> v; float* a(size_t n){ float* d=dev(n); if(d)v.push_back(d); return d; } void free(){ for(auto p:v)cudaFree(p); v.clear(); } }; |
There was a problem hiding this comment.
The Arena struct manually manages GPU memory allocations, but does not implement a destructor to automatically free tracked pointers. If an exception is thrown or an early return occurs, any allocated pointers in v will be leaked. Implementing a destructor to automatically call free() ensures exception safety and prevents GPU memory leaks.
struct Arena{ std::vector<float*> v; ~Arena() { free(); } float* a(size_t n){ float* d=dev(n); if(d)v.push_back(d); return d; } void free(){ for(auto p:v)cudaFree(p); v.clear(); } };
|
|
||
| static std::vector<float> LB(const std::string& d,const char* nm,size_t n){ std::vector<float> v; rd(d+"/"+nm+".bin",n,v); return v; } | ||
| static float* LDB(const std::string& d,const char* nm,size_t n){ auto h=LB(d,nm,n); return up(h); } | ||
|
|
There was a problem hiding this comment.
Add custom CUDA kernels to perform QKV and CKV splitting entirely on the GPU, avoiding expensive device-to-host round-trips.
static float* LDB(const std::string& d,const char* nm,size_t n){ auto h=LB(d,nm,n); return up(h); }
__global__ void k_split_qkv(float* q, float* k, const float* qkv, int N, int C) {
long i = blockIdx.x * (long)blockDim.x + threadIdx.x;
if (i >= (long)N * C) return;
int c = i % C;
int n = i / C;
q[i] = qkv[(long)n * 3 * C + c];
k[i] = qkv[(long)n * 3 * C + C + c];
}
__global__ void k_split_ckv(float* ck, float* cv, const float* ckv, int Lk, int C) {
long i = blockIdx.x * (long)blockDim.x + threadIdx.x;
if (i >= (long)Lk * C) return;
int c = i % C;
int l = i / C;
ck[i] = ckv[(long)l * 2 * C + c];
cv[i] = ckv[(long)l * 2 * C + C + c];
}
| float* qkv=A.a((size_t)Nt*3*C); linear(qkv,sa_in,qkvw,Nt,C,3*C); | ||
| float* qnw=LDB(sub,"attn_q_norm",C),*knw=LDB(sub,"attn_k_norm",C); A.v.push_back(qnw);A.v.push_back(knw); | ||
| float* q_sec=A.a((size_t)Nt*C),*k_sec=A.a((size_t)Nt*C); | ||
| { std::vector<float> qkvh; down(qkv,(size_t)Nt*3*C,qkvh); std::vector<float> qh((size_t)Nt*C),kh((size_t)Nt*C); for(int n=0;n<Nt;n++)for(int c=0;c<C;c++){qh[(size_t)n*C+c]=qkvh[(size_t)n*3*C+c];kh[(size_t)n*C+c]=qkvh[(size_t)n*3*C+C+c];} cudaMemcpy(q_sec,qh.data(),(size_t)Nt*C*4,cudaMemcpyHostToDevice); cudaMemcpy(k_sec,kh.data(),(size_t)Nt*C*4,cudaMemcpyHostToDevice); } |
There was a problem hiding this comment.
The current implementation performs a synchronous device-to-host copy (down), splits the QKV tensor on the CPU, and then copies the split tensors back to the GPU (cudaMemcpy). This introduces massive PCIe transfer overhead (~42 MB per block, over 600 MB per denoise step), which severely degrades performance. We can completely eliminate this round-trip by performing the split entirely on the GPU using a simple custom CUDA kernel.
k_split_qkv<<<gr((long)Nt*C), 256>>>(q_sec, k_sec, qkv, Nt, C);
| float* cq=A.a((size_t)Nt*C); linear(cq,x1,cqw,Nt,C,C); k_add_bias<<<gr((long)Nt*C),256>>>(cq,cqb,Nt,C); | ||
| float* ckv=A.a((size_t)Lk*2*C); linear(ckv,y,ckvw,Lk,C,2*C); k_add_bias<<<gr((long)Lk*2*C),256>>>(ckv,ckvb,Lk,2*C); | ||
| float* ck=A.a((size_t)Lk*C),*cv=A.a((size_t)Lk*C); | ||
| { std::vector<float> h; down(ckv,(size_t)Lk*2*C,h); std::vector<float> kh((size_t)Lk*C),vh((size_t)Lk*C); for(int l=0;l<Lk;l++)for(int c=0;c<C;c++){kh[(size_t)l*C+c]=h[(size_t)l*2*C+c];vh[(size_t)l*C+c]=h[(size_t)l*2*C+C+c];} cudaMemcpy(ck,kh.data(),(size_t)Lk*C*4,cudaMemcpyHostToDevice); cudaMemcpy(cv,vh.data(),(size_t)Lk*C*4,cudaMemcpyHostToDevice); } |
| float* qkv=A.a((size_t)N*3*C); linear(qkv,sa_in,qkvw,N,C,3*C); | ||
| float* qnw=LW("attn_q_norm",C),*knw=LW("attn_k_norm",C); A.v.push_back(qnw);A.v.push_back(knw); | ||
| float* q_sec=A.a((size_t)N*C),*k_sec=A.a((size_t)N*C); | ||
| { std::vector<float> qkvh; down(qkv,(size_t)N*3*C,qkvh); std::vector<float> qh((size_t)N*C),kh((size_t)N*C); for(int n=0;n<N;n++)for(int c=0;c<C;c++){qh[(size_t)n*C+c]=qkvh[(size_t)n*3*C+c];kh[(size_t)n*C+c]=qkvh[(size_t)n*3*C+C+c];} cudaMemcpy(q_sec,qh.data(),(size_t)N*C*4,cudaMemcpyHostToDevice); cudaMemcpy(k_sec,kh.data(),(size_t)N*C*4,cudaMemcpyHostToDevice); } |
| float* cq=A.a((size_t)N*C); linear(cq,x1,cqw,N,C,C); k_add_bias<<<gr((long)N*C),256>>>(cq,cqb,N,C); | ||
| float* ckv=A.a((size_t)Lk*2*C); linear(ckv,y,ckvw,Lk,C,2*C); k_add_bias<<<gr((long)Lk*2*C),256>>>(ckv,ckvb,Lk,2*C); | ||
| float* ck2=A.a((size_t)Lk*C),*cv2=A.a((size_t)Lk*C); | ||
| { std::vector<float> h; down(ckv,(size_t)Lk*2*C,h); std::vector<float> kh((size_t)Lk*C),vh((size_t)Lk*C); for(int l=0;l<Lk;l++)for(int c=0;c<C;c++){kh[(size_t)l*C+c]=h[(size_t)l*2*C+c];vh[(size_t)l*C+c]=h[(size_t)l*2*C+C+c];} cudaMemcpy(ck2,kh.data(),(size_t)Lk*C*4,cudaMemcpyHostToDevice); cudaMemcpy(cv2,vh.data(),(size_t)Lk*C*4,cudaMemcpyHostToDevice); } |
| SanaDiT::SanaDiT(const std::string& weights_dir){ p_=new Impl{weights_dir}; if(!sana::HBL) cublasCreate(&sana::HBL); } | ||
| SanaDiT::~SanaDiT(){ delete p_; } |
There was a problem hiding this comment.
Properly manage the lifetime of the static cublasHandle_t using the reference counter to prevent resource leaks.
SanaDiT::SanaDiT(const std::string& weights_dir){ p_=new Impl{weights_dir}; if(HBL_ref_count++ == 0) cublasCreate(&sana::HBL); }
SanaDiT::~SanaDiT(){ delete p_; if(--HBL_ref_count == 0 && sana::HBL) { cublasDestroy(sana::HBL); sana::HBL = nullptr; } }
| librediffusion_sana_handle librediffusion_sana_create(const char* weights_dir) | ||
| { | ||
| try | ||
| { | ||
| auto* h = new librediffusion_sana; | ||
| h->dit = std::make_unique<SanaDiT>(weights_dir ? weights_dir : ""); | ||
| return h; | ||
| } | ||
| catch(const std::exception& e) | ||
| { | ||
| fprintf(stderr, "sana_create failed: %s\n", e.what()); | ||
| return nullptr; | ||
| } | ||
| } |
There was a problem hiding this comment.
If the SanaDiT constructor throws an exception (e.g., due to missing weight files or allocation failure), the raw pointer h allocated with new will be leaked because the exception is caught in the outer try-catch block. Constructing the SanaDiT instance first before allocating h ensures exception safety.
| librediffusion_sana_handle librediffusion_sana_create(const char* weights_dir) | |
| { | |
| try | |
| { | |
| auto* h = new librediffusion_sana; | |
| h->dit = std::make_unique<SanaDiT>(weights_dir ? weights_dir : ""); | |
| return h; | |
| } | |
| catch(const std::exception& e) | |
| { | |
| fprintf(stderr, "sana_create failed: %s\n", e.what()); | |
| return nullptr; | |
| } | |
| } | |
| librediffusion_sana_handle librediffusion_sana_create(const char* weights_dir) | |
| { | |
| try | |
| { | |
| auto dit = std::make_unique<SanaDiT>(weights_dir ? weights_dir : ""); | |
| auto* h = new librediffusion_sana; | |
| h->dit = std::move(dit); | |
| return h; | |
| } | |
| catch(const std::exception& e) | |
| { | |
| fprintf(stderr, "sana_create failed: %s\n", e.what()); | |
| return nullptr; | |
| } | |
| } |
…RAII/refcount/ctor-safety Addresses the automated review on PR #21. Numerically identical (rollout selftest still 2.47% vs the Python sampler); no behavior change, only perf + resource safety. - GDN: replace the naive global-memory gdn_bidi (<<<HEADS,256>>>, 20 blocks) with the row-split + dynamic-shared-memory kernel (GDN_G=16 -> 320 grid blocks, state/K/Q tiles in shared mem), matching the "~8x / shared-mem" claim. One-time cudaFuncSetAttribute for the dynamic shared size. - Replace the synchronous device->host->device QKV/CKV splits (4 sites, ~600 MB/step of PCIe traffic) with on-GPU k_split_qkv / k_split_ckv kernels. - Arena: add a destructor (RAII) so GPU allocations are freed on exception / early return. - cuBLAS handle: reference-count create/destroy (cublasDestroy on last ref). - librediffusion_sana_create: construct SanaDiT before allocating the C handle so a throwing ctor cannot leak the handle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EPT5weE9MMS8cyxqui83zc
c9af001 to
7790b14
Compare
Independent C++/CUDA/TensorRT port of NVIDIA SANA-Streaming video-to-video into
librediffusion (no Python in the inference path), validated numerically against
the PyTorch reference.
- GDN linear-attention CUDA kernel (row-split grid + shared-mem state) + the full
20-block DiT forward (x_embedder -> blocks -> final); resident bf16 weights +
bf16 tensor-core GEMMs (~122 ms/step). src/librediffusion.sana.{cu,hpp}.
- Self-contained 5-chunk streaming rollout that produces its own cross-chunk caches
(GDN state, softmax K/V history + sink/window, FFN temporal); FlowMatchEuler
scheduler; C++ casual_wan_rope.
- LTX-2 VAE (bf16 encoder/decoder) + gemma-2 text encoder in-process via the
TensorRT C++ runtime (SanaTRT), engines held resident. src/librediffusion.sana_trt.{cu,hpp}.
- C-API (src/librediffusion_c.sana.cpp + librediffusion_c.h): librediffusion_sana_
{create, free, set_engines, encode_prompt_ids, set_text_embeds, dit_forward,
run_v2v, rollout_selftest[_sc]}.
- CMakeLists: sana + sana_trt CUDA units into librediffusion_cuda; module +
CUDA::cublas into the librediffusion DLL (nvinfer_11 already linked).
Validation (rel-L2 vs PyTorch): DiT full model 3.13% (bf16); self-contained rollout
6.79%; end-to-end run_v2v 7.54% vs the reference video; VAE/gemma 0.28-0.74%; GDN
kernel ~1e-7 vs fp32 ref. run_v2v 4.04 s / ~30 output-fps for a 121-frame clip.
The DiT/GDN algorithm is ported from NVIDIA SANA (https://github.com/NVlabs/Sana,
Apache-2.0). Model weights are research-only (NVIDIA Open Model License) and are NOT
included; loaded at runtime from a user-provided path (see train-lora.py --type sana).
The SANA .cu/.hpp/.cpp are clang-format'd (repo .clang-format) and commented
(every kernel/free function/struct + the major phases in the DiT/rollout paths).
train-lora.py gains the --type sana export path (export_sana: DiT .bin weights +
bf16 LTX-2 VAE + gemma TRT engines into <out>/{dit,vae,gemma}).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPT5weE9MMS8cyxqui83zc
7790b14 to
5c3a17a
Compare
Summary
In-tree port of SANA-Streaming (video-to-video, GDN linear-attention DiT) into librediffusion as an independent C++/CUDA implementation, exposed through a
librediffusion_sana_*C-API. Enables no-Python streaming V2V within the library.What's added
src/librediffusion.sana.cu— GDN (Gated-Delta-Net) linear-attention recurrence kernel (row-split grid + shared-mem state, ~8× over the naive port) + all DiT block kernels (RMSNorm / adaLN / RoPE / softmax-attn / cross-attn / GLU-MBConv temporal FFN) + the full 20-block forward. Namespacedlibrediffusion::sana(no symbol clash withkernels.cu).src/librediffusion.sana.hpp—SanaDiTC++ class (no CUDA types leaked).src/librediffusion_c.sana.cpp+librediffusion_c.h—librediffusion_sana_{create, dit_forward, rollout_selftest, free}C-API.CMakeLists.txt— compilelibrediffusion.sana.cuintolibrediffusion_cuda; link the module +CUDA::cublasinto thelibrediffusionDLL.Validation (rel-L2 vs PyTorch reference, fp32)
model_in → model_out)sampler.sample()(through the DLL C-API)Per-chunk rollout rel-L2: 3.51 / 1.54 / 1.24 / 1.98 / 2.65%. (rel-L2 is the correct metric — the latents have a large dynamic range, so clamped mean-rel is misleading.)
No regression
build_dll.bat(Debug) +build_dll_release.bat(Release) → ninja exit 0; DLL builds.gdn-kernel-testALL PASS; DiT selftest 3.0962%.Performance (RTX 5090)
DiT denoise step 98.95 ms (bf16); full 121-frame clip ~4.5 s ≈ 27 fps (→ ~3.5 s / ~34 fps with bf16 VAE engines). PyTorch baseline ~2.7 s. Per-chunk cost is constant → genuinely infinite-streamable.
Licensing / weights
The DiT + GDN algorithm is ported from NVIDIA SANA (Apache-2.0); this is an independent re-implementation. Model weights are research-only under the NVIDIA Open Model License and are NOT included or distributed —
librediffusion_sana_create()loads them at runtime from a user-provided path.Not included (follow-ups)
librediffusion_sana_run_v2v's in-process TRT VAE / gemma boundary (VAE bf16 + gemma each validated in C++ separately;dit_forward+ the 5-chunk rollout are wired and validated).cudaMalloc).🤖 Generated with Claude Code
https://claude.ai/code/session_01EPT5weE9MMS8cyxqui83zc