diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..92a8316488e12eaac18432ed01b9b01bc1524a56 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,3 +33,25 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+code/infer/example/eg_cn.wav filter=lfs diff=lfs merge=lfs -text
+code/infer/example/edit_cn.mp3 filter=lfs diff=lfs merge=lfs -text
+code/infer/example/edit_en.mp3 filter=lfs diff=lfs merge=lfs -text
+code/infer/example/eg_en.mp3 filter=lfs diff=lfs merge=lfs -text
+code/infer/example/output-windows/en-utf8-fixed.mp3 filter=lfs diff=lfs merge=lfs -text
+code/infer/example/output-windows/en-without-utf8-fix.mp3 filter=lfs diff=lfs merge=lfs -text
+code/infer/example/output/output.mp3 filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/3c7495db974af283bccc2dee306a75da6491b2ca664d55597da926c4fd104dd5 filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/0b4ec6da503185d389dcfeefc2c5d03ab13e0d76620b5fb6ce022f79bb64140f filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/556ec3c4c0e12c97f4c9b8dd0cd65ae321238c55438fbf498055f0ad7afc05bb filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/1f47921d53a3b8de72172e2fd5f0a0d27f1e8f4b77016c6b8a1a4b591bb83ae5 filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/b9ddf00cad9d9a3bc91d09c8ee0f4fef42056861bb5ca1209fcc204b68950abf filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/2d16f4104fc6e34c556d42d6a35adedd5ffa396e71d59ef6dd7d5e88c01225e0 filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/b7c2e1ec67bfd9d4578a81ca6987f095b74869db5f518bf2fc9d3ffeb30821e5 filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/4ec2b03c373212fbe7fda97dbf9eaadb1bd42c2b0afc8b8a49806fe1dbc507f7 filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/49353ead98e6a241dd015dda937bf8dcf335e4eed27fe732df8163dd955d042f filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/98c4455a98982b35380ec62f90d5cf0edff02eeb4b3bb18595d871fdeb0c0c67 filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/3375e6602aa7264d6d67ae69ccb99690c8f40af7ab48d0af060dac1a2e9c24c9 filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/e211ac567cceb6b5be62db5e135a1a5f3e77bcacebdac6a8cf9e0cdc24158c09 filter=lfs diff=lfs merge=lfs -text
+code/g2p/sources/chinese_lexicon.txt filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/672d584c094e51e29aa5e46a6dd22cbfb993f4f839609e954d2c299912ea5d77 filter=lfs diff=lfs merge=lfs -text
+image/blobs/sha256/428f99cfe94d9b1e42af2f3d8104598f8ca6e6105925c7687595a18c78d21011 filter=lfs diff=lfs merge=lfs -text
diff --git a/code/g2p/sources/chinese_lexicon.txt b/code/g2p/sources/chinese_lexicon.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4d7dbf347a29d3b87c199d0e56ef7f1dbf28a6ee
--- /dev/null
+++ b/code/g2p/sources/chinese_lexicon.txt
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a3a7685d1c3e68eb2fa304bfc63e90c90c3c1a1948839a5b1b507b2131b3e2fb
+size 14779443
diff --git a/code/g2p/sources/g2p_chinese_model/poly_bert_model.onnx b/code/g2p/sources/g2p_chinese_model/poly_bert_model.onnx
new file mode 100644
index 0000000000000000000000000000000000000000..6b952b9717eb71bb5a7aa2492478095f117858dd
--- /dev/null
+++ b/code/g2p/sources/g2p_chinese_model/poly_bert_model.onnx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8765d835ffdf9811c832d4dc7b6a552757aa8615c01d1184db716a50c20aebbc
+size 76583333
diff --git a/code/infer/example/edit_cn.lrc b/code/infer/example/edit_cn.lrc
new file mode 100644
index 0000000000000000000000000000000000000000..4f472bafeefe9866e9b58844ccc4627b5d5d820d
--- /dev/null
+++ b/code/infer/example/edit_cn.lrc
@@ -0,0 +1,23 @@
+[00:13.55]亲爱的 人间爱情就像一朵花
+[00:20.29]想要种鲜花
+[00:22.51]手上难免沾泥巴
+[00:27.90]大红喜字铺开乡间田野
+[00:32.01]让我们住下
+[00:34.96]听微风吟唱吧
+[00:38.44]用光阴发新芽
+[00:41.04]裂缝钻出的新绿
+[00:43.97]正叩响整个盛夏
+[00:47.15]候鸟衔来远方的彩霞
+[00:52.03]荆棘里开出的花
+[00:55.67]每片花瓣都是回答
+[00:58.56]伤疤酿成琥珀时差
+[01:01.42]折射出星空的密码
+[01:04.36]蝴蝶吻过褪皮的痂
+[01:07.28]绽放成盔甲
+[01:10.11]牵你的手啊
+[01:12.90]在岁月种下鲜花
+[01:16.49]在寻常烟火人家
+[01:20.05]用漫长慢慢地回答
+[01:25.23]云会升起 雨会落下
+[01:28.86]不用催促啊
+[01:33.25]耐心的人啊才可以看见童话
\ No newline at end of file
diff --git a/code/infer/example/edit_cn.mp3 b/code/infer/example/edit_cn.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..b133befcae41eca8e5fb7668d0c88593a1102300
--- /dev/null
+++ b/code/infer/example/edit_cn.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8fbc319c58215aff58b018a253abf6e6ec31673d6336947b932f1212964240d4
+size 8534237
diff --git a/code/infer/example/edit_en.lrc b/code/infer/example/edit_en.lrc
new file mode 100644
index 0000000000000000000000000000000000000000..1046adc05100ad1e1c60cd19fcbf69a4b4a9da4c
--- /dev/null
+++ b/code/infer/example/edit_en.lrc
@@ -0,0 +1,19 @@
+[00:12.56]后来
+[00:14.58]我总算学会了
+[00:17.81]如何去爱
+[00:19.37]可惜你 早已远去
+[00:22.21]消失在人海
+[00:25.42]Through tears I understood too late
+[00:32.11]Some souls slip away beyond fate's gate
+[00:39.86]White petals of gardenia
+[00:46.32]Drift onto my blue pleated skirt
+[00:51.95]I love you your whisper fell
+[00:58.96]I bowed my head
+[01:00.97]Breathing your lingering scent
+[01:05.40]That eternal summer night
+[01:09.35]Seventeen years young
+[01:12.20]When your lips found mine
+[01:18.23]Now through passing years
+[01:21.79]Each wistful sigh
+[01:24.91]Recalls starlight in your eyes
+[01:30.91]Why did young love's promise
\ No newline at end of file
diff --git a/code/infer/example/edit_en.mp3 b/code/infer/example/edit_en.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..47893b7ac48ca73cdba29b0bbfc305aadcad742d
--- /dev/null
+++ b/code/infer/example/edit_en.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d72cb739c351b00a738f50e37a94cfc783dfa12b091c84ef9943b85a064015eb
+size 4771890
diff --git a/code/infer/example/eg_cn.lrc b/code/infer/example/eg_cn.lrc
new file mode 100644
index 0000000000000000000000000000000000000000..1cf7ff0efbe7036319287c0f2e0eadaaa1eacd3e
--- /dev/null
+++ b/code/infer/example/eg_cn.lrc
@@ -0,0 +1,16 @@
+[00:18.23]让我掉下眼泪的
+[00:21.80]不止昨夜的酒
+[00:26.06]让我依依不舍的
+[00:29.91]不止你的温柔
+[00:33.79]余路还要走多久
+[00:37.92]你攥着我的手
+[00:41.81]让我感到为难的
+[00:45.78]是挣扎的自由
+[00:51.81]分别总是在九月
+[00:55.66]回忆是思念的愁
+[00:59.77]深秋嫩绿的垂柳
+[01:03.55]亲吻着我额头
+[01:07.55]在那座阴雨的小城里
+[01:11.47]我从未忘记你
+[01:15.37]成都 带不走的 只有你
+[01:23.51]和我在成都的街头走一走
\ No newline at end of file
diff --git a/code/infer/example/eg_cn.wav b/code/infer/example/eg_cn.wav
new file mode 100644
index 0000000000000000000000000000000000000000..b817eae85b328c8d6de9ea9ea3e545b8b617d9d0
--- /dev/null
+++ b/code/infer/example/eg_cn.wav
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:67fc2163fa200544e5ecc1fcd7889124a60e6ff20fd70c53ad05a18859d79fd3
+size 882044
diff --git a/code/infer/example/eg_cn_full.lrc b/code/infer/example/eg_cn_full.lrc
new file mode 100644
index 0000000000000000000000000000000000000000..f05dfaa370736e69a856a6f7ba6c31ede1336ab5
--- /dev/null
+++ b/code/infer/example/eg_cn_full.lrc
@@ -0,0 +1,44 @@
+[00:18.23]让我掉下眼泪的
+[00:21.80]不止昨夜的酒
+[00:26.06]让我依依不舍的
+[00:29.91]不止你的温柔
+[00:33.79]余路还要走多久
+[00:37.92]你攥着我的手
+[00:41.81]让我感到为难的
+[00:45.78]是挣扎的自由
+[00:51.81]分别总是在九月
+[00:55.66]回忆是思念的愁
+[00:59.77]深秋嫩绿的垂柳
+[01:03.55]亲吻着我额头
+[01:07.55]在那座阴雨的小城里
+[01:11.47]我从未忘记你
+[01:15.37]成都 带不走的 只有你
+[01:23.51]和我在成都的街头走一走
+[01:31.38]直到所有的灯都熄灭了也不停留
+[01:39.23]你会挽着我的衣袖
+[01:43.12]我会把手揣进裤兜
+[01:47.13]走到玉林路的尽头
+[01:50.97]坐在小酒馆的门口
+[02:30.76]分别总是在九月
+[02:34.46]回忆是思念的愁
+[02:38.48]深秋嫩绿的垂柳
+[02:42.38]亲吻着我额头
+[02:46.40]在那座阴雨的小城里
+[02:50.35]我从未忘记你
+[02:54.21]成都 带不走的 只有你
+[03:02.18]和我在成都的街头走一走
+[03:10.02]直到所有的灯都熄灭了也不停留
+[03:18.16]你会挽着我的衣袖
+[03:21.90]我会把手揣进裤兜
+[03:25.80]走到玉林路的尽头
+[03:29.81]坐在小酒馆的门口
+[03:37.97]和我在成都的街头走一走
+[03:45.76]直到所有的灯都熄灭了也不停留
+[03:53.72]和我在成都的街头走一走
+[04:01.44]直到所有的灯都熄灭了也不停留
+[04:09.68]你会挽着我的衣袖
+[04:13.35]我会把手揣进裤兜
+[04:17.41]走到玉林路的尽头
+[04:21.27]走过小酒馆的门口
+[04:35.78]和我在成都的街头走一走
+[04:43.18]直到所有的灯都熄灭了也不停留
\ No newline at end of file
diff --git a/code/infer/example/eg_en.lrc b/code/infer/example/eg_en.lrc
new file mode 100644
index 0000000000000000000000000000000000000000..c097f2e2062e931d660d7446ec0d08e7015556e0
--- /dev/null
+++ b/code/infer/example/eg_en.lrc
@@ -0,0 +1,26 @@
+[00:10.00]Moonlight spills through broken blinds
+[00:13.20]Your shadow dances on the dashboard shrine
+[00:16.85]Neon ghosts in gasoline rain
+[00:20.40]I hear your laughter down the midnight train
+[00:24.15]Static whispers through frayed wires
+[00:27.65]Guitar strings hum our cathedral choirs
+[00:31.30]Flicker screens show reruns of June
+[00:34.90]I'm drowning in this mercury lagoon
+[00:38.55]Electric veins pulse through concrete skies
+[00:42.10]Your name echoes in the hollow where my heartbeat lies
+[00:45.75]We're satellites trapped in parallel light
+[00:49.25]Burning through the atmosphere of endless night
+[01:00.00]Dusty vinyl spins reverse
+[01:03.45]Our polaroid timeline bleeds through the verse
+[01:07.10]Telescope aimed at dead stars
+[01:10.65]Still tracing constellations through prison bars
+[01:14.30]Electric veins pulse through concrete skies
+[01:17.85]Your name echoes in the hollow where my heartbeat lies
+[01:21.50]We're satellites trapped in parallel light
+[01:25.05]Burning through the atmosphere of endless night
+[02:10.00]Clockwork gears grind moonbeams to rust
+[02:13.50]Our fingerprint smudged by interstellar dust
+[02:17.15]Velvet thunder rolls through my veins
+[02:20.70]Chasing phantom trains through solar plane
+[02:24.35]Electric veins pulse through concrete skies
+[02:27.90]Your name echoes in the hollow where my heartbeat lies
\ No newline at end of file
diff --git a/code/infer/example/eg_en.mp3 b/code/infer/example/eg_en.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..b1c0d493008b31c6b2e7748950a432c05d3655af
--- /dev/null
+++ b/code/infer/example/eg_en.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cdf3610c437715af336102a9bd34c63a3e882d9d99fe86b9fb193d8ebf9f2aa4
+size 10386551
diff --git a/code/infer/example/eg_en_full.lrc b/code/infer/example/eg_en_full.lrc
new file mode 100644
index 0000000000000000000000000000000000000000..673721a9b25e2242fc16d94ea94bec3a36971f50
--- /dev/null
+++ b/code/infer/example/eg_en_full.lrc
@@ -0,0 +1,57 @@
+[00:00.52]Abracadabra abracadabra
+[00:03.97]Ha
+[00:04.66]Abracadabra abracadabra
+[00:12.02]Yeah
+[00:15.80]Pay the toll to the angels
+[00:19.08]Drawin' circles in the clouds
+[00:23.31]Keep your mind on the distance
+[00:26.67]When the devil turns around
+[00:30.95]Hold me in your heart tonight
+[00:34.11]In the magic of the dark moonlight
+[00:38.44]Save me from this empty fight
+[00:43.83]In the game of life
+[00:45.84]Like a poem said by a lady in red
+[00:49.45]You hear the last few words of your life
+[00:53.15]With a haunting dance now you're both in a trance
+[00:56.90]It's time to cast your spell on the night
+[01:01.40]Abracadabra ama-ooh-na-na
+[01:04.88]Abracadabra porta-ooh-ga-ga
+[01:08.92]Abracadabra abra-ooh-na-na
+[01:12.30]In her tongue she's sayin'
+[01:14.76]Death or love tonight
+[01:18.61]Abracadabra abracadabra
+[01:22.18]Abracadabra abracadabra
+[01:26.08]Feel the beat under your feet
+[01:27.82]The floor's on fire
+[01:29.90]Abracadabra abracadabra
+[01:33.78]Choose the road on the west side
+[01:37.09]As the dust flies watch it burn
+[01:41.45]Don't waste time on feeling
+[01:44.64]Your depression won't return
+[01:49.15]Hold me in your heart tonight
+[01:52.21]In the magic of the dark moonlight
+[01:56.54]Save me from this empty fight
+[02:01.77]In the game of life
+[02:03.94]Like a poem said by a lady in red
+[02:07.52]You hear the last few words of your life
+[02:11.19]With a haunting dance now you're both in a trance
+[02:14.95]It's time to cast your spell on the night
+[02:19.53]Abracadabra ama-ooh-na-na
+[02:22.71]Abracadabra porta-ooh-ga-ga
+[02:26.94]Abracadabra abra-ooh-na-na
+[02:30.42]In her tongue she's sayin'
+[02:32.83]Death or love tonight
+[02:36.55]Abracadabra abracadabra
+[02:40.27]Abracadabra abracadabra
+[02:44.19]Feel the beat under your feet
+[02:46.14]The floor's on fire
+[02:47.95]Abracadabra abracadabra
+[02:51.17]Phantom of the dance floor come to me
+[02:58.46]Sing for me a sinful melody
+[03:06.51]Ah-ah-ah-ah-ah ah-ah ah-ah
+[03:13.76]Ah-ah-ah-ah-ah ah-ah ah-ah
+[03:22.39]Abracadabra ama-ooh-na-na
+[03:25.66]Abracadabra porta-ooh-ga-ga
+[03:29.87]Abracadabra abra-ooh-na-na
+[03:33.16]In her tongue she's sayin'
+[03:35.55]Death or love tonight
\ No newline at end of file
diff --git a/code/infer/example/music_prompt_keywords.json b/code/infer/example/music_prompt_keywords.json
new file mode 100644
index 0000000000000000000000000000000000000000..3f8a4ab7e07a86a0e893674db69f13a6f7aee2c8
--- /dev/null
+++ b/code/infer/example/music_prompt_keywords.json
@@ -0,0 +1,67 @@
+{
+ "genre": [
+ "Pop",
+ "Rock",
+ "Electronic",
+ "Blues",
+ "R&B",
+ "Jazz",
+ "Soul",
+ "Country",
+ "Folk",
+ "Dance",
+ "Rap",
+ "Classical",
+ "Hip-Hop",
+ "Chinese",
+ "House",
+ "Punk",
+ "Metal",
+ "Funk",
+ "Reggae",
+ "Disco"
+ ],
+ "mood": [
+ "Romantic",
+ "Nostalgic",
+ "Heartfelt",
+ "Happy",
+ "Melancholic",
+ "Love",
+ "Upbeat",
+ "Energetic",
+ "Uplifting",
+ "Carefree",
+ "Fun",
+ "Emotional",
+ "Heartbroken",
+ "Reflective",
+ "Mellow",
+ "Hopeful",
+ "Relaxed",
+ "Passionate",
+ "Aggressive"
+ ],
+ "instrument": [
+ "Piano",
+ "Electric Guitar",
+ "Drums",
+ "Synthesizer",
+ "Bass",
+ "Strings",
+ "Bass Guitar",
+ "Acoustic Guitar",
+ "Guitar",
+ "Trumpet",
+ "Saxophone",
+ "Violin",
+ "Organ",
+ "Percussion",
+ "Vocal Sample",
+ "Flute",
+ "Harmonica",
+ "Banjo",
+ "Electric Bass",
+ "Cello"
+ ]
+ }
\ No newline at end of file
diff --git a/code/infer/example/output-windows/en-utf8-fixed.mp3 b/code/infer/example/output-windows/en-utf8-fixed.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..f4957cd9c49340256ea74ee25b2102e86b5f0152
--- /dev/null
+++ b/code/infer/example/output-windows/en-utf8-fixed.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:10abbeefc22c1d03607cc074f825bbd4e0b7326e438fd13cb46985edf4a7ae6f
+size 1522668
diff --git a/code/infer/example/output-windows/en-without-utf8-fix.mp3 b/code/infer/example/output-windows/en-without-utf8-fix.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..188009ca6381cab2b9bf588d737cc25f251fe67f
--- /dev/null
+++ b/code/infer/example/output-windows/en-without-utf8-fix.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:acf1fd072cf269745eaf2ee3c8674662a4c99441fc839c3bc0635f2dd89794ca
+size 1522668
diff --git a/code/infer/example/output/output.mp3 b/code/infer/example/output/output.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..431939aadcc010031c20723b4995d2deef25b648
--- /dev/null
+++ b/code/infer/example/output/output.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6311c518821ee0d37d24c65d92c479f6c26da68b782687ab0ee69fb7a9ef97d3
+size 2832762
diff --git a/code/infer/example/vocal.npy b/code/infer/example/vocal.npy
new file mode 100644
index 0000000000000000000000000000000000000000..f3056dc285d16aa6abaf81ba70f6f0eb7038c304
--- /dev/null
+++ b/code/infer/example/vocal.npy
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6cb7d74eb7a8eda12acb8247b21d373928301db8a8cb0db480d341799fed3ce5
+size 2176
diff --git a/code/models/tt_dit/experimental/lora/__init__.py b/code/models/tt_dit/experimental/lora/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e13459ada2877a67fad216d55a31379cb8db5e78
--- /dev/null
+++ b/code/models/tt_dit/experimental/lora/__init__.py
@@ -0,0 +1,2 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/code/models/tt_dit/experimental/lora/adapter_loader.py b/code/models/tt_dit/experimental/lora/adapter_loader.py
new file mode 100644
index 0000000000000000000000000000000000000000..350de02989670bbae56c55c940dcf3b8d7339807
--- /dev/null
+++ b/code/models/tt_dit/experimental/lora/adapter_loader.py
@@ -0,0 +1,392 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Adapter loader for the runtime-LoRA Wan 2.2 pipeline.
+
+Reads a LoRA safetensors file, parses A/B pairs using the same key
+normalization as ``pipeline_wan_lora.py``, and uploads them into the
+LoRA-aware Linear modules of a `WanTransformer3DModel` constructed with
+``lora_enabled=True``.
+
+Fused-QKV handling (the hard part):
+ Wan attention uses a fused ``to_qkv`` (self-attn) and ``to_kv``
+ (cross-attn) projection. The adapter ships separate ``to_q``,
+ ``to_k``, ``to_v`` LoRA pairs. We combine them into a single LoRA
+ pair on the fused module by:
+
+ - stacking A along the rank dim: A_fused = [A_q; A_k; A_v] → [3r, in]
+ - building a block-diagonal B with rank=3r and head-interleaving
+ it the same way the base weight is head-interleaved:
+ B_fused = head_interleave([B_q | 0 | 0, 0 | B_k | 0, 0 | 0 | B_v])
+
+ This triples the LoRA matmul cost on those layers (rank=3r) but keeps
+ the math exact and shippable in v0. A follow-up could split this into
+ 3 independent adapters at the layer level for ~3x lower cost.
+
+`.diff` / `.diff_b` direct deltas are out of scope for v0 — the loader
+warns and skips them.
+"""
+from __future__ import annotations
+
+import re
+from collections.abc import Sequence
+from dataclasses import dataclass
+from pathlib import Path
+
+import torch
+from loguru import logger
+from safetensors.torch import load_file
+
+from models.tt_dit.experimental.utils.lightx2v_loader import wan_lightx2v_to_diffusers_key
+
+# Reused from pipeline_wan_lora.py — kept inline so this module is self-contained.
+_STRIP_PREFIXES = ("diffusion_model.", "transformer.", "unet.", "model.")
+_LOW_RANK_RE = re.compile(r"^(?P.*)\.lora_(?PA|B|down|up)(?:\.[^.]+)?\.weight$")
+_SLOT_MAP = {"A": "A", "down": "A", "B": "B", "up": "B"}
+
+
+def _strip_known_prefixes(key: str) -> str:
+ for prefix in _STRIP_PREFIXES:
+ if key.startswith(prefix):
+ return key[len(prefix) :]
+ return key
+
+
+def _kohya_to_lightx2v(key: str) -> str:
+ if not key.startswith("lora_unet_"):
+ return key
+ parts = key.split(".", 1)
+ module_path = parts[0][len("lora_unet_") :]
+ suffix = f".{parts[1]}" if len(parts) > 1 else ""
+ m = re.match(r"blocks_(\d+)_(cross_attn|self_attn)_([a-z]+)", module_path)
+ if m:
+ return f"blocks.{m.group(1)}.{m.group(2)}.{m.group(3)}{suffix}"
+ m = re.match(r"blocks_(\d+)_(ffn)_(\d+)", module_path)
+ if m:
+ return f"blocks.{m.group(1)}.{m.group(2)}.{m.group(3)}{suffix}"
+ logger.warning(f"unrecognized kohya key structure: {key}")
+ return key
+
+
+def _normalize_key(raw: str) -> str:
+ return _kohya_to_lightx2v(_strip_known_prefixes(raw))
+
+
+# --------------------------------------------------------------------
+# Public API
+# --------------------------------------------------------------------
+@dataclass(frozen=True)
+class AdapterHandle:
+ name: str
+ rank: int # the "expected bound rank" — may be 3r for fused-QKV targets
+ target_indices: dict[str, int]
+ """Map from a stable target identifier (e.g. ``'blocks.0.attn1.to_qkv'``)
+ to the bank index returned by `register_lora` on that module."""
+
+
+def load_adapter_into(
+ transformer, # WanTransformer3DModel constructed with lora_enabled=True
+ path: str,
+ *,
+ scale: float = 1.0,
+ name: str = "",
+) -> AdapterHandle:
+ """Load a single LoRA safetensors file into `transformer`.
+
+ Returns an `AdapterHandle` recording the bank index assigned to each
+ LoRA-targeted Linear. Use it later with
+ ``pipeline.set_active_lora(handle)``.
+ """
+ raw = load_file(str(path))
+ if not any(_is_lora_key(k) for k in raw):
+ raise RuntimeError(f"no LoRA-style keys (lora_A/lora_B, lora_down/lora_up) in {path}")
+
+ pairs, alphas, skipped_direct = _collect_pairs(raw)
+ if skipped_direct:
+ logger.warning(
+ f"{path}: skipping {skipped_direct} direct (.diff/.diff_b) deltas "
+ "— runtime-LoRA pipeline does not support these yet."
+ )
+
+ # Group targets
+ fused_qkv: dict[tuple[int, str], dict[str, dict[str, torch.Tensor]]] = {}
+ singletons: list[tuple[str, dict[str, torch.Tensor]]] = [] # (diffusers_path, ab)
+ skipped_unmapped = 0
+
+ for base_path, ab in pairs.items():
+ diff_path = _diffusers_path(base_path)
+ if diff_path is None:
+ skipped_unmapped += 1
+ continue
+ m = re.match(r"blocks\.(\d+)\.(attn1|attn2)\.to_([qkv])$", diff_path)
+ if m:
+ block_idx, attn, qkv = int(m.group(1)), m.group(2), m.group(3)
+ # attn1 (self-attn): Q/K/V all fold into to_qkv.
+ # attn2 (cross-attn): Q is its own singleton; K/V fold into to_kv.
+ if attn == "attn1" or qkv in ("k", "v"):
+ fused_qkv.setdefault((block_idx, attn), {})[qkv] = ab
+ continue
+ # attn2.to_q is a real singleton — fall through.
+ # Everything else: 1:1 mapping
+ singletons.append((diff_path, ab))
+
+ if skipped_unmapped:
+ logger.warning(
+ f"{path}: {skipped_unmapped} pairs unmapped — adapter targets a module "
+ "that doesn't exist in the tt-dit name map."
+ )
+
+ target_indices: dict[str, int] = {}
+ fused_ranks: list[int] = []
+ singleton_ranks: list[int] = []
+
+ # Fused QKV / KV
+ for (block_idx, attn_name), qkvs in fused_qkv.items():
+ attn = getattr(transformer.blocks[block_idx], attn_name)
+ bank_idx, fused_rank = _register_fused_qkv(attn, block_idx, qkvs, alphas, scale, name)
+ if bank_idx is None:
+ continue
+ key = f"blocks.{block_idx}.{attn_name}.{'to_qkv' if attn.is_self else 'to_kv'}"
+ target_indices[key] = bank_idx
+ fused_ranks.append(fused_rank)
+
+ # Singletons
+ for diff_path, ab in singletons:
+ target, canonical = _resolve_singleton(transformer, diff_path)
+ if target is None:
+ logger.warning(f"unmapped singleton LoRA target: {diff_path}")
+ continue
+ A = ab["A"]
+ B = ab["B"]
+ rank = A.shape[0]
+ alpha = alphas.get(_lightx2v_for_path(diff_path), float(rank))
+ eff_scale = scale * (alpha / rank)
+ idx = target.register_lora(A, B, scale=eff_scale, name=name)
+ target_indices[canonical] = idx
+ singleton_ranks.append(rank)
+
+ # Sanity: every LoRA module in this adapter should have the same
+ # bound rank, but fused_QKV produces a 3x larger rank than singletons.
+ # The bound-tensor system handles per-module ranks independently, so
+ # we just record both for reporting.
+ all_ranks = fused_ranks + singleton_ranks
+ if not all_ranks:
+ raise RuntimeError(f"{path}: no LoRA targets registered")
+ representative_rank = max(all_ranks)
+ return AdapterHandle(name=name or Path(path).stem, rank=representative_rank, target_indices=target_indices)
+
+
+# --------------------------------------------------------------------
+# Helpers
+# --------------------------------------------------------------------
+def _is_lora_key(raw: str) -> bool:
+ key = _normalize_key(raw)
+ return bool(_LOW_RANK_RE.match(key)) or key.endswith((".diff", ".diff_b"))
+
+
+def _collect_pairs(raw: dict[str, torch.Tensor]):
+ """Returns ({base_path: {'A': T, 'B': T}}, {base_path: alpha}, num_skipped_direct)."""
+ pairs: dict[str, dict[str, torch.Tensor]] = {}
+ alphas: dict[str, float] = {}
+ skipped_direct = 0
+ unrecognized: list[tuple[str, str]] = []
+
+ for raw_key, tensor in raw.items():
+ key = _normalize_key(raw_key)
+ m = _LOW_RANK_RE.match(key)
+ if m:
+ pairs.setdefault(m.group("base"), {})[_SLOT_MAP[m.group("slot")]] = tensor
+ elif key.endswith(".alpha"):
+ alphas[key[: -len(".alpha")]] = float(tensor.item())
+ elif key.endswith(".diff") or key.endswith(".diff_b"):
+ skipped_direct += 1
+ else:
+ unrecognized.append((raw_key, key))
+
+ # Surface dropped keys — a non-standard naming convention silently
+ # producing plausible-but-wrong output is the failure mode we cannot
+ # afford here.
+ if unrecognized:
+ sample = ", ".join(f"{raw}→{norm}" for raw, norm in unrecognized[:5])
+ more = "" if len(unrecognized) <= 5 else f" (+{len(unrecognized) - 5} more)"
+ logger.warning(
+ f"adapter loader: {len(unrecognized)} key(s) did not match any known "
+ f"pattern (lora_A/B, lora_down/up, .alpha, .diff, .diff_b) and were "
+ f"dropped. Samples: {sample}{more}"
+ )
+
+ return pairs, alphas, skipped_direct
+
+
+def _diffusers_path(lightx2v_base: str) -> str | None:
+ """Map a lightx2v base path to its diffusers-canonical equivalent
+ (without ``.weight``). Returns None if mapping fails."""
+ try:
+ key = wan_lightx2v_to_diffusers_key(f"{lightx2v_base}.weight")
+ except Exception: # noqa: BLE001
+ return None
+ if not key.endswith(".weight"):
+ return None
+ return key[: -len(".weight")]
+
+
+def _lightx2v_for_path(diffusers_path: str) -> str:
+ """Reverse-map (best-effort) a diffusers path back to lightx2v for alpha
+ lookups — alphas are keyed in the source naming."""
+ # Simple cases that the existing map covers:
+ rev = {
+ "attn1.to_q": "self_attn.q",
+ "attn1.to_k": "self_attn.k",
+ "attn1.to_v": "self_attn.v",
+ "attn1.to_out.0": "self_attn.o",
+ "attn2.to_q": "cross_attn.q",
+ "attn2.to_k": "cross_attn.k",
+ "attn2.to_v": "cross_attn.v",
+ "attn2.to_out.0": "cross_attn.o",
+ "ffn.net.0.proj": "ffn.0",
+ "ffn.net.2": "ffn.2",
+ }
+ for diff_seg, lx_seg in rev.items():
+ if diff_seg in diffusers_path:
+ return diffusers_path.replace(diff_seg, lx_seg)
+ return diffusers_path
+
+
+def _resolve_singleton(transformer, diff_path: str):
+ """Resolve a diffusers-style path to (module, tt_dit_dotted_path).
+
+ Returns (None, None) when no match. The tt-dit path is the one
+ consumed by the pipeline mixin's bind/unbind walker — it skips
+ diffusers-only segments like the trailing ``.0`` in ``to_out.0``.
+ """
+ m = re.match(r"blocks\.(\d+)\.(attn1|attn2)\.to_q$", diff_path)
+ if m:
+ block_idx, attn_name = int(m.group(1)), m.group(2)
+ attn = getattr(transformer.blocks[block_idx], attn_name)
+ if not attn.is_self:
+ return attn.to_q, f"blocks.{block_idx}.{attn_name}.to_q"
+ return None, None # self-attn to_q shouldn't arrive here
+ m = re.match(r"blocks\.(\d+)\.(attn1|attn2)\.to_out\.0$", diff_path)
+ if m:
+ block_idx, attn_name = int(m.group(1)), m.group(2)
+ return getattr(transformer.blocks[block_idx], attn_name).to_out, (f"blocks.{block_idx}.{attn_name}.to_out")
+ m = re.match(r"blocks\.(\d+)\.ffn\.net\.0\.proj$", diff_path)
+ if m:
+ block_idx = int(m.group(1))
+ return transformer.blocks[block_idx].ffn.ff1, f"blocks.{block_idx}.ffn.ff1"
+ m = re.match(r"blocks\.(\d+)\.ffn\.net\.2$", diff_path)
+ if m:
+ block_idx = int(m.group(1))
+ return transformer.blocks[block_idx].ffn.ff2, f"blocks.{block_idx}.ffn.ff2"
+ return None, None
+
+
+def _register_fused_qkv(
+ attn,
+ block_idx: int,
+ qkvs: dict[str, dict[str, torch.Tensor]],
+ alphas: dict[str, float],
+ scale: float,
+ name: str,
+) -> tuple[int | None, int]:
+ """Stack Q/K/V (or K/V for cross-attn) LoRA pairs onto attn.to_qkv (or
+ attn.to_kv). Returns (bank_idx, fused_rank) or (None, 0) on skip."""
+ if attn.is_self:
+ required = ["q", "k", "v"]
+ target = attn.to_qkv
+ else:
+ required = ["k", "v"]
+ target = attn.to_kv
+
+ missing = [r for r in required if r not in qkvs]
+ if missing:
+ logger.warning(
+ f"fused QKV LoRA on {target.__class__.__name__} is missing {missing}; "
+ "skipping this group (need all of "
+ f"{required}, got {list(qkvs)})."
+ )
+ return None, 0
+
+ A_per = [qkvs[r]["A"] for r in required]
+ B_per = [qkvs[r]["B"] for r in required]
+ ranks = [A.shape[0] for A in A_per]
+ if len(set(ranks)) != 1:
+ raise ValueError(f"QKV LoRA ranks must match; got {ranks}")
+ r = ranks[0]
+ n = len(required)
+ in_dim = attn.dim
+ out_per = attn.dim # each of Q/K/V has out=dim
+
+ # A stacked on rank dim → [n*r, in]
+ A_fused = torch.cat(A_per, dim=0)
+ assert A_fused.shape == (n * r, in_dim), f"A_fused {A_fused.shape}"
+
+ # B: each per-source B is [out_per=dim, r]. We embed each into a
+ # [out_per, n*r] tile (zeros for other-source rank columns). Then
+ # head-interleave the list of [out_per, n*r] tiles into a single
+ # [n*out_per, n*r] tensor — matching the head layout of the fused
+ # to_qkv / to_kv base weight.
+ n_dev = attn.parallel_config.tensor_parallel.factor
+ n_local_heads = attn.n_local_heads
+ head_dim = attn.head_dim
+
+ B_per_padded: list[torch.Tensor] = []
+ for i, B in enumerate(B_per):
+ pad = torch.zeros(out_per, n * r, dtype=B.dtype)
+ pad[:, i * r : (i + 1) * r] = B
+ B_per_padded.append(pad)
+
+ B_fused = _head_interleave_lora_B(B_per_padded, n_dev=n_dev, n_local_heads=n_local_heads, head_dim=head_dim)
+ assert B_fused.shape == (n * out_per, n * r), f"B_fused {B_fused.shape}"
+
+ # Effective scale: pick the first source's alpha (alphas are usually
+ # identical across Q/K/V in a single adapter; if not, the choice is
+ # safe-ish because we apply scale to all three the same way).
+ alpha_keys = [_lightx2v_qkv_key(attn, block_idx, r_name) for r_name in required]
+ alpha = next((alphas[k] for k in alpha_keys if k in alphas), float(r))
+ eff_scale = scale * (alpha / r)
+
+ bank_idx = target.register_lora(A_fused, B_fused, scale=eff_scale, name=name)
+ return bank_idx, n * r
+
+
+def _lightx2v_qkv_key(attn, block_idx: int, qkv: str) -> str:
+ """Build the lightx2v alpha key for a Q/K/V projection inside a
+ given WanAttention. Alphas in `_collect_pairs` are keyed by the full
+ normalized base path (``blocks...``), so the block
+ index must be threaded in alongside ``attn.is_self``."""
+ side = "self_attn" if attn.is_self else "cross_attn"
+ return f"blocks.{block_idx}.{side}.{qkv}"
+
+
+def _head_interleave_lora_B(
+ tensors: Sequence[torch.Tensor],
+ *,
+ n_dev: int,
+ n_local_heads: int,
+ head_dim: int,
+) -> torch.Tensor:
+ """Mirror of WanAttention._interleave_heads for LoRA B tensors.
+
+ Each input is [out_per_source=num_heads*head_dim, rank_fused].
+ Returns a concatenated [N*out_per, rank_fused] tensor with the out
+ dim head-interleaved so column-parallel sharding lines up with the
+ fused base weight's layout.
+ """
+ out_per = n_dev * n_local_heads * head_dim
+ fused_rank = tensors[0].shape[1]
+ assert all(t.shape == (out_per, fused_rank) for t in tensors), (
+ f"all B tensors must be [{out_per}, {fused_rank}]; got " f"{[tuple(t.shape) for t in tensors]}"
+ )
+ n = len(tensors)
+ # Transpose each to [fused_rank, out_per]
+ transposed = [t.T for t in tensors]
+ # Reshape out dim to [fused_rank, n_dev, n_local_heads, head_dim]
+ reshaped = [t.reshape(fused_rank, n_dev, n_local_heads, head_dim) for t in transposed]
+ # Concat on heads sub-dim → [fused_rank, n_dev, N*n_local_heads, head_dim]
+ merged = torch.cat(reshaped, dim=2)
+ # Flatten back → [fused_rank, N*n_dev*n_local_heads*head_dim] = [fused_rank, N*out_per]
+ merged = merged.reshape(fused_rank, n * out_per)
+ # Transpose to [N*out_per, fused_rank]
+ return merged.T
diff --git a/code/models/tt_dit/experimental/lora/ltx_adapter_loader.py b/code/models/tt_dit/experimental/lora/ltx_adapter_loader.py
new file mode 100644
index 0000000000000000000000000000000000000000..6351462d928af30d5b5dc8e6933aa9cc0d5c8f99
--- /dev/null
+++ b/code/models/tt_dit/experimental/lora/ltx_adapter_loader.py
@@ -0,0 +1,363 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Adapter loader for on-device fuse-mode LoRA on the LTX-2.3 transformer.
+
+Reads a LoRA safetensors file and registers its A/B pairs into the
+LoRA-aware Linear modules of an ``LTXTransformerModel`` built with
+``lora_enabled=True``. Swap is then a ``bind_active`` on each module
+(``weight.data += scale * A@B`` on device) — no host fuse, no reload.
+
+Why this differs from the Wan loader
+-------------------------------------
+LTX's ``LTXAttention._prepare_torch_state`` transforms the raw checkpoint
+Q/K/V weights before they reach the device:
+
+ - ``_permute_qk``: reorders each head's channels from the checkpoint's
+ SPLIT rotary layout to the INTERLEAVED layout the RoPE op expects.
+ Applied to Q and K (self-attn) and to the cross-attn ``to_q``.
+ - ``_interleave_heads``: folds separate Q/K/V (or K/V) into the fused
+ ``to_qkv`` / ``to_kv`` weight with heads interleaved so column-parallel
+ sharding lands the right head on each device.
+
+The host-fuse path (``utils.fuse_loras.fuse_loras_into``) is correct
+without any of this because it merges the delta into the *raw* checkpoint
+weight and lets ``_prepare_torch_state`` permute the merged result. Here
+the on-device weight is ALREADY permuted+interleaved, so the LoRA B must
+be pre-transformed identically or the merged delta lands in the wrong
+layout. ``to_out``/``ff1``/``ff2`` are untouched by ``_prepare_torch_state``,
+so their adapters need no transform (the mixin copies W's own sharding).
+
+Scope: every LoRA-adaptable Linear in the transformer — video/audio/cross-modal
+attention (``to_qkv``/``to_q``/``to_kv``/``to_out``/``to_gate_logits``), both FFNs
+(ff1/ff2), and the globals (``patchify_proj``, ``proj_out``, and the
+``adaln_single`` / timestep-embedder linears). ``promote_to_lora`` makes the plain
+globals bindable; attention/FFN are already LoRA via ``lora_enabled``. Only
+attention q/k/v/q-of-cross carry the rotary permute + head-interleave transform;
+everything else registers directly. ``.diff``/``.diff_b`` direct deltas remain
+unsupported; unmapped keys are surfaced loudly, never silently dropped.
+"""
+from __future__ import annotations
+
+import re
+from dataclasses import dataclass
+from pathlib import Path
+
+import torch
+from loguru import logger
+from safetensors.torch import load_file
+
+from ...layers.lora import LoRAMixin
+from .promote import promote_to_lora
+
+_STRIP_PREFIXES = ("model.diffusion_model.", "diffusion_model.", "transformer.", "model.")
+_LOW_RANK_RE = re.compile(r"^(?P.*)\.lora_(?PA|B|down|up)(?:\.[^.]+)?\.weight$")
+_SLOT_MAP = {"A": "A", "down": "A", "B": "B", "up": "B"}
+
+# Diffusers LTX checkpoints name blocks "transformer_blocks."; some exports
+# use the shorter "blocks.". Accept both.
+_BLOCK_RE = r"(?:transformer_blocks|blocks)\.(\d+)"
+
+# All LTXAttention instances on a block share the same to_q/k/v/out/gate_logits
+# structure and fusion rules (self -> to_qkv, cross -> to_q + to_kv), so the loader
+# handles them uniformly, reading is_self/heads off each resolved module.
+_ATTN_NAMES = ("attn1", "attn2", "audio_attn1", "audio_attn2", "audio_to_video_attn", "video_to_audio_attn")
+# Diffusers FFN container name -> tt-dit attribute (ff.net.0.proj/net.2 -> ffX).
+_FF_NAME_TO_ATTR = {"ff": "ffn", "audio_ff": "audio_ff"}
+
+
+@dataclass(frozen=True)
+class LTXAdapterHandle:
+ name: str
+ target_indices: dict[str, int]
+ """Map from a canonical tt-dit module path (e.g. ``transformer_blocks.0.attn1.to_qkv``)
+ to the bank index returned by ``register_lora`` on that module."""
+
+
+def load_ltx_adapter_into(
+ transformer, # LTXTransformerModel built with lora_enabled=True
+ path: str,
+ *,
+ scale: float = 1.0,
+ name: str = "",
+) -> LTXAdapterHandle:
+ """Register a single LoRA safetensors file into ``transformer``.
+
+ Returns a handle recording the bank index assigned per module; pass it
+ to the pipeline's ``set_active_lora`` to bind it on device.
+ """
+ raw = load_file(str(path))
+ pairs, alphas, skipped_direct, unrecognized = _collect_pairs(raw)
+ if not pairs:
+ raise RuntimeError(f"no LoRA A/B pairs (lora_A/B or lora_down/up) found in {path}")
+ if skipped_direct:
+ logger.warning(f"{path}: skipping {skipped_direct} direct (.diff/.diff_b) deltas — not supported.")
+ if unrecognized:
+ sample = ", ".join(unrecognized[:5])
+ more = "" if len(unrecognized) <= 5 else f" (+{len(unrecognized) - 5} more)"
+ logger.warning(
+ f"{path}: {len(unrecognized)} key(s) matched no known LoRA pattern and were dropped: {sample}{more}"
+ )
+
+ # Promote every plain Linear (adaln/proj/patchify/gate globals) to a LoRA
+ # variant so it can be bound; attn/ffn are already LoRA via lora_enabled.
+ promote_to_lora(transformer)
+
+ # Q/K/V fold into a fused projection; the rest are 1:1 block singletons or
+ # direct globals (patchify/proj_out/adaln/timestep-embedder).
+ fused: dict[tuple[int, str], dict[str, dict[str, torch.Tensor]]] = {}
+ singletons: list[tuple[int, str, str, dict[str, torch.Tensor]]] = [] # (block, container, sub, ab)
+ globals_: list[tuple[str, dict[str, torch.Tensor]]] = []
+
+ for base, ab in pairs.items():
+ parsed = _parse_target(base)
+ if parsed is None:
+ globals_.append((base, ab))
+ continue
+ block_idx, container, sub = parsed
+ if sub in ("to_q", "to_k", "to_v"):
+ attn = _get_attn(transformer, block_idx, container)
+ is_self = attn.is_self if attn is not None else container in ("attn1", "audio_attn1")
+ qkv = sub[-1]
+ # self-attn: q/k/v all fold into to_qkv. cross-attn: k/v fold into to_kv, q is a singleton.
+ if is_self or qkv in ("k", "v"):
+ fused.setdefault((block_idx, container), {})[qkv] = ab
+ continue
+ singletons.append((block_idx, container, sub, ab))
+
+ target_indices: dict[str, int] = {}
+
+ for (block_idx, container), qkvs in fused.items():
+ attn = _get_attn(transformer, block_idx, container)
+ if attn is None:
+ logger.warning(f"{path}: no attention module {container} in block {block_idx} — skipping fused group.")
+ continue
+ idx = _register_fused(attn, qkvs, alphas, scale, name, block_idx, container)
+ if idx is None:
+ continue
+ canonical = f"transformer_blocks.{block_idx}.{container}.{'to_qkv' if attn.is_self else 'to_kv'}"
+ target_indices[canonical] = idx
+
+ for block_idx, container, sub, ab in singletons:
+ if "A" not in ab or "B" not in ab:
+ logger.warning(f"{path}: {container}.{sub} in block {block_idx} missing one of A/B — skipping.")
+ continue
+ target, canonical, permute_dims = _resolve_singleton(transformer, block_idx, container, sub)
+ if target is None:
+ logger.warning(f"{path}: unmapped {container}.{sub} in block {block_idx} — skipping.")
+ continue
+ A = ab["A"]
+ B = ab["B"]
+ if permute_dims is not None:
+ B = _permute_qk_rows(B, *permute_dims)
+ rank = A.shape[0]
+ alpha = alphas.get(_alpha_key(block_idx, container, sub), float(rank))
+ idx = target.register_lora(A, B, scale=scale * (alpha / rank), name=name)
+ target_indices[canonical] = idx
+
+ unmapped_global = 0
+ for base, ab in globals_:
+ if "A" not in ab or "B" not in ab:
+ continue
+ target = _resolve_global(transformer, base)
+ if not isinstance(target, LoRAMixin):
+ unmapped_global += 1
+ continue
+ A = ab["A"]
+ B = ab["B"]
+ rank = A.shape[0]
+ alpha = alphas.get(base, float(rank))
+ idx = target.register_lora(A, B, scale=scale * (alpha / rank), name=name)
+ target_indices[base] = idx
+
+ if unmapped_global:
+ logger.warning(f"{path}: {unmapped_global} global key(s) matched no LTX module — dropped.")
+
+ if not target_indices:
+ raise RuntimeError(f"{path}: no LoRA targets registered — check the adapter's key naming against LTX modules.")
+
+ return LTXAdapterHandle(name=name or Path(path).stem, target_indices=target_indices)
+
+
+def iter_lora_modules(root, prefix: str = ""):
+ """Yield (dotted_path, module) for every LoRAMixin descendant of ``root``.
+ Paths match the loader's canonical keys (e.g. ``transformer_blocks.0.attn1.to_qkv``)."""
+ for name, child in root.named_children():
+ path = f"{prefix}.{name}" if prefix else name
+ if isinstance(child, LoRAMixin):
+ yield path, child
+ yield from iter_lora_modules(child, path)
+
+
+# --------------------------------------------------------------------
+# Key parsing
+# --------------------------------------------------------------------
+def _strip_prefixes(key: str) -> str:
+ for p in _STRIP_PREFIXES:
+ if key.startswith(p):
+ return key[len(p) :]
+ return key
+
+
+def _collect_pairs(raw: dict[str, torch.Tensor]):
+ """Return ({base: {'A','B'}}, {base: alpha}, num_skipped_direct, unrecognized_keys)."""
+ pairs: dict[str, dict[str, torch.Tensor]] = {}
+ alphas: dict[str, float] = {}
+ skipped_direct = 0
+ unrecognized: list[str] = []
+ for raw_key, tensor in raw.items():
+ key = _strip_prefixes(raw_key)
+ m = _LOW_RANK_RE.match(key)
+ if m:
+ pairs.setdefault(m.group("base"), {})[_SLOT_MAP[m.group("slot")]] = tensor
+ elif key.endswith(".alpha"):
+ alphas[key[: -len(".alpha")]] = float(tensor.item())
+ elif key.endswith((".diff", ".diff_b")):
+ skipped_direct += 1
+ else:
+ unrecognized.append(raw_key)
+ return pairs, alphas, skipped_direct, unrecognized
+
+
+def _parse_target(base: str) -> tuple[int, str, str] | None:
+ """Parse a stripped per-block LoRA base path into (block_idx, container, sub).
+
+ Covers every block attention family (video/audio/cross-modal) and both FFN
+ containers. Returns None for non-block paths (globals: patchify/proj_out/adaln),
+ which ``_resolve_global`` handles by direct path lookup."""
+ attn_alt = "|".join(_ATTN_NAMES)
+ m = re.match(rf"^{_BLOCK_RE}\.({attn_alt})\.to_(q|k|v|out|gate_logits)(?:\.0)?$", base)
+ if m:
+ raw = m.group(3)
+ sub = {"out": "to_out", "gate_logits": "to_gate_logits"}.get(raw, f"to_{raw}")
+ return int(m.group(1)), m.group(2), sub
+ ff_alt = "|".join(_FF_NAME_TO_ATTR)
+ m = re.match(rf"^{_BLOCK_RE}\.({ff_alt})\.net\.0\.proj$", base)
+ if m:
+ return int(m.group(1)), m.group(2), "ff1"
+ m = re.match(rf"^{_BLOCK_RE}\.({ff_alt})\.net\.2$", base)
+ if m:
+ return int(m.group(1)), m.group(2), "ff2"
+ return None
+
+
+def _alpha_key(block_idx: int, attn_name: str, sub: str) -> str:
+ return f"transformer_blocks.{block_idx}.{attn_name}.{sub}"
+
+
+# --------------------------------------------------------------------
+# Module resolution
+# --------------------------------------------------------------------
+def _get_attn(transformer, block_idx: int, attn_name: str):
+ blocks = transformer.transformer_blocks
+ if not (0 <= block_idx < len(blocks)):
+ return None
+ return getattr(blocks[block_idx], attn_name, None)
+
+
+def _resolve_singleton(transformer, block_idx: int, container: str, sub: str):
+ """Return (module, canonical_path, permute_dims) where permute_dims is
+ (num_heads, head_dim) if the base weight is _permute_qk'd, else None.
+ (None, None, None) when the target does not exist."""
+ blocks = transformer.transformer_blocks
+ if not (0 <= block_idx < len(blocks)):
+ return None, None, None
+ block = blocks[block_idx]
+ if sub in ("to_q", "to_out", "to_gate_logits"):
+ attn = getattr(block, container, None)
+ if attn is None:
+ return None, None, None
+ target = getattr(attn, sub, None)
+ if target is None:
+ return None, None, None
+ # Only cross-attn to_q is _permute_qk'd; to_out/to_gate_logits are not.
+ permute_dims = (attn.num_heads, attn.head_dim) if (sub == "to_q" and not attn.is_self) else None
+ return target, f"transformer_blocks.{block_idx}.{container}.{sub}", permute_dims
+ if sub in ("ff1", "ff2"):
+ ff_attr = _FF_NAME_TO_ATTR.get(container, "ffn")
+ ff = getattr(block, ff_attr, None)
+ if ff is None:
+ return None, None, None
+ target = getattr(ff, sub, None)
+ return target, f"transformer_blocks.{block_idx}.{ff_attr}.{sub}", None
+ return None, None, None
+
+
+def _resolve_global(transformer, base: str):
+ """Resolve a non-block LoRA key (patchify_proj, proj_out, adaln_single.*, ...)
+ to the module at that dotted path on the transformer root, or None if absent.
+ These are plain replicated/parallel Linears with no rotary layout transform."""
+ mod = transformer
+ for part in base.split("."):
+ mod = getattr(mod, part, None)
+ if mod is None:
+ return None
+ return mod
+
+
+# --------------------------------------------------------------------
+# Fused QKV / KV registration (permute_qk + head-interleave on B)
+# --------------------------------------------------------------------
+def _register_fused(attn, qkvs, alphas, scale, name, block_idx, attn_name) -> int | None:
+ required = ["q", "k", "v"] if attn.is_self else ["k", "v"]
+ missing = [r for r in required if r not in qkvs or "A" not in qkvs[r] or "B" not in qkvs[r]]
+ if missing:
+ logger.warning(f"fused LoRA on block {block_idx}.{attn_name} missing {missing}; skipping (got {list(qkvs)}).")
+ return None
+
+ ranks = [qkvs[r]["A"].shape[0] for r in required]
+ if len(set(ranks)) != 1:
+ raise ValueError(f"QKV LoRA ranks must match on block {block_idx}.{attn_name}; got {ranks}")
+ r = ranks[0]
+ n = len(required)
+
+ num_heads = attn.num_heads
+ head_dim = attn.head_dim
+ n_dev = attn.parallel_config.tensor_parallel.factor
+ n_local_heads = attn.n_local_heads
+ out_per = attn.dim # each fused source projects to dim
+
+ # A stacked on rank -> [n*r, in]
+ A_fused = torch.cat([qkvs[x]["A"] for x in required], dim=0)
+
+ # B: permute Q/K rows (RoPE), pad each into a block-diagonal [out, n*r] tile,
+ # then head-interleave the tiles into [n*out, n*r] to match the fused weight.
+ B_padded = []
+ for i, x in enumerate(required):
+ B = qkvs[x]["B"]
+ if x in ("q", "k"):
+ B = _permute_qk_rows(B, num_heads, head_dim)
+ pad = torch.zeros(out_per, n * r, dtype=B.dtype)
+ pad[:, i * r : (i + 1) * r] = B
+ B_padded.append(pad)
+ B_fused = _interleave_heads_rows(B_padded, num_heads, head_dim, n_dev, n_local_heads)
+
+ alpha = alphas.get(_alpha_key(block_idx, attn_name, f"to_{required[0]}"), float(r))
+ eff_scale = scale * (alpha / r)
+ # register_lora lives on the fused projection Linear (LoRAMixin), not the attention wrapper.
+ fused_linear = attn.to_qkv if attn.is_self else attn.to_kv
+ return fused_linear.register_lora(A_fused, B_fused, scale=eff_scale, name=name)
+
+
+def _permute_qk_rows(t: torch.Tensor, num_heads: int, head_dim: int) -> torch.Tensor:
+ """Reorder each head's output channels SPLIT -> INTERLEAVED, mirroring
+ ``LTXAttention._prepare_torch_state._permute_qk``. ``t`` is [num_heads*head_dim, ...]."""
+ D = head_dim
+ D_half = D // 2
+ perm = torch.empty(D, dtype=torch.long)
+ perm[0::2] = torch.arange(D_half)
+ perm[1::2] = torch.arange(D_half, D)
+ rest = t.shape[1:]
+ return t.reshape(num_heads, D, *rest).index_select(1, perm).reshape(num_heads * D, *rest)
+
+
+def _interleave_heads_rows(tensors, num_heads: int, head_dim: int, n_dev: int, n_local_heads: int) -> torch.Tensor:
+ """Interleave the OUT (row) dim across sources, mirroring
+ ``LTXAttention._prepare_torch_state._interleave_heads``. Each input is
+ [out=num_heads*head_dim, C]; returns [n*out, C]."""
+ transposed = [t.T for t in tensors] # [C, out]
+ reshaped = [t.reshape(t.shape[0], n_dev, n_local_heads, head_dim) for t in transposed]
+ merged = torch.cat(reshaped, dim=2) # [C, n_dev, n*n_local_heads, head_dim]
+ merged = merged.reshape(merged.shape[0], len(tensors) * num_heads * head_dim) # [C, n*out]
+ return merged.T # [n*out, C]
diff --git a/code/models/tt_dit/experimental/lora/promote.py b/code/models/tt_dit/experimental/lora/promote.py
new file mode 100644
index 0000000000000000000000000000000000000000..729d0ff60d94d8d2a46191c3c81e61d428c33160
--- /dev/null
+++ b/code/models/tt_dit/experimental/lora/promote.py
@@ -0,0 +1,63 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+"""Generic in-place promotion of Linear-family modules to their LoRA variants.
+
+Rather than thread a ``lora_enabled`` flag through every module constructor,
+walk a built model once and upgrade each plain ``Linear``/``ColParallelLinear``/
+``RowParallelLinear`` to the matching ``LoRAMixin`` subclass. The subclasses are
+thin (mixin + base + ``_init_lora_state``), so swapping ``__class__`` and calling
+``_init_lora_state`` post-construction is equivalent to having built the LoRA
+variant directly. Modules already LoRA-aware are left untouched.
+
+This keeps the swap surface identical — ``bind_active`` / ``set_active_lora`` /
+``reapply_after_load`` act on the promoted modules exactly as before — while
+making every Linear in the model a LoRA target, not just the attn/ffn subset.
+"""
+from __future__ import annotations
+
+from ...layers.linear import ColParallelLinear, Linear, RowParallelLinear
+from ...layers.lora import LoRAMixin
+
+# Base types eligible for in-place promotion. Exact-type match (not isinstance)
+# so an unknown Linear subclass isn't silently mis-promoted.
+_PROMOTABLE = (Linear, ColParallelLinear, RowParallelLinear)
+
+# Per-base promotion classes, built lazily. Base FIRST, mixin SECOND: Python
+# forbids ``__class__`` assignment when the mixin comes first (the instance
+# "solid base" changes -> 'object layout differs'). With the base first the
+# layout is identical to the original, so the swap is allowed. The pre-defined
+# LoRA*Linear classes are mixin-first (so their forward override wins in runtime
+# mode) and can't be used here; fuse mode never needs that override, and every
+# bind method is LoRAMixin-exclusive, so ordering is otherwise irrelevant.
+_PROMOTED_CACHE: dict[type, type] = {}
+
+
+def _promoted_class(base_cls: type) -> type:
+ cls = _PROMOTED_CACHE.get(base_cls)
+ if cls is None:
+ cls = type(f"LoRA{base_cls.__name__}", (base_cls, LoRAMixin), {})
+ _PROMOTED_CACHE[base_cls] = cls
+ return cls
+
+
+def _iter_modules(root):
+ yield root
+ for _, child in root.named_children():
+ yield from _iter_modules(child)
+
+
+def promote_to_lora(root, *, mode: str = "fuse") -> int:
+ """Upgrade every plain Linear-family descendant of ``root`` to a LoRA-aware
+ class in place. Returns the number promoted. Idempotent: modules already
+ ``LoRAMixin`` (e.g. built via ``lora_enabled``) are skipped."""
+ promoted = 0
+ for module in _iter_modules(root):
+ if isinstance(module, LoRAMixin):
+ continue
+ if type(module) not in _PROMOTABLE:
+ continue
+ module.__class__ = _promoted_class(type(module))
+ module._init_lora_state(mode=mode)
+ promoted += 1
+ return promoted
diff --git a/code/models/tt_dit/experimental/models/AniSora.md b/code/models/tt_dit/experimental/models/AniSora.md
new file mode 100644
index 0000000000000000000000000000000000000000..6e8ca7384ba06234dbb3c5d06a9b9ac59004f636
--- /dev/null
+++ b/code/models/tt_dit/experimental/models/AniSora.md
@@ -0,0 +1,136 @@
+# Index-AniSora V3.2 (I2V)
+
+## Introduction
+
+[IndexTeam/Index-anisora](https://huggingface.co/IndexTeam/Index-anisora) V3.2
+is an anime-domain image-to-video model derived from
+[Wan2.2-I2V-A14B](https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B-Diffusers).
+It preserves the two-expert (high-noise / low-noise) MoE structure of Wan2.2
+and ships a finetuned weight pair specialized for anime-style motion.
+
+This is a **full checkpoint replacement** (not a LoRA adapter or distilled model).
+The model graph is identical to base Wan2.2 I2V — see [Wan2_2.md](Wan2_2.md) for
+the underlying architecture.
+
+## Details
+
+The two-stage MoE schedule is preserved: the high-noise expert handles the
+first 90% of the denoising trajectory and the low-noise expert handles the
+final 10% (`boundary_ratio=0.9`, vs 0.5 for base Wan2.2).
+
+The AniSora safetensors use the original-Wan key naming
+(`blocks.X.self_attn.q.weight`, etc), identical to the lightx2v distill
+checkpoints. The same `wan_lightx2v_to_diffusers_key` rename function in
+`models/tt_dit/utils/lightx2v_loader.py` is reused.
+
+`NUM_STEPS` is configurable (default 40). Lower step counts (16, 8) can produce video at decent quality.
+
+## Weights
+
+### AniSora V3.2 experts (DiT only)
+
+- **Source:** [IndexTeam/Index-anisora](https://huggingface.co/IndexTeam/Index-anisora)
+- **Files:**
+ - `V3.2/high_noise_model/diffusion_pytorch_model.safetensors` (~28 GB, BF16)
+ - `V3.2/low_noise_model/diffusion_pytorch_model.safetensors` (~28 GB, BF16)
+- **Total download:** ~57 GB (auto-downloaded on first run with `TT_DIT_ALLOW_HF_DOWNLOAD=1`)
+
+### Base Wan2.2 components (shared, not re-downloaded if already cached)
+
+- **Source:** [Wan-AI/Wan2.2-I2V-A14B-Diffusers](https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B-Diffusers)
+- **Components:** tokenizer, UMT5 text encoder, VAE, scheduler config
+- **Total download:** ~12 GB (shared with base Wan2.2)
+
+### Download weights manually (optional)
+
+Weights are auto-downloaded via HuggingFace Hub on first run. To pre-download or use a local copy:
+
+```bash
+# Option 1: Pre-download to HF cache
+python -c "
+from huggingface_hub import hf_hub_download
+for f in ['V3.2/high_noise_model/diffusion_pytorch_model.safetensors',
+ 'V3.2/low_noise_model/diffusion_pytorch_model.safetensors']:
+ hf_hub_download('IndexTeam/Index-anisora', f)
+"
+
+# Option 2: Point to a local directory
+# Layout must be:
+# $ANISORA_LOCAL_DIR/V3.2/high_noise_model/diffusion_pytorch_model.safetensors
+# $ANISORA_LOCAL_DIR/V3.2/low_noise_model/diffusion_pytorch_model.safetensors
+export ANISORA_LOCAL_DIR=/path/to/index-anisora-weights
+```
+
+## Inference defaults
+
+Mirrors `anisoraV3.2/wan/configs/wan_i2v_A14B.py`:
+
+| Parameter | Value | Note |
+|---|---|---|
+| `num_inference_steps` | 40 | UniPC sampler (configurable via `NUM_STEPS` env var) |
+| `boundary_ratio` | 0.9 | High-noise covers first 90% of timesteps |
+| `guidance_scale` | 3.5 | High-noise stage CFG |
+| `guidance_scale_2` | 3.5 | Low-noise stage CFG |
+| `sample_shift` | 5.0 | Inherited from base Wan2.2 scheduler config |
+
+## Performance
+
+BH Galaxy 4x8 Ring (sp=8, tp=4), 81 frames @ 16fps (~5s video):
+
+| Stage | Base Wan2.2 I2V 480p | AniSora V3.2 I2V 480p | AniSora V3.2 I2V 720p |
+|---|---|---|---|
+| Text Encoding | 0.13s | 0.14s | 0.14s |
+| Image Encoding | 5.03s | 5.02s | 12.84s |
+| Denoising | 48.92s (40 steps, 1.22s/step) | 48.29s (40 steps, 1.21s/step) | 133.61s (40 steps, 3.34s/step) |
+| VAE Decoding | 0.43s | 0.43s | 0.70s |
+| **Total** | **54.52s** | **53.90s** | **147.32s** |
+
+### Reduced-steps perf (AniSora V3.2)
+
+| Steps | 480p (832x480) | 720p (1280x720) |
+|---|---|---|
+| 40 | 53.90s | 147.32s |
+| 16 | 25.0s | 67.2s |
+| 8 | 15.3s | 40.4s |
+
+## Supported configurations
+
+| System | Mesh | SP | TP | Topology | Test ID |
+|---|---|---|---|---|---|
+| BH Galaxy | 4x8 | 8 (axis 1) | 4 (axis 0) | Ring | `bh_4x8sp1tp0_ring` |
+
+## Prerequisites
+- Cloned [tt-metal repository](https://github.com/tenstorrent/tt-metal) for source code
+- Installed: [TT-Metalium / TT-NN](https://github.com/tenstorrent/tt-metal/blob/main/INSTALLING.md)
+
+## How to Run
+
+```bash
+# Environment setup
+export TT_DIT_CACHE_DIR=/your/cache/path
+export TT_METAL_HOME=$(pwd)
+export PYTHONPATH=$(pwd)
+export TT_DIT_ALLOW_HF_DOWNLOAD=1
+
+# Set input image and prompt
+export PROMPT_IMAGE=$TT_METAL_HOME/dog.jpg
+export PROMPT="An anime girl smiling, soft lighting, cinematic"
+export NUM_STEPS=40 # 40 default, or 16/8 for faster previews
+export NO_PROMPT=1
+
+# Run AniSora inference (480p)
+pytest models/tt_dit/experimental/tests/test_pipeline_anisora.py \
+ -v -k "bh_4x8sp1tp0_ring and resolution_480p and not random_weights" \
+ --timeout 1800 -s
+# Output: ./wan_anisora_i2v_832x480_0.mp4
+
+# Run AniSora inference (720p)
+pytest models/tt_dit/experimental/tests/test_pipeline_anisora.py \
+ -v -k "bh_4x8sp1tp0_ring and resolution_720p and not random_weights" \
+ --timeout 1800 -s
+# Output: ./wan_anisora_i2v_1280x720_0.mp4
+
+# Random-weights smoke test (no large download needed)
+pytest models/tt_dit/experimental/tests/test_pipeline_anisora.py::test_pipeline_inference_random_weights \
+ -v --timeout 1500 -s
+```
diff --git a/code/models/tt_dit/experimental/models/Wan2_2_Distill.md b/code/models/tt_dit/experimental/models/Wan2_2_Distill.md
new file mode 100644
index 0000000000000000000000000000000000000000..58c07b65d431d965ccc0e1c2f0e4090ddb66aba0
--- /dev/null
+++ b/code/models/tt_dit/experimental/models/Wan2_2_Distill.md
@@ -0,0 +1,89 @@
+# Wan2.2-Distill (lightx2v, 4-step I2V)
+
+## Introduction
+
+[lightx2v/Wan2.2-Distill-Models](https://huggingface.co/lightx2v/Wan2.2-Distill-Models)
+is a 4-step distillation of [Wan2.2-I2V-A14B](https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B-Diffusers).
+It produces image-to-video outputs in roughly 1/10 the denoising steps of the
+base model, with classifier-free guidance baked in.
+
+This page documents the tt_dit integration. See [Wan2_2.md](Wan2_2.md) for the
+underlying architecture — the distill is weight-only; the model graph is identical.
+
+## Details
+
+The distill ships only the DiT weights as flat `.safetensors` files. The
+tokenizer, UMT5 text encoder, VAE, and scheduler are loaded from the base
+`Wan-AI/Wan2.2-I2V-A14B-Diffusers` repo.
+
+The two-stage MoE structure is preserved: a high-noise expert handles the first
+half of the denoising trajectory and a low-noise expert handles the rest. With
+4 inference steps and `boundary_ratio=0.5`, that's 2 steps per expert.
+
+## Files used
+
+From [lightx2v/Wan2.2-Distill-Models](https://huggingface.co/lightx2v/Wan2.2-Distill-Models):
+
+- `wan2.2_i2v_A14b_high_noise_lightx2v_4step.safetensors` (BF16, 28.6 GB) — high-noise expert
+- `wan2.2_i2v_A14b_low_noise_lightx2v_4step.safetensors` (BF16, 28.6 GB) — low-noise expert
+
+From [Wan-AI/Wan2.2-I2V-A14B-Diffusers](https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B-Diffusers):
+tokenizer, text encoder, VAE, scheduler config.
+
+## Inference defaults
+
+| Parameter | Value | Note |
+|---|---|---|
+| `num_inference_steps` | 4 | Distill target |
+| `boundary_ratio` | 0.5 | 2 high-noise + 2 low-noise |
+| `guidance_scale` | 1.0 | CFG baked in — disables negative-prompt path |
+| `guidance_scale_2` | 1.0 | Same as above for the low-noise stage |
+
+## Supported configurations (PR1)
+
+| System | Mesh | SP | TP | Topology | Test ID |
+|---|---|---|---|---|---|
+| BH Galaxy | 4x8 | 8 (axis 1) | 4 (axis 0) | Ring | `bh_4x8sp1tp0_ring` |
+
+Additional mesh shapes will be added in follow-up work.
+
+## How to run
+
+```bash
+# Required environment
+export TT_DIT_CACHE_DIR=/your/cache/path
+export TT_METAL_HOME=$(pwd)
+export PYTHONPATH=$(pwd)
+
+# Allow first-time download of the lightx2v safetensors (~57 GB total).
+# Skip if you've already downloaded them and put them in the HF cache or
+# pointed LIGHTX2V_LOCAL_DIR at a directory that contains both files.
+export TT_DIT_ALLOW_HF_DOWNLOAD=1
+
+# Place a seed image at ./prompt_image.png in the working directory.
+
+NO_PROMPT=1 pytest \
+ models/tt_dit/experimental/tests/test_pipeline_wan_distill_i2v.py \
+ -v -k "bh_4x8sp1tp0_ring and resolution_480p" --timeout 1500
+```
+
+To use weights from a custom location:
+
+```bash
+export LIGHTX2V_LOCAL_DIR=/path/to/lightx2v/weights
+```
+
+## Performance
+
+BH Galaxy 4×8 Ring, 81 frames, single timed iteration (2026-05-08):
+
+| Stage | Base 480p (40 steps, CFG) | Base 720p (40 steps, CFG) | Distill 480p (4 steps, no CFG) | Distill 720p (4 steps, no CFG) |
+|---|---:|---:|---:|---:|
+| Text encoding (UMT5) | 0.136 s | 0.133 s | 0.134 s | 0.138 s |
+| Image encoding (CLIP+VAE encode) | 5.020 s | 13.024 s | 4.949 s | 12.577 s |
+| Denoising | 48.806 s | 133.243 s | 2.436 s | 6.553 s |
+| VAE decoding | 0.426 s | 0.711 s | 0.424 s | 0.746 s |
+| **Total** | **54.4 s** | **147.1 s** | **7.96 s** | **20.0 s** |
+| Speedup vs base | — | — | 6.84× | 7.34× |
+
+Per-forward denoise cost is identical between base and distill at each resolution; distill's win is "10× fewer steps × 2× from no-CFG" with kernel performance unchanged.
diff --git a/code/models/tt_dit/experimental/models/Wan2_2_LoRA.md b/code/models/tt_dit/experimental/models/Wan2_2_LoRA.md
new file mode 100644
index 0000000000000000000000000000000000000000..d9a9b2878c52b4e640a8255cbcf005cd83ee63fa
--- /dev/null
+++ b/code/models/tt_dit/experimental/models/Wan2_2_LoRA.md
@@ -0,0 +1,197 @@
+# Wan2.2 I2V — LoRA Adapter Pipeline
+
+## Introduction
+
+`WanPipelineI2VLora` fuses [LoRA](https://arxiv.org/abs/2106.09685) adapters
+into the base [Wan2.2-I2V-A14B](https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B-Diffusers)
+weights on CPU before pushing to device. Inference uses vanilla I2V
+machinery with no LoRA-specific runtime cost.
+
+Each expert transformer (high-noise + low-noise) accepts an ordered LoRA
+stack — multiple adapters can be combined on the same expert. Later entries
+see earlier ones' deltas already folded into the base, which matches the
+training-time expectation for adapters that were fine-tuned on top of
+another adapter (e.g. an SVI LoRA on top of a LightX2V LoRA).
+
+See [../../models/Wan2_2.md](../../models/Wan2_2.md) for the base model architecture and
+[Wan2_2_SVI.md](Wan2_2_SVI.md) for long-video generation that builds on this pipeline.
+
+## Details
+
+- **CPU-side fusion:** `W' = W + sum_i scale_i * (B_i @ A_i)` for low-rank
+ adapter pairs, with per-module alpha scaling applied when an `.alpha` key
+ is present. The `fp32` promote → add → cast-back uses a fresh allocation
+ per pair to avoid mutating the base when a stack folds in multiple
+ adapters touching the same target.
+- **Single aggregate verification:** after the full stack is fused, an L2
+ norm check confirms the base weights actually changed (catches silent
+ load failures from key mismatches).
+- **Cache namespacing:** the TT cache is keyed by a SHA1 hash of the ordered
+ `(resolved_path, scale)` tuples per expert. Two stacks with the same files
+ in different order get different namespaces.
+
+### Supported adapter key formats
+
+`fuse_lora_state_dict` auto-detects these key shapes per adapter entry:
+
+1. **Low-rank pairs** at one of:
+ - `.lora_A.weight` / `.lora_B.weight` (lightx2v native)
+ - `.lora_down.weight` / `.lora_up.weight` (legacy / kohya)
+ - PEFT-style `.lora_A.default.weight` (the trailing `.default`
+ adapter-name segment is tolerated; SVI uses this convention)
+2. **Bias deltas** at `.diff_b` — added to the base `.bias`.
+3. **Full-parameter deltas** at `.diff` — added to the base
+ `.weight`. Some LoRAs ship RMSNorm gammas via this channel.
+4. **Per-module alpha** at `.alpha` — applied as
+ `effective_scale = scale * alpha / rank` for the low-rank pair under
+ the same ``.
+
+### Supported key namespaces (auto-stripped or remapped)
+
+- **lightx2v native:** `blocks..attn.q.lora_A.weight` — passes through.
+- **diffusers prefixes:** `diffusion_model.`, `transformer.`, `unet.`,
+ `model.` — stripped automatically before matching.
+- **kohya / A1111:** `lora_unet_blocks__cross_attn_k.lora_down.weight`
+ — remapped to lightx2v-style.
+
+Anything outside the above is logged as a skipped-unknown warning rather
+than erroring, so legitimate-but-unsupported keys don't break the load.
+
+## API
+
+The adapter paths are constructor arguments, so build the config with
+`default_config` (which pins the I2V model type and checkpoint) and construct the
+pipeline directly.
+
+```python
+from models.tt_dit.experimental.pipelines.pipeline_wan_lora import LoRASpec, WanPipelineI2VLora
+
+config = WanPipelineI2VLora.default_config(mesh_device=mesh_device, height=480, width=832)
+
+# Single-LoRA, both experts
+pipe = WanPipelineI2VLora(
+ device=mesh_device,
+ config=config,
+ lora_high="/path/high.safetensors",
+ lora_low="/path/low.safetensors",
+)
+
+# Multi-LoRA stack (LightX2V applied first, then SVI on top, half strength)
+pipe = WanPipelineI2VLora(
+ device=mesh_device,
+ config=config,
+ lora_high=[
+ LoRASpec("/path/lightx2v_high.safetensors", scale=1.0),
+ LoRASpec("/path/svi_high.safetensors", scale=0.5),
+ ],
+ lora_low=[
+ LoRASpec("/path/lightx2v_low.safetensors", scale=1.0),
+ LoRASpec("/path/svi_low.safetensors", scale=1.0),
+ ],
+)
+```
+
+Either `lora_high` or `lora_low` may be `None` — that expert uses unmodified
+base weights. At least one must be set.
+
+## Weights
+
+### Earth zoom-out LoRA
+
+- **Source:** [wangkanai/wan22-fp16-i2v-loras](https://huggingface.co/wangkanai/wan22-fp16-i2v-loras)
+- **File:** `loras/wan/wan22-camera-earthzoomout.safetensors` (293 MB)
+- **Format:** Single-file, kohya/A1111-style keys
+
+### Camera rotation LoRA
+
+- **Source:** [wangkanai/wan22-fp16-i2v-loras](https://huggingface.co/wangkanai/wan22-fp16-i2v-loras)
+- **File:** `loras/wan/wan22-camera-rotation-rank16-v2.safetensors` (293 MB)
+- **Format:** Single-file, kohya/A1111-style keys
+
+### Base Wan2.2 components (shared)
+
+- **Source:** [Wan-AI/Wan2.2-I2V-A14B-Diffusers](https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B-Diffusers)
+- **Components:** tokenizer, UMT5 text encoder, VAE, scheduler config, transformer base weights (~70 GB)
+
+### Download
+
+```bash
+python -c "
+from huggingface_hub import hf_hub_download
+import os
+os.makedirs('/path/to/lora-camera', exist_ok=True)
+for f in ['loras/wan/wan22-camera-earthzoomout.safetensors',
+ 'loras/wan/wan22-camera-rotation-rank16-v2.safetensors']:
+ hf_hub_download('wangkanai/wan22-fp16-i2v-loras', f, local_dir='/path/to/lora-camera')
+"
+```
+
+## Supported configurations
+
+| System | Mesh | SP | TP | Topology | Test ID |
+|---|---|---|---|---|---|
+| BH Loud Box | 2x4 | 4 (axis 1) | 2 (axis 0) | Linear | `bh_2x4sp1tp0` |
+| BH Galaxy | 4x8 | 8 (axis 1) | 4 (axis 0) | Ring | `bh_4x8sp1tp0_ring` |
+
+## How to Run
+
+### Single-LoRA (40 steps)
+
+```bash
+export TT_DIT_CACHE_DIR=/your/cache/path
+export TT_METAL_HOME=$(pwd)
+export PYTHONPATH=$(pwd)
+export TT_DIT_ALLOW_HF_DOWNLOAD=1
+
+export LORA_HIGH_PATH=/path/to/lora-camera/wan22-camera-earthzoomout.safetensors
+unset LORA_LOW_PATH # single-file: low-noise expert uses base weights
+export LORA_SCALE=1.0
+
+export PROMPT_IMAGE=$TT_METAL_HOME/dog.jpg
+export PROMPT="A golden retriever running on a sandy beach, waves in the background"
+export NUM_STEPS=40
+export GUIDANCE_SCALE=3.5
+export BOUNDARY_RATIO=0.875
+export NO_PROMPT=1
+
+pytest models/tt_dit/experimental/tests/test_pipeline_lora.py \
+ -v -k "bh_4x8sp1tp0_ring and resolution_480p" \
+ --timeout 1800 -s
+```
+
+### Multi-LoRA stack
+
+```bash
+# Comma-separated path[:scale] entries. Order matters -- earlier entries are
+# fused first.
+export LORA_STACK_HIGH="/path/lightx2v_high.safetensors:1.0,/path/style_high.safetensors:0.5"
+export LORA_STACK_LOW="/path/lightx2v_low.safetensors:1.0,/path/style_low.safetensors:1.0"
+
+pytest models/tt_dit/experimental/tests/test_pipeline_lora.py \
+ -v -k "bh_4x8sp1tp0_ring and resolution_480p" \
+ --timeout 1800 -s
+```
+
+## Environment variables
+
+| Variable | Required | Default | Description |
+|---|---|---|---|
+| `LORA_HIGH_PATH` | Either this or stack | — | Path to high-noise expert LoRA `.safetensors` |
+| `LORA_LOW_PATH` | No | None | Path to low-noise expert LoRA |
+| `LORA_SCALE` | No | 1.0 | Blend strength applied to single-LoRA entries |
+| `LORA_STACK_HIGH` | Either this or single | — | Comma-separated `path[:scale]` list for the high-noise expert |
+| `LORA_STACK_LOW` | No | None | Comma-separated `path[:scale]` list for the low-noise expert |
+| `NUM_STEPS` | No | 40 | Inference steps |
+| `GUIDANCE_SCALE` | No | 3.5 | CFG scale |
+| `BOUNDARY_RATIO` | No | 0.875 | High/low expert split ratio |
+| `PROMPT_IMAGE` | No | `./prompt_image.png` | Seed image path |
+| `PROMPT` | No | golden retriever prompt | Text prompt |
+
+## Limitations / open items
+
+- LoRA fusion happens on CPU before device push — adds ~5–10 s per expert to
+ pipeline initialization (one-time cost, scales with stack depth).
+- Only `bh_4x8sp1tp0_ring` is exercised in CI.
+- Any community LoRA trained for Wan2.2 I2V should work if its keys follow
+ lightx2v native, diffusers, or kohya/A1111 naming. Untested LoRAs may need
+ key remap additions.
diff --git a/code/models/tt_dit/experimental/models/Wan2_2_SVI.md b/code/models/tt_dit/experimental/models/Wan2_2_SVI.md
new file mode 100644
index 0000000000000000000000000000000000000000..55229ad3e3c0ff464882506f89f34b5ab7cfd89a
--- /dev/null
+++ b/code/models/tt_dit/experimental/models/Wan2_2_SVI.md
@@ -0,0 +1,204 @@
+# Wan2.2 I2V — Stable-Video-Infinity (SVI 2.0 Pro)
+
+## Introduction
+
+`WanPipelineSVI` generates long videos by autoregressively chaining short
+Wan2.2 I2V clips. Per upstream's
+[SVI 2.0 Pro doc](https://github.com/vita-epfl/Stable-Video-Infinity/blob/svi_wan22/docs/svi/svi_2.0_pro.md),
+the conditioning latent for each clip is built as
+
+```
+y = concat([anchor_latent, motion_latent, padding], dim=temporal)
+```
+
+- **`anchor_latent`** — the VAE-encoded user-provided first frame. The same
+ anchor is used for every clip so the long video keeps a fixed visual
+ reference.
+- **`motion_latent`** — the last `num_motion_latent` latent time steps from
+ the previous clip's denoised output (`None` on the first clip). This is a
+ continuity-strength knob, not a value derived from `num_frames`.
+- **`padding`** — literal-zero latents for the remaining frame slots.
+
+The original SVI 2.0 last-frame handoff is explicitly **not** used in 2.0
+Pro — the decoded last frame of the previous clip is never re-encoded.
+Setting `num_motion_latent=0` collapses 2.0 Pro back to 2.0 behavior (anchor
+only, no latent handoff).
+
+SVI ships as a LoRA adapter pair (one per Wan2.2 MoE expert) trained with
+"Error-Recycling Fine-Tuning" so the model is robust to its own drift when
+chained. See [Wan2_2_LoRA.md](Wan2_2_LoRA.md) for the underlying LoRA
+fusion pipeline this builds on.
+
+Upstream: [vita-epfl/Stable-Video-Infinity](https://github.com/vita-epfl/Stable-Video-Infinity)
+(svi_wan22 branch).
+
+## Sampling regimes
+
+| Regime | Steps | CFG | Scheduler | flow_shift | LoRA stack (high / low) |
+|---|---|---|---|---|---|
+| `python` | 50 | 5.0 | `FlowMatchEulerDiscreteScheduler` → `EulerSolver` | 5.0 | SVI 1.0 / SVI 1.0 |
+| `comfyui` | 6 | 1.5 | `UniPCMultistepScheduler(use_flow_sigmas=True)` → `UniPCSolver` *(see note)* | 8.0 | LightX2V 0.5 + SVI 1.0 / LightX2V 1.0 + SVI 1.0 |
+
+The `python` regime matches upstream's `inference_svi_2.0_pro.py` —
+diffsynth's `FlowMatchScheduler("Wan")` is a plain Euler step on a
+flow-matching schedule, so `FlowMatchEulerDiscreteScheduler` + `EulerSolver`
+is semantically equivalent.
+
+**Scheduler caveat for `comfyui` regime.** The upstream ComfyUI workflow
+(`comfyui_workflow/SVI-Wan22-1210-{4,10}-Clips.json`) uses k-diffusion's
+`dpm++_sde` sampler. tt-metal has no on-device port of that stochastic
+2nd-order singlestep solver, so we substitute UniPC at the same
+`flow_shift=8`. Both are order-2 flow-aware solvers; output is visually
+close but not bit-exact to ComfyUI. The 4-clip variant additionally uses
+`cfg=2.0` for clips 1-2 and `cfg=1.5` for clips 3-4 — pass
+`GUIDANCE_SCALE="2.0;2.0;1.5;1.5"` (and matching `SVI_NUM_CLIPS=4`) to
+match that pattern; the 10-clip variant uses uniform `cfg=1.5` which is the
+default.
+
+## Weights
+
+### SVI 2.0 Pro LoRA
+
+- **Source:** [vita-video-gen/svi-model](https://huggingface.co/vita-video-gen/svi-model)
+- **Files (one per MoE expert):**
+ - `SVI_Wan2.2-I2V-A14B_high_noise_lora_v2.0_pro.safetensors`
+ - `SVI_Wan2.2-I2V-A14B_low_noise_lora_v2.0_pro.safetensors`
+- **Format:** PEFT-style — keys are `.lora_A.default.weight` /
+ `.lora_B.default.weight` (the trailing `.default` adapter-name
+ segment is tolerated by the loader; see
+ [Wan2_2_LoRA.md](Wan2_2_LoRA.md#supported-adapter-key-formats)).
+
+### LightX2V LoRA (required for `comfyui` regime only)
+
+- **Source:** [lightx2v/Wan2.2-Distill-Loras](https://huggingface.co/lightx2v/Wan2.2-Distill-Loras)
+- **Files:**
+ - `wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors`
+ - `wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors`
+
+### Download
+
+```bash
+python -c "
+from huggingface_hub import hf_hub_download
+for f in [
+ 'SVI_Wan2.2-I2V-A14B_high_noise_lora_v2.0_pro.safetensors',
+ 'SVI_Wan2.2-I2V-A14B_low_noise_lora_v2.0_pro.safetensors',
+]:
+ hf_hub_download('vita-video-gen/svi-model', f, local_dir='/path/to/svi')
+for f in [
+ 'wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors',
+ 'wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors',
+]:
+ hf_hub_download('lightx2v/Wan2.2-Distill-Loras', f, local_dir='/path/to/lightx2v')
+"
+```
+
+### Base Wan2.2 components (shared)
+
+- **Source:** [Wan-AI/Wan2.2-I2V-A14B-Diffusers](https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B-Diffusers)
+
+## Supported configurations
+
+| System | Mesh | Topology | Test ID |
+|---|---|---|---|
+| BH Loud Box | 2x4 | Linear | `bh_2x4sp1tp0` |
+| BH Galaxy | 4x8 | Ring | `bh_4x8sp1tp0_ring` |
+
+## How to Run
+
+### ComfyUI regime (6 steps, LightX2V-accelerated — recommended)
+
+```bash
+export TT_DIT_CACHE_DIR=/your/cache/path
+export TT_METAL_HOME=$(pwd)
+export PYTHONPATH=$(pwd)
+export TT_DIT_ALLOW_HF_DOWNLOAD=1
+
+export SVI_HIGH_PATH=/path/to/svi/SVI_Wan2.2-I2V-A14B_high_noise_lora_v2.0_pro.safetensors
+export SVI_LOW_PATH=/path/to/svi/SVI_Wan2.2-I2V-A14B_low_noise_lora_v2.0_pro.safetensors
+export LIGHTX2V_HIGH_PATH=/path/to/lightx2v/wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors
+export LIGHTX2V_LOW_PATH=/path/to/lightx2v/wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors
+
+export PROMPT_IMAGE=$TT_METAL_HOME/example.png
+export PROMPT="A subject moving naturally through the scene"
+export SVI_NUM_CLIPS=10
+
+pytest models/tt_dit/experimental/tests/test_pipeline_wan_svi.py \
+ -v -k "bh_2x4sp1tp0 and resolution_480p and comfyui" \
+ --timeout 3600 -s
+```
+
+### Python regime (50 steps, no LightX2V needed)
+
+Same `SVI_HIGH_PATH` / `SVI_LOW_PATH` exports as above; omit the LightX2V
+ones. Replace the `-k` selector with `... and python`. Expect roughly 6×
+the wall time per clip.
+
+## Configuration
+
+### Required paths
+
+- `SVI_HIGH_PATH` / `SVI_LOW_PATH`: SVI 2.0 Pro LoRA files for the high-noise
+ and low-noise Wan2.2 experts.
+- `LIGHTX2V_HIGH_PATH` / `LIGHTX2V_LOW_PATH`: LightX2V LoRA files for the
+ high-noise and low-noise experts. Required only for `regime='comfyui'`.
+
+### Normal inputs
+
+- `PROMPT_IMAGE` (default `./prompt_image.png`): the anchor image. This is the
+ first-frame reference used for every clip.
+- `PROMPT` (default golden retriever prompt): one prompt string for all clips,
+ or a semicolon-separated list with exactly `SVI_NUM_CLIPS` entries for
+ per-clip prompts. Example: `PROMPT="wide shot;close-up;camera pans left"` with
+ `SVI_NUM_CLIPS=3`.
+- `SVI_NUM_CLIPS` (default `2`): number of 81-frame clips to generate and
+ chain.
+
+### Expert knobs
+
+These are useful for matching upstream workflows, ablations, or debugging. The
+defaults are the intended starting point.
+
+- `NUM_STEPS` (default `50` for `python`, `6` for `comfyui`): denoising steps
+ per clip. Higher values are slower and usually improve quality. The `comfyui`
+ default assumes the LightX2V acceleration LoRAs are present.
+- `GUIDANCE_SCALE` (default `5.0` for `python`, `1.5` for `comfyui`): CFG for
+ the high-noise expert. Accepts either one value for all clips or a
+ semicolon-separated list of length `SVI_NUM_CLIPS`.
+- `GUIDANCE_SCALE_2` (default: matches `GUIDANCE_SCALE`): CFG for the low-noise
+ expert. Uses the same scalar or semicolon per-clip format. For the upstream
+ ComfyUI 4-clip workflow, use
+ `GUIDANCE_SCALE="2.0;2.0;1.5;1.5"` and the same value for
+ `GUIDANCE_SCALE_2`.
+- `SVI_SEED` (default `0`): base random seed. Clip `i` uses
+ `SVI_SEED + i * 42`, so each clip gets deterministic but distinct noise.
+- `SVI_NUM_MOTION_LATENT` (default `1`): number of latent time steps copied
+ from the previous clip into the next clip's conditioning. Increasing this can
+ strengthen cross-clip continuity but may make the next clip follow the
+ previous motion more tightly. Setting it to `0` disables the SVI Pro latent
+ handoff and leaves only the anchor image.
+- `SVI_NUM_OVERLAP_FRAME` (default `4`): number of decoded frames dropped from
+ the start of every clip after the first when concatenating the final video.
+ This removes duplicate or unstable boundary frames; it does not affect latent
+ conditioning.
+
+## Limitations / open items
+
+- **`comfyui` regime is not bit-exact to upstream** — see the scheduler
+ caveat above. A native `DPMSolverSDESolver` is the follow-up that would
+ close the gap.
+- **Anchor encoding differs slightly from upstream.** Our implementation
+ reuses `WanPipelineI2V.prepare_latents`, which encodes an anchor-at-frame-0 /
+ zero-elsewhere video and then overwrites the non-anchor latents with the
+ motion-latent splice and zero-padding. Upstream's `wan_video_svi_pro`
+ encodes only the single anchor frame and concatenates. Both produce the
+ same anchor latent at frame 0 and zeros / motion at the rest, but the
+ encode path is not byte-identical.
+- **CPU-side LoRA fusion** (~5–10 s per expert at cold cache, scales with
+ stack depth). Switching between LoRA stacks across runs rebuilds the TT
+ cache; runtime LoRA scale tuning or mid-clip switching is not supported.
+- **Per-clip latency** (BH-LB 2x4, 480p, with anchor VAE-encode cache hit
+ after clip 1): `comfyui` regime ~63 s/clip; `python` regime not yet
+ measured end-to-end on this branch.
+- `bh_2x4sp1tp0` (BH-LB) is the primary tested config; `bh_4x8sp1tp0_ring`
+ (Galaxy) is parameterized but not yet exercised.
diff --git a/code/models/tt_dit/experimental/pipelines/__init__.py b/code/models/tt_dit/experimental/pipelines/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/code/models/tt_dit/experimental/pipelines/pipeline_anisora.py b/code/models/tt_dit/experimental/pipelines/pipeline_anisora.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb50da20d7ec321331a8e40f3cd43b5866f1ca8c
--- /dev/null
+++ b/code/models/tt_dit/experimental/pipelines/pipeline_anisora.py
@@ -0,0 +1,118 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""IndexTeam/Index-anisora V3.2 I2V pipeline.
+
+A thin subclass of :class:`WanPipelineI2V` that:
+
+1. Uses ``Wan-AI/Wan2.2-I2V-A14B-Diffusers`` for tokenizer, text encoder, VAE,
+ and scheduler — the AniSora HF repo only ships the DiT experts.
+2. Replaces both expert transformer state dicts with AniSora's ``V3.2``
+ ``diffusion_pytorch_model.safetensors`` files (high-noise + low-noise pair).
+3. Defaults to 40 sampling steps with ``boundary_ratio=0.9`` and
+ ``guidance_scale=3.5`` for both stages, matching the upstream config in
+ ``anisoraV3.2/wan/configs/wan_i2v_A14B.py``.
+
+AniSora's safetensors use the same original-Wan key naming as the lightx2v
+distill checkpoints (verified: 1095 keys, identical layout). The
+``wan_lightx2v_to_diffusers_key`` rename function in
+:mod:`models.tt_dit.utils.lightx2v_loader` is reused as-is.
+
+A ``random_weights`` mode is provided for smoke-testing without HuggingFace
+downloads. It (a) skips the two ~28 GB transformer subfolder downloads by
+constructing config-only random ``TorchWanTransformer3DModel`` instances and
+(b) feeds those random ``state_dict()``s into the TT model in place of the
+AniSora safetensors. Tokenizer, text encoder, VAE, and scheduler still come
+from the base diffusers repo (~12 GB total).
+"""
+from __future__ import annotations
+
+import contextlib
+import os
+from typing import TYPE_CHECKING
+
+import ttnn
+from models.tt_dit.experimental.pipelines.pipeline_wan_distill import _patch_torch_transformer_random
+from models.tt_dit.experimental.utils.lightx2v_loader import load_lightx2v_state_dict
+from models.tt_dit.pipelines.wan.pipeline_wan import WanPipelineConfig
+from models.tt_dit.pipelines.wan.pipeline_wan_i2v import WanPipelineI2V
+from models.tt_dit.utils import cache
+
+if TYPE_CHECKING:
+ from diffusers.schedulers import SchedulerMixin
+
+
+class AniSoraPipeline(WanPipelineI2V):
+ HF_REPO = "IndexTeam/Index-anisora"
+ HIGH_NOISE_FILE = "V3.2/high_noise_model/diffusion_pytorch_model.safetensors"
+ LOW_NOISE_FILE = "V3.2/low_noise_model/diffusion_pytorch_model.safetensors"
+ CACHE_NAMESPACE = "Index-anisora-V3.2"
+ RANDOM_CACHE_NAMESPACE = "Index-anisora-random"
+ ANISORA_BOUNDARY_RATIO = 0.9
+
+ @classmethod
+ def _config_overrides(cls) -> dict[str, object]:
+ # Matches upstream anisoraV3.2/wan/configs/wan_i2v_A14B.py.
+ return {**super()._config_overrides(), "boundary_ratio": cls.ANISORA_BOUNDARY_RATIO}
+
+ def __init__(
+ self,
+ *,
+ device: ttnn.MeshDevice,
+ config: WanPipelineConfig,
+ anisora_local_dir: str | None = None,
+ allow_download: bool | None = None,
+ random_weights: bool | None = None,
+ scheduler: SchedulerMixin | None = None,
+ run_warmup: bool = True,
+ lora_enabled: bool = False,
+ ) -> None:
+ if allow_download is None:
+ allow_download = os.environ.get("TT_DIT_ALLOW_HF_DOWNLOAD") == "1"
+ if anisora_local_dir is None:
+ anisora_local_dir = os.environ.get("ANISORA_LOCAL_DIR")
+ if random_weights is None:
+ random_weights = os.environ.get("TT_DIT_RANDOM_WEIGHTS") == "1"
+
+ self._anisora_local_dir = anisora_local_dir
+ self._allow_download = allow_download
+ self._random_weights = random_weights
+
+ ctx = _patch_torch_transformer_random() if random_weights else contextlib.nullcontext()
+ with ctx:
+ super().__init__(
+ device=device, config=config, scheduler=scheduler, run_warmup=run_warmup, lora_enabled=lora_enabled
+ )
+
+ def _prepare_transformer(self, idx: int):
+ state = self.transformer_states[idx]
+ if self._random_weights:
+ cache.load_model(
+ state.model,
+ model_name=self.RANDOM_CACHE_NAMESPACE,
+ subfolder=state.checkpoint.subfolder,
+ parallel_config=self.parallel_config,
+ mesh_shape=tuple(self.mesh_device.shape),
+ mesh_device=self.mesh_device,
+ is_fsdp=self.is_fsdp,
+ get_torch_state_dict=lambda s=state: s.checkpoint.state_dict(),
+ )
+ return
+
+ filename = self.HIGH_NOISE_FILE if idx == 0 else self.LOW_NOISE_FILE
+ cache.load_model(
+ state.model,
+ model_name=self.CACHE_NAMESPACE,
+ subfolder=state.checkpoint.subfolder,
+ parallel_config=self.parallel_config,
+ mesh_shape=tuple(self.mesh_device.shape),
+ mesh_device=self.mesh_device,
+ is_fsdp=self.is_fsdp,
+ get_torch_state_dict=lambda f=filename: load_lightx2v_state_dict(
+ self.HF_REPO,
+ f,
+ allow_download=self._allow_download,
+ local_dir=self._anisora_local_dir,
+ ),
+ )
diff --git a/code/models/tt_dit/experimental/pipelines/pipeline_wan_distill.py b/code/models/tt_dit/experimental/pipelines/pipeline_wan_distill.py
new file mode 100644
index 0000000000000000000000000000000000000000..17681213901417f9a507a324810f5cc1afe40f68
--- /dev/null
+++ b/code/models/tt_dit/experimental/pipelines/pipeline_wan_distill.py
@@ -0,0 +1,181 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Wan2.2 lightx2v distilled I2V pipeline.
+
+A thin subclass of :class:`WanPipelineI2V` that:
+
+1. Uses ``Wan-AI/Wan2.2-I2V-A14B-Diffusers`` for tokenizer, text encoder, VAE,
+ and scheduler — the lightx2v repo ships only the DiT.
+2. Replaces both expert transformer state dicts with lightx2v's flat
+ ``.safetensors`` files (4-step distill, high-noise + low-noise pair).
+3. Defaults ``boundary_ratio=0.5`` (2 high-noise + 2 low-noise steps).
+
+Test/caller passes ``num_inference_steps=4`` and ``guidance_scale=1.0`` for both
+stages since CFG is baked into the distill.
+
+A ``random_weights`` mode is provided for smoke-testing without HuggingFace
+downloads. It (a) skips the two ~28 GB transformer subfolder downloads by
+constructing config-only random ``TorchWanTransformer3DModel`` instances and
+(b) feeds those random ``state_dict()``s into the TT model in place of the
+lightx2v safetensors. Tokenizer, text encoder, VAE, and scheduler still come
+from the base diffusers repo (~12 GB total).
+"""
+from __future__ import annotations
+
+import contextlib
+import os
+from typing import TYPE_CHECKING
+
+import torch
+
+import ttnn
+from models.tt_dit.experimental.utils.lightx2v_loader import load_lightx2v_state_dict
+from models.tt_dit.models.transformers.wan2_2.transformer_wan import TorchWanTransformer3DModel
+from models.tt_dit.pipelines.wan.pipeline_wan import WanPipelineConfig
+from models.tt_dit.pipelines.wan.pipeline_wan_i2v import WanPipelineI2V
+from models.tt_dit.utils import cache
+
+if TYPE_CHECKING:
+ from diffusers.schedulers import SchedulerMixin
+
+# Hard-coded config for Wan2.2-I2V-A14B-Diffusers transformer subfolders. Used
+# only in random_weights mode so we don't have to fetch transformer/config.json
+# from HF. Both transformer and transformer_2 share this architecture; only
+# weights differ between high-noise and low-noise experts.
+_RANDOM_I2V_TRANSFORMER_CONFIG = dict(
+ patch_size=(1, 2, 2),
+ num_attention_heads=40,
+ attention_head_dim=128,
+ in_channels=36, # I2V: 4 mask + 16 image latent + 16 noise latent
+ out_channels=16,
+ text_dim=4096,
+ freq_dim=256,
+ ffn_dim=13824,
+ num_layers=40,
+ cross_attn_norm=True,
+ qk_norm="rms_norm_across_heads",
+ eps=1e-6,
+ rope_max_seq_len=1024,
+ image_dim=None,
+ added_kv_proj_dim=None,
+ pos_embed_seq_len=None,
+)
+
+
+@contextlib.contextmanager
+def _patch_torch_transformer_random(seed: int = 0):
+ """Replace ``TorchWanTransformer3DModel.from_pretrained`` with a stub that
+ instantiates from a hard-coded I2V config and never touches the network or
+ disk. Restored on exit."""
+ cls = TorchWanTransformer3DModel
+ sentinel = object()
+ saved = cls.__dict__.get("from_pretrained", sentinel)
+
+ def _stub(_cls, *args, **kwargs):
+ with torch.random.fork_rng():
+ torch.manual_seed(seed)
+ return _cls(**_RANDOM_I2V_TRANSFORMER_CONFIG)
+
+ cls.from_pretrained = classmethod(_stub)
+ try:
+ yield
+ finally:
+ if saved is sentinel:
+ # `from_pretrained` was inherited; delete our override to restore inheritance.
+ try:
+ delattr(cls, "from_pretrained")
+ except AttributeError:
+ pass
+ else:
+ cls.from_pretrained = saved
+
+
+class WanDistillPipelineI2V(WanPipelineI2V):
+ LIGHTX2V_REPO = "lightx2v/Wan2.2-Distill-Models"
+ HIGH_NOISE_FILE = "wan2.2_i2v_A14b_high_noise_lightx2v_4step.safetensors"
+ LOW_NOISE_FILE = "wan2.2_i2v_A14b_low_noise_lightx2v_4step.safetensors"
+ CACHE_NAMESPACE = "Wan2.2-Distill-lightx2v-4step"
+ RANDOM_CACHE_NAMESPACE = "Wan2.2-Distill-random"
+ DISTILL_BOUNDARY_RATIO = 0.5
+
+ @classmethod
+ def _config_overrides(cls) -> dict[str, object]:
+ # The 4-step distill splits 2 high-noise + 2 low-noise steps.
+ return {**super()._config_overrides(), "boundary_ratio": cls.DISTILL_BOUNDARY_RATIO}
+
+ def __init__(
+ self,
+ *,
+ device: ttnn.MeshDevice,
+ config: WanPipelineConfig,
+ lightx2v_local_dir: str | None = None,
+ allow_download: bool | None = None,
+ random_weights: bool | None = None,
+ scheduler: SchedulerMixin | None = None,
+ run_warmup: bool = True,
+ lora_enabled: bool = False,
+ ) -> None:
+ if allow_download is None:
+ allow_download = os.environ.get("TT_DIT_ALLOW_HF_DOWNLOAD") == "1"
+ if lightx2v_local_dir is None:
+ lightx2v_local_dir = os.environ.get("LIGHTX2V_LOCAL_DIR")
+ if random_weights is None:
+ random_weights = os.environ.get("TT_DIT_RANDOM_WEIGHTS") == "1"
+
+ self._lightx2v_local_dir = lightx2v_local_dir
+ self._allow_download = allow_download
+ self._random_weights = random_weights
+
+ ctx = _patch_torch_transformer_random() if random_weights else contextlib.nullcontext()
+ with ctx:
+ super().__init__(
+ device=device, config=config, scheduler=scheduler, run_warmup=run_warmup, lora_enabled=lora_enabled
+ )
+
+ def prepare_text_conditioning(self, tt_model, prompt_embeds, buffer, traced=False):
+ # When CFG is baked in (guidance_scale=1.0), encode_prompt returns
+ # negative_prompt_embeds=None. The base loop still calls this for the
+ # negative buffer; forwarding None into the text embedder hits a
+ # NoneType.padded_shape in Linear. combined_step already short-circuits
+ # on do_classifier_free_guidance=False, so leaving the buffer as-is is
+ # safe.
+ if prompt_embeds is None:
+ return buffer
+ return super().prepare_text_conditioning(tt_model, prompt_embeds, buffer, traced)
+
+ def _prepare_transformer(self, idx: int):
+ state = self.transformer_states[idx]
+ if self._random_weights:
+ # Use the (random) state_dict the WanCheckpoint loaded under the
+ # _patch_torch_transformer_random monkey-patch. Cache under a
+ # separate namespace so a real-weights run doesn't reuse it.
+ cache.load_model(
+ state.model,
+ model_name=self.RANDOM_CACHE_NAMESPACE,
+ subfolder=state.checkpoint.subfolder,
+ parallel_config=self.parallel_config,
+ mesh_shape=tuple(self.mesh_device.shape),
+ mesh_device=self.mesh_device,
+ is_fsdp=self.is_fsdp,
+ get_torch_state_dict=lambda s=state: s.checkpoint.state_dict(),
+ )
+ return
+
+ filename = self.HIGH_NOISE_FILE if idx == 0 else self.LOW_NOISE_FILE
+ cache.load_model(
+ state.model,
+ model_name=self.CACHE_NAMESPACE,
+ subfolder=state.checkpoint.subfolder,
+ parallel_config=self.parallel_config,
+ mesh_shape=tuple(self.mesh_device.shape),
+ mesh_device=self.mesh_device,
+ is_fsdp=self.is_fsdp,
+ get_torch_state_dict=lambda f=filename: load_lightx2v_state_dict(
+ self.LIGHTX2V_REPO,
+ f,
+ allow_download=self._allow_download,
+ local_dir=self._lightx2v_local_dir,
+ ),
+ )
diff --git a/code/models/tt_dit/experimental/pipelines/pipeline_wan_lora.py b/code/models/tt_dit/experimental/pipelines/pipeline_wan_lora.py
new file mode 100644
index 0000000000000000000000000000000000000000..90d6e00e5c61d896da7f7c27570a8bc13650d0f7
--- /dev/null
+++ b/code/models/tt_dit/experimental/pipelines/pipeline_wan_lora.py
@@ -0,0 +1,414 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Wan2.2 I2V pipeline with LoRA adapters fused into the base weights.
+
+Each expert (high/low noise) takes an ordered LoRA stack; stacks are fused
+on CPU before TT conversion so inference has no LoRA-specific runtime cost.
+See ``experimental/models/Wan2_2_LoRA.md`` for the adapter-key formats
+detected by ``fuse_lora_state_dict`` and the supported namespaces.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import re
+from collections.abc import Sequence
+from dataclasses import dataclass
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+import torch
+from loguru import logger
+from safetensors.torch import load_file
+
+import ttnn
+from models.tt_dit.experimental.utils.lightx2v_loader import wan_lightx2v_to_diffusers_key
+from models.tt_dit.pipelines.wan.pipeline_wan import WanPipelineConfig
+from models.tt_dit.pipelines.wan.pipeline_wan_i2v import WanPipelineI2V
+from models.tt_dit.solvers.factory import CustomSigmaScheduler
+from models.tt_dit.utils import cache
+
+if TYPE_CHECKING:
+ from diffusers.schedulers import SchedulerMixin
+
+
+@dataclass(frozen=True)
+class LoRASpec:
+ """A single LoRA adapter file and its blend strength."""
+
+ path: str
+ scale: float = 1.0
+
+
+LoRAArg = LoRASpec | str | Sequence[LoRASpec | str] | None
+
+
+@dataclass(frozen=True)
+class _FusionStats:
+ applied_pairs: int = 0
+ applied_direct: int = 0
+ skipped_unknown: int = 0
+ skipped_unmapped: int = 0
+ skipped_shape_mismatch: int = 0
+
+ @property
+ def applied(self) -> int:
+ return self.applied_pairs + self.applied_direct
+
+
+def _normalize_lora_arg(arg: LoRAArg) -> list[LoRASpec]:
+ """Coerce None / path / LoRASpec / sequence-of-either into LoRASpecs."""
+ if arg is None:
+ return []
+ if isinstance(arg, LoRASpec):
+ return [arg]
+ if isinstance(arg, str):
+ return [LoRASpec(arg)]
+ if not isinstance(arg, Sequence):
+ raise TypeError(f"Expected LoRASpec, str, sequence, or None; got {type(arg).__name__}")
+ out: list[LoRASpec] = []
+ for item in arg:
+ if isinstance(item, LoRASpec):
+ out.append(item)
+ elif isinstance(item, str):
+ out.append(LoRASpec(item))
+ else:
+ raise TypeError(f"Expected LoRASpec or str in LoRA list, got {type(item).__name__}")
+ return out
+
+
+_STRIP_PREFIXES = ("diffusion_model.", "transformer.", "unet.", "model.")
+
+# PEFT may insert an adapter name before ``.weight``, e.g. ``lora_A.default.weight``.
+_LOW_RANK_RE = re.compile(r"^(?P.*)\.lora_(?PA|B|down|up)(?:\.[^.]+)?\.weight$")
+_SLOT_MAP = {"A": "A", "down": "A", "B": "B", "up": "B"}
+
+
+def _strip_known_prefixes(key: str) -> str:
+ for prefix in _STRIP_PREFIXES:
+ if key.startswith(prefix):
+ return key[len(prefix) :]
+ return key
+
+
+def _kohya_to_lightx2v(key: str) -> str:
+ """Convert kohya/A1111-style keys to lightx2v-style keys.
+
+ ``lora_unet_blocks_0_cross_attn_k.lora_down.weight``
+ → ``blocks.0.cross_attn.k.lora_down.weight``
+ """
+ if not key.startswith("lora_unet_"):
+ return key
+ parts = key.split(".", 1)
+ module_path = parts[0][len("lora_unet_") :]
+ suffix = f".{parts[1]}" if len(parts) > 1 else ""
+
+ m = re.match(r"blocks_(\d+)_(cross_attn|self_attn)_([a-z]+)", module_path)
+ if m:
+ return f"blocks.{m.group(1)}.{m.group(2)}.{m.group(3)}{suffix}"
+
+ m = re.match(r"blocks_(\d+)_(ffn)_(\d+)", module_path)
+ if m:
+ return f"blocks.{m.group(1)}.{m.group(2)}.{m.group(3)}{suffix}"
+
+ logger.warning(f"Unrecognized kohya key structure: {key}")
+ return key
+
+
+def _diffusers_target(lightx2v_base_path: str, suffix: str) -> str:
+ """Map ``.weight``/``.bias`` to the diffusers parameter key."""
+ weight_key = wan_lightx2v_to_diffusers_key(f"{lightx2v_base_path}.weight")
+ if suffix == ".weight":
+ return weight_key
+ return weight_key[: -len(".weight")] + suffix
+
+
+def _is_lora_key(raw_key: str) -> bool:
+ key = _kohya_to_lightx2v(_strip_known_prefixes(raw_key))
+ return bool(_LOW_RANK_RE.match(key)) or key.endswith((".diff", ".diff_b"))
+
+
+def _has_lora_keys(state_dict: dict[str, torch.Tensor]) -> bool:
+ """Return True iff the safetensors file contains at least one LoRA-style key."""
+ return any(_is_lora_key(k) for k in state_dict)
+
+
+def fuse_lora_state_dict(
+ base_state_dict: dict[str, torch.Tensor],
+ lora_state_dict: dict[str, torch.Tensor],
+ *,
+ scale: float = 1.0,
+ return_stats: bool = False,
+) -> dict[str, torch.Tensor] | tuple[dict[str, torch.Tensor], _FusionStats]:
+ """Return a fused state dict, optionally with stats describing what applied.
+
+ The base dict is not mutated; entries that were touched are fresh tensors
+ so the caller can chain fusions across a stack safely. See
+ ``experimental/models/Wan2_2_LoRA.md`` for the supported adapter key
+ formats. Raises ``KeyError`` when a low-rank pair is missing one half.
+ """
+ pairs: dict[str, dict[str, torch.Tensor]] = {}
+ direct_deltas: list[tuple[str, str, torch.Tensor]] = [] # (base_path, suffix, tensor)
+ alphas: dict[str, float] = {}
+ skipped_unknown: list[str] = []
+
+ for raw_key, tensor in lora_state_dict.items():
+ key = _kohya_to_lightx2v(_strip_known_prefixes(raw_key))
+ m = _LOW_RANK_RE.match(key)
+ if m:
+ pairs.setdefault(m.group("base"), {})[_SLOT_MAP[m.group("slot")]] = tensor
+ continue
+ if key.endswith(".diff_b"):
+ direct_deltas.append((key[: -len(".diff_b")], ".bias", tensor))
+ elif key.endswith(".diff"):
+ direct_deltas.append((key[: -len(".diff")], ".weight", tensor))
+ elif key.endswith(".alpha"):
+ alphas[key[: -len(".alpha")]] = tensor.item()
+ else:
+ skipped_unknown.append(raw_key)
+
+ if skipped_unknown:
+ logger.warning(
+ f"LoRA fusion: {len(skipped_unknown)} unrecognized keys ignored. Examples: {skipped_unknown[:5]}"
+ )
+
+ fused = dict(base_state_dict)
+ applied_pairs = 0
+ skipped_unmapped: list[str] = []
+
+ for base_path, ab in pairs.items():
+ if "A" not in ab or "B" not in ab:
+ raise KeyError(f"LoRA pair incomplete for '{base_path}': have {list(ab)}")
+ diffusers_key = _diffusers_target(base_path, ".weight")
+ if diffusers_key not in fused:
+ skipped_unmapped.append(diffusers_key)
+ continue
+ base_weight = fused[diffusers_key]
+ rank = ab["A"].shape[0]
+ alpha = alphas.get(base_path, float(rank))
+ effective_scale = scale * (alpha / rank)
+ delta = effective_scale * (ab["B"].to(torch.float32) @ ab["A"].to(torch.float32))
+ # ``+`` always allocates a new tensor; avoids the .to(fp32)+.add_()
+ # alias trap when base_weight is already fp32 (would mutate the
+ # caller's dict on a subsequent stacked-fusion pass).
+ fused[diffusers_key] = (base_weight.float() + delta).to(base_weight.dtype)
+ applied_pairs += 1
+
+ applied_direct = 0
+ skipped_shape_mismatch = 0
+ for base_path, suffix, tensor in direct_deltas:
+ diffusers_key = _diffusers_target(base_path, suffix)
+ if diffusers_key not in fused:
+ skipped_unmapped.append(diffusers_key)
+ continue
+ base = fused[diffusers_key]
+ if base.shape != tensor.shape:
+ logger.warning(
+ f"LoRA direct delta shape mismatch for '{diffusers_key}': "
+ f"base {tuple(base.shape)} vs delta {tuple(tensor.shape)}; skipping."
+ )
+ skipped_shape_mismatch += 1
+ continue
+ fused[diffusers_key] = (base.float() + scale * tensor.to(torch.float32)).to(base.dtype)
+ applied_direct += 1
+
+ if skipped_unmapped:
+ logger.warning(
+ f"LoRA fusion: {len(skipped_unmapped)} adapter targets not present in base; skipped. "
+ f"Examples: {skipped_unmapped[:5]}"
+ )
+
+ logger.info(f"Fused {applied_pairs} low-rank pairs and {applied_direct} direct deltas (scale={scale})")
+ stats = _FusionStats(
+ applied_pairs=applied_pairs,
+ applied_direct=applied_direct,
+ skipped_unknown=len(skipped_unknown),
+ skipped_unmapped=len(skipped_unmapped),
+ skipped_shape_mismatch=skipped_shape_mismatch,
+ )
+ return (fused, stats) if return_stats else fused
+
+
+def verify_fusion_changed_weights(
+ base_sd: dict[str, torch.Tensor],
+ fused_sd: dict[str, torch.Tensor],
+ *,
+ min_changed: int = 3,
+ label: str,
+) -> None:
+ """Sanity check that at least ``min_changed`` weights actually differ post-fusion.
+
+ Catches the canonical "LoRA silently failed to apply" failure mode where
+ a key-mapping bug leaves every fused target absent from the base. Logs a
+ sample of changed tensors with their L2 diff for visibility.
+
+ Raises ``RuntimeError`` if no weights changed at all, or if fewer than
+ ``min_changed`` weights changed (likely a partial load).
+ """
+ changed: list[tuple[str, float]] = []
+ max_diff = 0.0
+ for k, base in base_sd.items():
+ fused = fused_sd.get(k)
+ if fused is None or fused.shape != base.shape:
+ continue
+ if fused.data_ptr() == base.data_ptr():
+ continue
+ diff = (fused.to(torch.float32) - base.to(torch.float32)).norm().item()
+ if diff > 0.0:
+ changed.append((k, diff))
+ max_diff = max(max_diff, diff)
+
+ if max_diff == 0.0:
+ raise RuntimeError(
+ f"LoRA silently failed to apply -- weights are unchanged for '{label}'. "
+ f"Verified {len(base_sd)} keys against fused dict; max L2 diff is 0.0."
+ )
+
+ sample = changed[:5]
+ logger.info(
+ f"LoRA fusion verified for '{label}': {len(changed)} tensors changed, "
+ f"max L2 diff={max_diff:.4f}. Sample diffs:"
+ )
+ for k, d in sample:
+ logger.info(f" {k}: L2={d:.4f}")
+
+ if len(changed) < min_changed:
+ raise RuntimeError(
+ f"LoRA fusion changed only {len(changed)} weights for '{label}' "
+ f"(require >= {min_changed}). Likely indicates partial LoRA load."
+ )
+
+
+def _lora_stack_cache_namespace(specs_by_expert: dict[int, list[LoRASpec]]) -> str:
+ """Hash ordered ``(resolved_path, scale)`` per expert so distinct stacks cache separately."""
+ h = hashlib.sha1()
+ for idx in sorted(specs_by_expert.keys()):
+ h.update(f"expert_{idx}\x00".encode())
+ for spec in specs_by_expert[idx]:
+ h.update(str(Path(spec.path).resolve()).encode())
+ h.update(b"\x00")
+ h.update(f"{spec.scale:.6f}".encode())
+ h.update(b"\x00")
+ return f"Wan2.2-I2V-LoRA-{h.hexdigest()[:12]}"
+
+
+class WanPipelineI2VLora(WanPipelineI2V):
+ """Wan2.2 I2V with LoRA stacks fused into the base PyTorch weights.
+
+ Adapters distilled to be guidance-free want ``cfg_enabled=False``, which halves the
+ denoising cost by dropping the unconditional pass. With it off the caller must keep both
+ guidance scales at 1.0, which ``__call__`` enforces.
+ """
+
+ def __init__(
+ self,
+ *,
+ device: ttnn.MeshDevice,
+ config: WanPipelineConfig,
+ lora_high: LoRAArg = None,
+ lora_low: LoRAArg = None,
+ scheduler: SchedulerMixin | None = None,
+ run_warmup: bool = True,
+ lora_enabled: bool = False,
+ ) -> None:
+ high_specs = _normalize_lora_arg(lora_high)
+ low_specs = _normalize_lora_arg(lora_low)
+
+ if not high_specs and not low_specs:
+ raise ValueError(
+ "WanPipelineI2VLora requires at least one LoRA. "
+ "Pass lora_high and/or lora_low as a path, LoRASpec, or list."
+ )
+
+ for label, specs in [("lora_high", high_specs), ("lora_low", low_specs)]:
+ for spec in specs:
+ if not Path(spec.path).is_file():
+ raise FileNotFoundError(f"{label}: file does not exist: {spec.path}")
+
+ self._lora_specs: dict[int, list[LoRASpec]] = {0: high_specs, 1: low_specs}
+ self._cache_namespace = _lora_stack_cache_namespace(self._lora_specs)
+ # Cleared after TT cache handoff (see _prepare_transformer) to free CPU memory.
+ self._fused_state_dicts: dict[int, dict[str, torch.Tensor] | None] = {0: None, 1: None}
+ self.custom_sigmas = None
+
+ if scheduler is None:
+ scheduler = CustomSigmaScheduler()
+ self.custom_sigmas = [1.0, 0.9375, 0.8333333, 0.625, 0.0] # default for Wan 2.2 I2V Lightning
+ logger.info(
+ f"WanPipelineI2VLora: default Euler scheduler with custom sigmas {self.custom_sigmas} -- (Wan 2.2 I2V Lightning)"
+ )
+
+ super().__init__(
+ device=device, config=config, scheduler=scheduler, run_warmup=run_warmup, lora_enabled=lora_enabled
+ )
+
+ def prepare_text_conditioning(self, tt_model, prompt_embeds, buffer, traced=False):
+ # guidance_scale=1.0 → encoder returns None for negative embeds; combined_step
+ # skips CFG so the untouched buffer is fine.
+ if prompt_embeds is None:
+ return buffer
+ return super().prepare_text_conditioning(tt_model, prompt_embeds, buffer, traced)
+
+ def prepare_schedule(self, num_inference_steps: int, *, flow_shift: float | None = None) -> None:
+ if self._solver.scheduler is None:
+ self._solver.set_schedule(sigmas=self.custom_sigmas)
+ else:
+ super().prepare_schedule(num_inference_steps, flow_shift=flow_shift)
+
+ def _build_fused_state_dict(self, idx: int) -> dict[str, torch.Tensor] | None:
+ specs = self._lora_specs[idx]
+ state = self.transformer_states[idx]
+ subfolder = state.checkpoint.subfolder
+ if not specs:
+ logger.info(f"No LoRA for expert idx={idx} ('{subfolder}') -- using base weights")
+ return None
+
+ base_sd = state.checkpoint.state_dict()
+ fused_sd = base_sd
+ for spec in specs:
+ logger.info(f"Loading LoRA for '{subfolder}' from {spec.path} (scale={spec.scale})")
+ lora_sd = load_file(str(spec.path))
+ if not _has_lora_keys(lora_sd):
+ raise RuntimeError(
+ f"No LoRA-style keys (lora_A/lora_B, lora_down/lora_up, diff/diff_b) found in {spec.path}"
+ )
+ previous_sd = fused_sd
+ fused_sd, stats = fuse_lora_state_dict(previous_sd, lora_sd, scale=spec.scale, return_stats=True)
+ label = f"{subfolder}: {Path(spec.path).name}"
+ if stats.applied == 0:
+ raise RuntimeError(
+ f"LoRA fusion applied no tensors for '{label}'. "
+ f"Skipped unmapped={stats.skipped_unmapped}, unknown={stats.skipped_unknown}, "
+ f"shape_mismatch={stats.skipped_shape_mismatch}."
+ )
+ verify_fusion_changed_weights(previous_sd, fused_sd, label=label)
+ return fused_sd
+
+ def _prepare_transformer(self, idx: int):
+ state = self.transformer_states[idx]
+
+ if not self._lora_specs[idx]:
+ super()._prepare_transformer(idx)
+ return
+
+ def _get_state_dict(idx_=idx):
+ cached = self._fused_state_dicts.get(idx_)
+ if cached is not None:
+ return cached
+ sd = self._build_fused_state_dict(idx_)
+ self._fused_state_dicts[idx_] = sd
+ return sd
+
+ cache.load_model(
+ state.model,
+ model_name=self._cache_namespace,
+ subfolder=state.checkpoint.subfolder,
+ parallel_config=self.parallel_config,
+ mesh_shape=tuple(self.mesh_device.shape),
+ mesh_device=self.mesh_device,
+ is_fsdp=self.is_fsdp,
+ get_torch_state_dict=_get_state_dict,
+ )
+ self._fused_state_dicts[idx] = None
diff --git a/code/models/tt_dit/experimental/pipelines/pipeline_wan_runtime_lora.py b/code/models/tt_dit/experimental/pipelines/pipeline_wan_runtime_lora.py
new file mode 100644
index 0000000000000000000000000000000000000000..870b330c5c9303d5f183967a986e16f9f473aca7
--- /dev/null
+++ b/code/models/tt_dit/experimental/pipelines/pipeline_wan_runtime_lora.py
@@ -0,0 +1,267 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Wan 2.2 pipeline with runtime-swappable LoRA adapters.
+
+Constructs the transformer with ``lora_enabled=True`` so every attention
+and FFN Linear is a LoRA-aware variant. Exposes a small API for
+registering and switching adapters at runtime:
+
+ pipeline.register_lora(name, high_path, low_path, scale) → AdapterHandle
+ pipeline.set_active_lora(handle | None)
+ pipeline.unregister_lora(handle)
+ pipeline.list_loras()
+
+Adapter swap merges the active LoRA delta directly into the base weight:
+``W += scale * A.T @ B.T`` per LoRA Linear, on device, with W's sharding.
+Forward-time LoRA overhead is zero — the captured trace runs the
+un-modified base path. Swap cost is bounded to ~one small matmul + an
+in-place add per LoRA Linear; sub-second end to end. See
+``models/tt_dit/layers/lora.py`` for the merge details.
+
+Out of scope for v0:
+ - Stacking multiple adapters into a single bound slot (callers wanting
+ stacked LoRAs should pre-merge offline, or use the older CPU-fusion
+ pipeline at ``pipeline_wan_lora.py``).
+ - ``.diff`` / ``.diff_b`` direct deltas (the loader warns and skips).
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from pathlib import Path
+
+from loguru import logger
+
+from models.tt_dit.experimental.lora.adapter_loader import AdapterHandle, load_adapter_into
+from models.tt_dit.layers.lora import LoRAMixin
+from models.tt_dit.pipelines.wan.pipeline_wan import WanPipeline
+from models.tt_dit.pipelines.wan.pipeline_wan_i2v import WanPipelineI2V
+
+
+@dataclass
+class _RegisteredLoRA:
+ """Per-pipeline record of a registered adapter pair."""
+
+ name: str
+ handle_high: AdapterHandle | None
+ handle_low: AdapterHandle | None
+
+
+@dataclass
+class CombinedAdapterHandle:
+ """Returned by `register_lora`. Knows about both experts."""
+
+ name: str
+ high: AdapterHandle | None
+ low: AdapterHandle | None
+ rank: int = field(init=False)
+
+ def __post_init__(self):
+ ranks = [h.rank for h in (self.high, self.low) if h is not None]
+ if not ranks:
+ raise ValueError(f"adapter '{self.name}' loaded for neither expert")
+ self.rank = max(ranks)
+
+
+def _iter_lora_modules(module):
+ """Yield every LoRAMixin-bearing submodule of `module`, recursively."""
+ if isinstance(module, LoRAMixin):
+ yield module
+ for _, child in module.named_children():
+ yield from _iter_lora_modules(child)
+
+
+class _LoRAPipelineMixin:
+ """Implements the LoRA swap API on top of a WanPipeline subclass.
+
+ Assumes the host pipeline was constructed with ``lora_enabled=True``
+ so both ``self.transformer`` and ``self.transformer_2`` have LoRA-aware
+ Linear modules throughout.
+ """
+
+ def __init__(self, *args, lora_enabled: bool = True, **kwargs):
+ # Initialize LoRA state *before* super().__init__() because the base
+ # pipeline init calls self._prepare_transformer() during warmup, and
+ # our override below reads self._active_adapter_name.
+ self._lora_registry: dict[str, _RegisteredLoRA] = {}
+ self._active_adapter_name: str | None = None
+ super().__init__(*args, lora_enabled=lora_enabled, **kwargs)
+
+ # Fuse-mode bind/unbind adds and subtracts the delta in bf16; under
+ # dynamic_load=True the cache restore re-seeds the base every reload
+ # and drift is bounded. With dynamic_load=False the base weight is
+ # never refreshed, so W drifts by ~O(N · eps_bf16) over N swap cycles.
+ # See models/tt_dit/layers/lora.py for the snapshot+reseed escape hatch.
+ if not getattr(self, "dynamic_load", True):
+ logger.warning(
+ "LoRA fuse-mode pipeline constructed with dynamic_load=False — "
+ "repeated bind/unbind cycles will accumulate bf16 quantization "
+ "drift in the base weight. For long-lived sessions, snapshot "
+ "the transformer state dict at construction and re-seed via "
+ "load_torch_state_dict when drift becomes material."
+ )
+
+ @classmethod
+ def create_pipeline(cls, **kwargs):
+ # The base factory defaults lora_enabled=False; runtime LoRA needs it on.
+ kwargs.setdefault("lora_enabled", True)
+ return super().create_pipeline(**kwargs)
+
+ def _prepare_transformer(self, idx: int):
+ """Re-apply any active LoRA delta after the transformer is reloaded.
+
+ Under dynamic_load the two transformers swap in and out of DRAM
+ between uses; each reload restores the cached *base* weights, so
+ the merged LoRA delta is wiped. We walk this transformer's LoRA
+ modules and re-merge their active adapter (no-op for modules that
+ already have the delta applied, e.g., the first load).
+ """
+ super()._prepare_transformer(idx)
+ if self._active_adapter_name is None:
+ return
+ transformer = self.transformer_states[idx].model
+ for mod in _iter_lora_modules(transformer):
+ mod.reapply_after_load()
+
+ # ---- public API ----
+ def register_lora(
+ self,
+ name: str,
+ *,
+ high_path: str | Path | None = None,
+ low_path: str | Path | None = None,
+ scale: float = 1.0,
+ ) -> CombinedAdapterHandle:
+ """Load a LoRA adapter for one or both experts.
+
+ Both ``high_path`` and ``low_path`` are independently optional — pass
+ either or both. Adapter weights are parsed from disk and stored on the
+ host LoRA bank of every applicable Linear in the respective transformer;
+ device-side merge happens later in ``set_active_lora`` (fuse mode) or
+ in the layer's forward (runtime mode). Device-OOM therefore surfaces
+ at ``set_active_lora`` time, not here.
+ """
+ if not high_path and not low_path:
+ raise ValueError("register_lora requires high_path and/or low_path")
+ if name in self._lora_registry:
+ raise ValueError(f"LoRA name '{name}' already registered")
+
+ high_handle = None
+ low_handle = None
+
+ if high_path is not None:
+ logger.info(f"loading LoRA '{name}' high-noise from {high_path} (scale={scale})")
+ high_handle = load_adapter_into(self.transformer, str(high_path), scale=scale, name=name)
+ if low_path is not None:
+ logger.info(f"loading LoRA '{name}' low-noise from {low_path} (scale={scale})")
+ low_handle = load_adapter_into(self.transformer_2, str(low_path), scale=scale, name=name)
+
+ combined = CombinedAdapterHandle(name=name, high=high_handle, low=low_handle)
+ self._lora_registry[name] = _RegisteredLoRA(name=name, handle_high=high_handle, handle_low=low_handle)
+ return combined
+
+ def unregister_lora(self, handle: CombinedAdapterHandle | str) -> None:
+ name = handle if isinstance(handle, str) else handle.name
+ rec = self._lora_registry.pop(name, None)
+ if rec is None:
+ return
+ if self._active_adapter_name == name:
+ self.set_active_lora(None)
+ # Walk and unregister bank entries
+ for transformer, handle_ in (
+ (self.transformer, rec.handle_high),
+ (self.transformer_2, rec.handle_low),
+ ):
+ if handle_ is None:
+ continue
+ self._unregister_handle(transformer, handle_)
+
+ def set_active_lora(self, handle: CombinedAdapterHandle | str | None) -> None:
+ """Bind the named adapter as the active LoRA for both experts.
+
+ Each LoRA Linear's bind merges the new delta into the base weight
+ in-place. If a different adapter is currently active, its delta is
+ subtracted first. Passing ``None`` removes the active delta (W is
+ restored to its base value).
+ """
+ if handle is None:
+ self._unbind_all()
+ self._active_adapter_name = None
+ return
+
+ name = handle if isinstance(handle, str) else handle.name
+ rec = self._lora_registry.get(name)
+ if rec is None:
+ raise KeyError(f"no LoRA registered with name '{name}'")
+
+ self._bind_per_module(self.transformer, rec.handle_high)
+ self._bind_per_module(self.transformer_2, rec.handle_low)
+ self._active_adapter_name = name
+
+ def list_loras(self) -> list[str]:
+ return list(self._lora_registry)
+
+ def deallocate_all_loras(self) -> None:
+ """Unbind every active adapter and clear the LoRA bank on every layer.
+
+ Releases host-side adapter records across both transformers. Use this
+ on a long-running server to reset state between unrelated jobs.
+ """
+ self._unbind_all()
+ self._active_adapter_name = None
+ self._lora_registry.clear()
+ for transformer in (self.transformer, self.transformer_2):
+ for mod in _iter_lora_modules(transformer):
+ mod.deallocate_lora()
+
+ # ---- internals ----
+ def _path_to_module(self, transformer, dotted: str):
+ """Resolve a dotted path like ``blocks.0.attn1.to_qkv`` to a child module."""
+ cur = transformer
+ for part in dotted.split("."):
+ if part.isdigit():
+ cur = cur[int(part)]
+ else:
+ cur = getattr(cur, part)
+ return cur
+
+ def _bind_per_module(self, transformer, handle: AdapterHandle | None):
+ addressed: set[int] = set()
+ if handle is not None:
+ for dotted, bank_idx in handle.target_indices.items():
+ mod = self._path_to_module(transformer, dotted)
+ mod.bind_active(bank_idx)
+ addressed.add(id(mod))
+
+ # Any LoRA module not touched by this adapter (including the all-orphan
+ # case where handle is None, e.g. switching to an adapter that only
+ # covers the other expert) but currently active from a previous bind
+ # must have its delta removed.
+ for mod in _iter_lora_modules(transformer):
+ if id(mod) in addressed:
+ continue
+ if mod.is_lora_active:
+ mod.unbind_active()
+
+ def _unbind_all(self):
+ for transformer in (self.transformer, self.transformer_2):
+ for mod in _iter_lora_modules(transformer):
+ if mod.is_lora_active:
+ mod.unbind_active()
+
+ def _unregister_handle(self, transformer, handle: AdapterHandle):
+ for dotted, bank_idx in handle.target_indices.items():
+ mod = self._path_to_module(transformer, dotted)
+ mod.unregister_lora(bank_idx)
+
+
+# --------------------------------------------------------------------
+# Public pipeline classes
+# --------------------------------------------------------------------
+class WanPipelineRuntimeLoRA(_LoRAPipelineMixin, WanPipeline):
+ """T2V pipeline with runtime LoRA support."""
+
+
+class WanPipelineI2VRuntimeLoRA(_LoRAPipelineMixin, WanPipelineI2V):
+ """I2V pipeline with runtime LoRA support."""
diff --git a/code/models/tt_dit/experimental/pipelines/pipeline_wan_svi.py b/code/models/tt_dit/experimental/pipelines/pipeline_wan_svi.py
new file mode 100644
index 0000000000000000000000000000000000000000..2955eb045098fdea2d8c36d5711cc5bb1cb5631b
--- /dev/null
+++ b/code/models/tt_dit/experimental/pipelines/pipeline_wan_svi.py
@@ -0,0 +1,399 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Stable-Video-Infinity (SVI) 2.0 Pro pipeline for Wan2.2 I2V.
+
+Chains short I2V clips into long videos with latent-space continuity.
+See ``experimental/models/Wan2_2_SVI.md`` for the regime parameters,
+upstream-workflow comparison, and the documented scheduler gap.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from pathlib import Path
+from typing import List, Literal, Optional, Union
+
+import torch
+from diffusers.schedulers import FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler
+from loguru import logger
+from PIL import Image
+
+import ttnn
+from models.tt_dit.experimental.pipelines.pipeline_wan_lora import LoRASpec, WanPipelineI2VLora
+from models.tt_dit.pipelines.wan.pipeline_wan import WanPipeline, WanPipelineConfig
+from models.tt_dit.pipelines.wan.pipeline_wan_i2v import ImagePrompt
+
+Regime = Literal["python", "comfyui"]
+
+
+_REGIMES = {
+ "python": {
+ "flow_shift": 5.0,
+ "call_defaults": dict(num_inference_steps=50, guidance_scale=5.0, guidance_scale_2=5.0),
+ },
+ "comfyui": {
+ "flow_shift": 8.0,
+ "call_defaults": dict(num_inference_steps=6, guidance_scale=1.5, guidance_scale_2=1.5),
+ },
+}
+
+
+@dataclass(frozen=True)
+class _ClipSpec:
+ prompt: str
+ seed: int
+ guidance_scale: float
+ guidance_scale_2: float
+
+
+class WanPipelineSVI(WanPipelineI2VLora):
+ """Wan2.2 I2V with SVI 2.0 Pro LoRA + autoregressive clip-chaining driver."""
+
+ def __init__(
+ self,
+ *,
+ device: ttnn.MeshDevice,
+ config: WanPipelineConfig,
+ svi_high: str,
+ svi_low: str,
+ lightx2v_high: Optional[str] = None,
+ lightx2v_low: Optional[str] = None,
+ regime: Regime = "python",
+ num_motion_latent: int = 1,
+ num_overlap_frame: int = 4,
+ ) -> None:
+ if regime not in _REGIMES:
+ raise ValueError(f"regime must be one of {sorted(_REGIMES)}, got {regime!r}")
+
+ for label, path in [("svi_high", svi_high), ("svi_low", svi_low)]:
+ if not Path(path).is_file():
+ raise FileNotFoundError(f"{label}: file does not exist: {path}")
+
+ self._regime: Regime = regime
+ self._num_motion_latent = int(num_motion_latent)
+ self._num_overlap_frame = int(num_overlap_frame)
+ # Lives for the duration of one generate_long_video call; cleared in
+ # that method's finally block.
+ self._anchor_latents_cache: Optional[tuple] = None
+
+ lora_high, lora_low, scheduler = self._configure_regime(
+ regime=regime,
+ svi_high=svi_high,
+ svi_low=svi_low,
+ lightx2v_high=lightx2v_high,
+ lightx2v_low=lightx2v_low,
+ )
+
+ super().__init__(
+ device=device,
+ config=config,
+ lora_high=lora_high,
+ lora_low=lora_low,
+ scheduler=scheduler,
+ )
+
+ @staticmethod
+ def _configure_regime(
+ *,
+ regime: "Regime",
+ svi_high: str,
+ svi_low: str,
+ lightx2v_high: Optional[str],
+ lightx2v_low: Optional[str],
+ ) -> tuple[list[LoRASpec], list[LoRASpec], object]:
+ """Return ``(lora_high, lora_low, scheduler)`` per regime."""
+ shift = _REGIMES[regime]["flow_shift"]
+
+ if regime == "python":
+ if lightx2v_high is not None or lightx2v_low is not None:
+ raise ValueError("lightx2v_* are only valid in regime='comfyui'; drop them for 'python'.")
+ # Matches diffsynth's FlowMatchScheduler("Wan") — plain Euler on a
+ # flow-matching schedule, dispatched to EulerSolver downstream.
+ return (
+ [LoRASpec(svi_high, 1.0)],
+ [LoRASpec(svi_low, 1.0)],
+ FlowMatchEulerDiscreteScheduler(shift=shift),
+ )
+
+ # comfyui regime: requires LightX2V LoRAs stacked under SVI.
+ if lightx2v_high is None or lightx2v_low is None:
+ raise ValueError(
+ "regime='comfyui' requires lightx2v_high and lightx2v_low LoRA paths. "
+ "See experimental/models/Wan2_2_SVI.md for the recommended HF repo."
+ )
+ for label, path in [("lightx2v_high", lightx2v_high), ("lightx2v_low", lightx2v_low)]:
+ if not Path(path).is_file():
+ raise FileNotFoundError(f"{label}: file does not exist: {path}")
+ # Per upstream docs/svi/comfyui.md: LightX2V at 1.0 on the high-noise
+ # expert hurts SVI; 0.5 is recommended.
+ # ComfyUI workflow uses k-diffusion dpm++_sde; tt-metal has no
+ # on-device port, so we substitute UniPC at the same flow_shift.
+ # Visually close, not bit-exact.
+ return (
+ [LoRASpec(lightx2v_high, 0.5), LoRASpec(svi_high, 1.0)],
+ [LoRASpec(lightx2v_low, 1.0), LoRASpec(svi_low, 1.0)],
+ UniPCMultistepScheduler(
+ use_flow_sigmas=True,
+ prediction_type="flow_prediction",
+ flow_shift=shift,
+ ),
+ )
+
+ def __call__(
+ self,
+ *args,
+ anchor_image: Optional[Image.Image] = None,
+ prev_last_latent: Optional[torch.Tensor] = None,
+ **kwargs,
+ ):
+ self._svi_anchor_image = anchor_image
+ self._svi_prev_last_latent = prev_last_latent
+ try:
+ return super().__call__(*args, **kwargs)
+ finally:
+ self._svi_anchor_image = None
+ self._svi_prev_last_latent = None
+
+ def prepare_latents(
+ self,
+ batch_size: int,
+ image_prompt,
+ num_channels_latents: int = 16,
+ height: int = 480,
+ width: int = 832,
+ num_frames: int = 81,
+ dtype=None,
+ device=None,
+ anchor_image: Optional[Image.Image] = None,
+ prev_last_latent: Optional[torch.Tensor] = None,
+ ):
+ if anchor_image is None:
+ anchor_image = getattr(self, "_svi_anchor_image", None)
+ if prev_last_latent is None:
+ prev_last_latent = getattr(self, "_svi_prev_last_latent", None)
+
+ # WanPipelineI2V.__init__'s warmup_buffers invokes __call__ with no
+ # SVI state — defer to the plain I2V path for that one pass.
+ if anchor_image is None and prev_last_latent is None:
+ return super().prepare_latents(
+ batch_size=batch_size,
+ image_prompt=image_prompt,
+ num_channels_latents=num_channels_latents,
+ height=height,
+ width=width,
+ num_frames=num_frames,
+ dtype=dtype,
+ device=device,
+ )
+
+ anchor_pil = _as_anchor_image(anchor_image if anchor_image is not None else image_prompt)
+
+ # Cache the encoded anchor + mask across clips; the encoder is the
+ # dominant non-DiT cost per clip (~3-8s on BH 2x4) and the result is
+ # invariant under (anchor, H, W, T, dtype).
+ cache_key = (id(anchor_pil), height, width, num_frames, num_channels_latents, dtype)
+ cached = self._anchor_latents_cache
+ if cached is not None and cached[0] == cache_key:
+ tt_y_template = cached[1]
+ latents, _ = WanPipeline.prepare_latents(
+ self,
+ batch_size=batch_size,
+ num_channels_latents=num_channels_latents,
+ height=height,
+ width=width,
+ num_frames=num_frames,
+ dtype=dtype,
+ device=device,
+ )
+ tt_y = tt_y_template.clone()
+ else:
+ latents, tt_y = super().prepare_latents(
+ batch_size=batch_size,
+ image_prompt=[ImagePrompt(image=anchor_pil, frame_pos=0)],
+ num_channels_latents=num_channels_latents,
+ height=height,
+ width=width,
+ num_frames=num_frames,
+ dtype=dtype,
+ device=device,
+ )
+ # Cloned because the motion-latent splice below mutates tt_y in place.
+ self._anchor_latents_cache = (cache_key, tt_y.clone())
+
+ # tt_y layout: (B, 4 mask channels + 16 image-latent channels, T_lat, H_lat, W_lat).
+ t_lat = tt_y.shape[2]
+ n = 0
+ if prev_last_latent is not None and self._num_motion_latent > 0:
+ n = min(self._num_motion_latent, prev_last_latent.shape[2], t_lat - 1)
+ if n > 0:
+ motion = prev_last_latent[:, :, -n:, :, :].to(tt_y.device, tt_y.dtype)
+ if motion.shape[-2:] != tt_y.shape[-2:]:
+ logger.warning(
+ f"prev_last_latent spatial shape {tuple(motion.shape[-2:])} != "
+ f"conditioning {tuple(tt_y.shape[-2:])}; skipping motion splice."
+ )
+ n = 0
+ else:
+ tt_y[:, 4:, 1 : 1 + n, :, :] = motion
+
+ if 1 + n < t_lat:
+ tt_y[:, 4:, 1 + n :, :, :] = 0.0
+
+ return latents, tt_y
+
+ def generate_long_video(
+ self,
+ *,
+ prompt: Union[str, List[str]],
+ num_clips: int,
+ anchor_image: Optional[Image.Image] = None,
+ image_prompt=None,
+ base_seed: int = 0,
+ seed_stride: int = 42,
+ partial_output_path: Optional[str] = None,
+ **call_kwargs,
+ ):
+ """Run ``num_clips`` chained I2V generations and return the concat.
+
+ ``anchor_image`` is the persistent SVI reference frame. ``image_prompt``
+ is accepted for compatibility with the base I2V test harness.
+ """
+ if num_clips < 1:
+ raise ValueError("num_clips must be >= 1")
+ call_kwargs.setdefault("output_type", "pt_with_last_latent")
+
+ defaults = _REGIMES[self._regime]["call_defaults"]
+ for k, v in defaults.items():
+ call_kwargs.setdefault(k, v)
+
+ anchor_pil = _as_anchor_image(anchor_image if anchor_image is not None else image_prompt)
+ clip_specs = _make_clip_specs(
+ prompt=prompt,
+ guidance_scale=call_kwargs.pop("guidance_scale"),
+ guidance_scale_2=call_kwargs.pop("guidance_scale_2"),
+ num_clips=num_clips,
+ base_seed=base_seed,
+ seed_stride=seed_stride,
+ )
+
+ try:
+ return self._generate_clips_loop(
+ clip_specs=clip_specs,
+ anchor_image=anchor_pil,
+ partial_output_path=partial_output_path,
+ call_kwargs=call_kwargs,
+ )
+ finally:
+ # Drop the anchor-encode cache so the next generate_long_video
+ # call can re-encode with a different anchor / shape.
+ self._anchor_latents_cache = None
+
+ def _generate_clips_loop(
+ self,
+ *,
+ clip_specs: List[_ClipSpec],
+ anchor_image: Image.Image,
+ partial_output_path,
+ call_kwargs,
+ ):
+ prev_last_latent: Optional[torch.Tensor] = None
+ clips: List[torch.Tensor] = []
+
+ for clip_idx, spec in enumerate(clip_specs):
+ logger.info(
+ f"SVI clip {clip_idx + 1}/{len(clip_specs)} (seed={spec.seed}, "
+ f"num_motion_latent={self._num_motion_latent}, "
+ f"cfg={spec.guidance_scale}/{spec.guidance_scale_2}, "
+ f"prompt={spec.prompt[:80]!r}...)"
+ )
+
+ result = self(
+ prompts=[spec.prompt],
+ image_prompt=[ImagePrompt(image=anchor_image, frame_pos=0)],
+ seed=spec.seed,
+ guidance_scale=spec.guidance_scale,
+ guidance_scale_2=spec.guidance_scale_2,
+ anchor_image=anchor_image,
+ prev_last_latent=prev_last_latent,
+ **call_kwargs,
+ )
+
+ if not (isinstance(result, tuple) and len(result) == 2):
+ raise RuntimeError("SVI requires output_type='pt_with_last_latent' from the inner pipeline.")
+ frames, prev_last_latent = result
+
+ clips.append(frames[0] if frames.ndim == 5 else frames)
+
+ # Save partial concat so a mid-run hang still leaves a usable
+ # video covering the clips that completed.
+ if partial_output_path is not None:
+ partial = _concat_with_overlap(clips, overlap=self._num_overlap_frame)
+ torch.save(partial.detach(), partial_output_path)
+ logger.info(
+ f"saved partial output after clip {clip_idx + 1}/{len(clip_specs)} "
+ f"({partial.shape[0]} frames) to {partial_output_path}"
+ )
+
+ return _concat_with_overlap(clips, overlap=self._num_overlap_frame)
+
+
+def _make_clip_specs(
+ *,
+ prompt: Union[str, List[str]],
+ guidance_scale,
+ guidance_scale_2,
+ num_clips: int,
+ base_seed: int,
+ seed_stride: int,
+) -> List[_ClipSpec]:
+ prompts = _broadcast_prompt(prompt, num_clips)
+ cfgs = _broadcast_float(guidance_scale, num_clips, "guidance_scale")
+ cfg2s = _broadcast_float(guidance_scale_2, num_clips, "guidance_scale_2")
+ return [
+ _ClipSpec(
+ prompt=prompts[i],
+ seed=base_seed + i * seed_stride,
+ guidance_scale=cfgs[i],
+ guidance_scale_2=cfg2s[i],
+ )
+ for i in range(num_clips)
+ ]
+
+
+def _broadcast_prompt(prompt: Union[str, List[str]], num_clips: int) -> List[str]:
+ if isinstance(prompt, list):
+ if len(prompt) != num_clips:
+ raise ValueError(f"prompt list length {len(prompt)} != num_clips {num_clips}")
+ return prompt
+ return [prompt] * num_clips
+
+
+def _broadcast_float(value, num_clips: int, name: str) -> List[float]:
+ if isinstance(value, (list, tuple)):
+ if len(value) != num_clips:
+ raise ValueError(f"{name} list length {len(value)} != num_clips {num_clips}")
+ return [float(v) for v in value]
+ return [float(value)] * num_clips
+
+
+def _as_anchor_image(value) -> Image.Image:
+ """Accept a PIL image or single ImagePrompt/list wrapper from the I2V API."""
+ if isinstance(value, list):
+ if len(value) != 1:
+ raise ValueError("SVI supports a single anchor image; got list of length != 1")
+ return _as_anchor_image(value[0])
+ if isinstance(value, ImagePrompt):
+ return value.image
+ if not isinstance(value, Image.Image):
+ raise TypeError("SVI requires anchor_image or image_prompt to be a PIL image")
+ return value
+
+
+def _concat_with_overlap(clips: List[torch.Tensor], *, overlap: int) -> torch.Tensor:
+ """Temporal-concat decoded clips, dropping ``overlap`` boundary frames between adjacent clips."""
+ if not clips:
+ raise ValueError("no clips to concatenate")
+ pieces = [clips[0]] + [c[overlap:] if overlap > 0 else c for c in clips[1:]]
+ return torch.cat(pieces, dim=0)
diff --git a/code/models/tt_dit/experimental/tests/__init__.py b/code/models/tt_dit/experimental/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/code/models/tt_dit/experimental/tests/conftest.py b/code/models/tt_dit/experimental/tests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f0ca06eded788a9fc20cd2f612eb183e603d670
--- /dev/null
+++ b/code/models/tt_dit/experimental/tests/conftest.py
@@ -0,0 +1,12 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import os
+
+import torch
+from loguru import logger
+
+num_torch_threads = max(1, os.cpu_count())
+logger.info(f"Setting torch num_threads to {num_torch_threads}")
+torch.set_num_threads(num_torch_threads)
diff --git a/code/models/tt_dit/experimental/tests/test_adapter_loader.py b/code/models/tt_dit/experimental/tests/test_adapter_loader.py
new file mode 100644
index 0000000000000000000000000000000000000000..098dafe786e05742cdcba6693daaca38d7bcdc78
--- /dev/null
+++ b/code/models/tt_dit/experimental/tests/test_adapter_loader.py
@@ -0,0 +1,224 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Integration tests for the runtime-LoRA adapter loader.
+
+Builds a tiny WanTransformer3DModel (1 block) with ``lora_enabled=True``,
+writes a synthetic LoRA safetensors file, runs ``load_adapter_into``, and
+checks that:
+
+ - every LoRA-aware Linear in the block got an adapter registered
+ - ``bind_active`` marks the right modules active across all paths
+ - ``unbind_active`` clears the active state
+
+The forward math is covered by ``test_lora_variants.py``; this file
+exercises the loader's key-mapping logic and the pipeline-mixin bind
+flow against the default (``fuse``) mode.
+
+Run with:
+ pytest -xvs models/tt_dit/experimental/tests/test_adapter_loader.py
+"""
+from __future__ import annotations
+
+import tempfile
+from pathlib import Path
+
+import pytest
+import torch
+
+import ttnn
+from models.tt_dit.experimental.lora.adapter_loader import load_adapter_into
+from models.tt_dit.layers.lora import LoRAMixin
+from models.tt_dit.models.transformers.wan2_2.transformer_wan import WanTransformer3DModel
+from models.tt_dit.parallel.config import DiTParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+
+
+def _save_synthetic_adapter(
+ path: Path,
+ *,
+ num_blocks: int = 1,
+ dim: int = 128,
+ ffn_dim: int = 256,
+ rank: int = 8,
+) -> None:
+ """Write a fake LoRA safetensors that covers all 6 LoRA-targeted Linears
+ per block, using the lightx2v naming convention."""
+ from safetensors.torch import save_file
+
+ state: dict[str, torch.Tensor] = {}
+ dtype = torch.bfloat16
+
+ def add_pair(base: str, in_dim: int, out_dim: int):
+ state[f"{base}.lora_A.weight"] = torch.randn(rank, in_dim, dtype=dtype) * 0.02
+ state[f"{base}.lora_B.weight"] = torch.randn(out_dim, rank, dtype=dtype) * 0.02
+
+ for i in range(num_blocks):
+ # self-attn Q, K, V, O
+ add_pair(f"blocks.{i}.self_attn.q", dim, dim)
+ add_pair(f"blocks.{i}.self_attn.k", dim, dim)
+ add_pair(f"blocks.{i}.self_attn.v", dim, dim)
+ add_pair(f"blocks.{i}.self_attn.o", dim, dim)
+ # cross-attn Q, K, V, O
+ add_pair(f"blocks.{i}.cross_attn.q", dim, dim)
+ add_pair(f"blocks.{i}.cross_attn.k", dim, dim)
+ add_pair(f"blocks.{i}.cross_attn.v", dim, dim)
+ add_pair(f"blocks.{i}.cross_attn.o", dim, dim)
+ # ffn ff1, ff2 (lightx2v naming: ffn.0 = ff1, ffn.2 = ff2)
+ add_pair(f"blocks.{i}.ffn.0", dim, ffn_dim)
+ add_pair(f"blocks.{i}.ffn.2", ffn_dim, dim)
+
+ save_file(state, str(path))
+
+
+def _count_registered(transformer) -> int:
+ """Count LoRA modules with at least one entry in their bank."""
+ n = 0
+ for mod in _iter_lora_modules(transformer):
+ if any(a is not None for a in mod.lora_bank):
+ n += 1
+ return n
+
+
+def _iter_lora_modules(module):
+ if isinstance(module, LoRAMixin):
+ yield module
+ for _, child in module.named_children():
+ yield from _iter_lora_modules(child)
+
+
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
+def test_adapter_loader_registers_all_lora_linears(mesh_device: ttnn.MeshDevice) -> None:
+ """Loader picks up Q/K/V/O for both attn1/attn2 plus ff1/ff2 per block."""
+ num_heads = 4
+ head_dim = 32
+ dim = num_heads * head_dim
+ ffn_dim = 2 * dim
+ rank = 8
+ num_blocks = 1
+
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=1, mesh_axis=0),
+ tensor_parallel=ParallelFactor(factor=1, mesh_axis=1),
+ sequence_parallel=ParallelFactor(factor=1, mesh_axis=0),
+ )
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+
+ transformer = WanTransformer3DModel(
+ patch_size=(1, 2, 2),
+ num_heads=num_heads,
+ dim=dim,
+ in_channels=16,
+ out_channels=16,
+ text_dim=128,
+ freq_dim=64,
+ ffn_dim=ffn_dim,
+ num_layers=num_blocks,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=False,
+ lora_enabled=True,
+ )
+
+ # Every block should have 5 LoRA-aware Linears wired in: to_qkv (self),
+ # to_q (cross), to_kv (cross), to_out for each attention (×2), ff1, ff2.
+ # That's 1 + 1 + 1 + 2 + 1 + 1 = 7 modules per block.
+ expected_per_block = 7
+ n_modules = sum(1 for _ in _iter_lora_modules(transformer))
+ assert (
+ n_modules == num_blocks * expected_per_block
+ ), f"expected {num_blocks * expected_per_block} LoRA modules; got {n_modules}"
+
+ with tempfile.TemporaryDirectory() as tmp:
+ adapter_path = Path(tmp) / "synthetic.safetensors"
+ _save_synthetic_adapter(adapter_path, num_blocks=num_blocks, dim=dim, ffn_dim=ffn_dim, rank=rank)
+
+ handle = load_adapter_into(transformer, str(adapter_path), scale=1.0, name="synthetic")
+
+ # All 7 LoRA modules per block should have a registered adapter.
+ registered = _count_registered(transformer)
+ assert (
+ registered == num_blocks * expected_per_block
+ ), f"expected {num_blocks * expected_per_block} registered adapters; got {registered}"
+
+ # Fused-QKV (to_qkv self-attn): rank should be 3*r. Fused KV (cross-attn
+ # to_kv): rank should be 2*r. Singletons: rank = r.
+ block0 = transformer.blocks[0]
+ assert block0.attn1.to_qkv.lora_bank[0].rank == 3 * rank
+ assert block0.attn2.to_kv.lora_bank[0].rank == 2 * rank
+ assert block0.attn2.to_q.lora_bank[0].rank == rank
+ assert block0.attn1.to_out.lora_bank[0].rank == rank
+ assert block0.attn2.to_out.lora_bank[0].rank == rank
+ assert block0.ffn.ff1.lora_bank[0].rank == rank
+ assert block0.ffn.ff2.lora_bank[0].rank == rank
+
+ # handle.rank tracks the max rank across targets (3*rank for the fused-QKV group).
+ assert handle.rank == 3 * rank
+
+
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
+def test_pipeline_bind_unbind_walks_all_modules(mesh_device: ttnn.MeshDevice) -> None:
+ """The pipeline mixin's bind/unbind walks every LoRA module on each set_active_lora."""
+ from models.tt_dit.experimental.pipelines.pipeline_wan_runtime_lora import _iter_lora_modules as pipeline_iter
+
+ num_heads = 4
+ head_dim = 32
+ dim = num_heads * head_dim
+ ffn_dim = 2 * dim
+ rank = 8
+
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=1, mesh_axis=0),
+ tensor_parallel=ParallelFactor(factor=1, mesh_axis=1),
+ sequence_parallel=ParallelFactor(factor=1, mesh_axis=0),
+ )
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+
+ transformer = WanTransformer3DModel(
+ patch_size=(1, 2, 2),
+ num_heads=num_heads,
+ dim=dim,
+ in_channels=16,
+ out_channels=16,
+ text_dim=128,
+ freq_dim=64,
+ ffn_dim=ffn_dim,
+ num_layers=1,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=False,
+ lora_enabled=True,
+ )
+
+ with tempfile.TemporaryDirectory() as tmp:
+ adapter_path = Path(tmp) / "synthetic.safetensors"
+ _save_synthetic_adapter(adapter_path, num_blocks=1, dim=dim, ffn_dim=ffn_dim, rank=rank)
+ handle = load_adapter_into(transformer, str(adapter_path), name="synthetic")
+
+ # Simulate the bind path the pipeline mixin runs.
+ for dotted, bank_idx in handle.target_indices.items():
+ cur = transformer
+ for part in dotted.split("."):
+ cur = cur[int(part)] if part.isdigit() else getattr(cur, part)
+ cur.bind_active(bank_idx)
+
+ # All modules covered by the handle should be active (delta merged).
+ for dotted in handle.target_indices:
+ cur = transformer
+ for part in dotted.split("."):
+ cur = cur[int(part)] if part.isdigit() else getattr(cur, part)
+ assert cur.is_lora_active, f"{dotted} not active after bind_active"
+
+ # Unbind everything → delta subtracted, is_lora_active False.
+ for mod in pipeline_iter(transformer):
+ if mod.is_lora_active:
+ mod.unbind_active()
+ for dotted in handle.target_indices:
+ cur = transformer
+ for part in dotted.split("."):
+ cur = cur[int(part)] if part.isdigit() else getattr(cur, part)
+ assert not cur.is_lora_active
+ assert cur.active_idx is None
diff --git a/code/models/tt_dit/experimental/tests/test_lora_variants.py b/code/models/tt_dit/experimental/tests/test_lora_variants.py
new file mode 100644
index 0000000000000000000000000000000000000000..a56f8620eedf8ebd3577698a0ad3a2ef76bb942e
--- /dev/null
+++ b/code/models/tt_dit/experimental/tests/test_lora_variants.py
@@ -0,0 +1,262 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Parity tests for the three LoRA-aware Linear subclasses, each exercised
+under both ``lora_mode`` options:
+
+ fuse — bind merges ``scale * A.T @ B.T`` into the base weight in-place.
+ Forward runs the un-modified base path.
+ runtime — bind uploads A, B to device; forward adds
+ ``scale * (x @ A) @ B`` on top of the base output.
+
+The unbind branch is checked in both modes — for ``fuse`` it subtracts
+the delta back out of W; for ``runtime`` it frees the A/B tensors so the
+forward path collapses to the base.
+
+Mesh shape is 1x1 so sharding collapses to the replicated case for all
+three variants. The tests still exercise the LoRA-specific upload and
+forward code paths and catch class-level bugs in shape handling,
+fused-activation fallback, and chunked-output handling.
+
+Run with:
+ pytest -xvs models/tt_dit/experimental/tests/test_lora_variants.py
+"""
+from __future__ import annotations
+
+import pytest
+import torch
+
+import ttnn
+from models.tt_dit.layers.linear import LoRAColParallelLinear, LoRALinear, LoRARowParallelLinear
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.tensor import bf16_tensor
+
+
+def _make_pair(in_features: int, out_features: int, rank: int, dtype: torch.dtype):
+ A = torch.randn(rank, in_features, dtype=dtype) * 0.02
+ B = torch.randn(out_features, rank, dtype=dtype) * 0.02
+ return A, B
+
+
+def _ref_lora(x, W, b, A, B, scale):
+ """y = x @ (W + scale * B @ A).T + b"""
+ W_eff = W + scale * (B @ A)
+ return torch.nn.functional.linear(x, W_eff, b)
+
+
+# --------------------------------------------------------------------
+# LoRALinear (replicated)
+# --------------------------------------------------------------------
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
+@pytest.mark.parametrize("lora_mode", ["fuse", "runtime"])
+@pytest.mark.parametrize(
+ ("seq_len", "in_features", "out_features", "rank"),
+ [
+ (128, 256, 256, 8),
+ (256, 512, 1024, 16),
+ ],
+)
+def test_lora_linear_variant(
+ mesh_device: ttnn.MeshDevice,
+ lora_mode: str,
+ seq_len: int,
+ in_features: int,
+ out_features: int,
+ rank: int,
+) -> None:
+ torch.manual_seed(0)
+ dtype = torch.bfloat16
+ scale = 0.6
+
+ torch_lin = torch.nn.Linear(in_features, out_features, bias=True).to(dtype=dtype)
+ torch_lin.eval()
+ A_t, B_t = _make_pair(in_features, out_features, rank, dtype)
+ x_t = torch.randn(1, 1, seq_len, in_features, dtype=dtype)
+
+ with torch.no_grad():
+ y_base = torch_lin(x_t)
+ y_lora = _ref_lora(x_t, torch_lin.weight, torch_lin.bias, A_t, B_t, scale)
+
+ tt = LoRALinear(in_features, out_features, bias=True, mesh_device=mesh_device, lora_mode=lora_mode)
+ tt.load_torch_state_dict(torch_lin.state_dict())
+ idx = tt.register_lora(A_t, B_t, scale=scale)
+ x_tt = bf16_tensor(x_t, device=mesh_device)
+
+ # 1) no adapter bound → base behavior
+ y = tt(x_tt)
+ for t in ttnn.get_device_tensors(y):
+ assert_quality(y_base, ttnn.to_torch(t), pcc=0.999_5)
+
+ # 2) bind active → LoRA effect applied
+ tt.bind_active(idx, scale=scale)
+ y = tt(x_tt)
+ for t in ttnn.get_device_tensors(y):
+ assert_quality(y_lora, ttnn.to_torch(t), pcc=0.999_0)
+
+ # 3) unbind collapses back to base
+ tt.unbind_active()
+ y = tt(x_tt)
+ for t in ttnn.get_device_tensors(y):
+ assert_quality(y_base, ttnn.to_torch(t), pcc=0.999_5)
+
+ tt.deallocate_lora()
+
+
+# --------------------------------------------------------------------
+# LoRAColParallelLinear — single output and chunked output
+# --------------------------------------------------------------------
+# Chunked configs run fuse-only: the runtime delta path doesn't split
+# its delta into the same chunk layout as the base output.
+_COL_PARALLEL_PARAMS = [
+ # (label, lora_mode, seq, in, out, rank, chunks, activation_fn)
+ ("plain_fuse", "fuse", 128, 256, 512, 8, None, None),
+ ("plain_runtime", "runtime", 128, 256, 512, 8, None, None),
+ ("gelu_fuse", "fuse", 128, 256, 1024, 8, None, "gelu"),
+ ("gelu_runtime", "runtime", 128, 256, 1024, 8, None, "gelu"),
+ ("chunked_qkv_fuse", "fuse", 128, 256, 3 * 256, 8, 3, None),
+]
+
+
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
+@pytest.mark.parametrize(
+ ("lora_mode", "seq_len", "in_features", "out_features", "rank", "chunks", "activation_fn"),
+ [params[1:] for params in _COL_PARALLEL_PARAMS],
+ ids=[params[0] for params in _COL_PARALLEL_PARAMS],
+)
+def test_lora_col_parallel_variant(
+ mesh_device: ttnn.MeshDevice,
+ lora_mode: str,
+ seq_len: int,
+ in_features: int,
+ out_features: int,
+ rank: int,
+ chunks: int | None,
+ activation_fn: str | None,
+) -> None:
+ torch.manual_seed(0)
+ dtype = torch.bfloat16
+ scale = 0.5
+
+ torch_lin = torch.nn.Linear(in_features, out_features, bias=True).to(dtype=dtype)
+ torch_lin.eval()
+ A_t, B_t = _make_pair(in_features, out_features, rank, dtype)
+ x_t = torch.randn(1, 1, seq_len, in_features, dtype=dtype)
+
+ def torch_act(y):
+ if activation_fn == "gelu":
+ return torch.nn.functional.gelu(y)
+ return y
+
+ with torch.no_grad():
+ y_base = torch_act(torch_lin(x_t))
+ y_lora = torch_act(_ref_lora(x_t, torch_lin.weight, torch_lin.bias, A_t, B_t, scale))
+
+ tt = LoRAColParallelLinear(
+ in_features,
+ out_features,
+ bias=True,
+ mesh_device=mesh_device,
+ mesh_axis=0,
+ activation_fn=activation_fn,
+ chunks=chunks,
+ lora_mode=lora_mode,
+ )
+ tt.load_torch_state_dict(torch_lin.state_dict())
+ idx = tt.register_lora(A_t, B_t, scale=scale)
+ x_tt = bf16_tensor(x_t, device=mesh_device)
+
+ def _join(out):
+ """Concat chunked outputs along last dim; pass through single tensors."""
+ if isinstance(out, list):
+ torch_chunks = [ttnn.to_torch(ttnn.get_device_tensors(o)[0]) for o in out]
+ return torch.cat(torch_chunks, dim=-1)
+ return ttnn.to_torch(ttnn.get_device_tensors(out)[0])
+
+ # 1) no adapter bound → base behavior
+ assert_quality(y_base, _join(tt(x_tt)), pcc=0.999_5)
+
+ # 2) bind active → LoRA effect applied
+ tt.bind_active(idx, scale=scale)
+ assert_quality(y_lora, _join(tt(x_tt)), pcc=0.999_0)
+
+ # 3) unbind collapses to base
+ tt.unbind_active()
+ assert_quality(y_base, _join(tt(x_tt)), pcc=0.999_0)
+
+ tt.deallocate_lora()
+
+
+# --------------------------------------------------------------------
+# LoRARowParallelLinear — N_tp=1 (1x1 mesh) sanity
+# --------------------------------------------------------------------
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
+@pytest.mark.parametrize("lora_mode", ["fuse", "runtime"])
+@pytest.mark.parametrize(
+ ("seq_len", "in_features", "out_features", "rank"),
+ [
+ (128, 512, 256, 8),
+ (256, 1024, 512, 16),
+ ],
+)
+def test_lora_row_parallel_variant(
+ mesh_device: ttnn.MeshDevice,
+ lora_mode: str,
+ seq_len: int,
+ in_features: int,
+ out_features: int,
+ rank: int,
+) -> None:
+ from models.tt_dit.parallel.manager import CCLManager
+
+ torch.manual_seed(0)
+ dtype = torch.bfloat16
+ scale = 0.4
+
+ torch_lin = torch.nn.Linear(in_features, out_features, bias=True).to(dtype=dtype)
+ torch_lin.eval()
+ A_t, B_t = _make_pair(in_features, out_features, rank, dtype)
+ x_t = torch.randn(1, 1, seq_len, in_features, dtype=dtype)
+
+ with torch.no_grad():
+ y_base = torch_lin(x_t)
+ y_lora = _ref_lora(x_t, torch_lin.weight, torch_lin.bias, A_t, B_t, scale)
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+ tt = LoRARowParallelLinear(
+ in_features,
+ out_features,
+ bias=True,
+ mesh_device=mesh_device,
+ mesh_axis=0,
+ ccl_manager=ccl_manager,
+ lora_mode=lora_mode,
+ )
+ tt.load_torch_state_dict(torch_lin.state_dict())
+ idx = tt.register_lora(A_t, B_t, scale=scale)
+ x_tt = bf16_tensor(x_t, device=mesh_device)
+
+ # baseline
+ y = tt(x_tt)
+ assert_quality(y_base, ttnn.to_torch(ttnn.get_device_tensors(y)[0]), pcc=0.999_5)
+
+ # bind active
+ tt.bind_active(idx, scale=scale)
+ y = tt(x_tt)
+ assert_quality(y_lora, ttnn.to_torch(ttnn.get_device_tensors(y)[0]), pcc=0.999_0)
+
+ # unbind
+ tt.unbind_active()
+ y = tt(x_tt)
+ assert_quality(y_base, ttnn.to_torch(ttnn.get_device_tensors(y)[0]), pcc=0.999_5)
+
+ tt.deallocate_lora()
+
+
+# --------------------------------------------------------------------
+# Construction-time validation
+# --------------------------------------------------------------------
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
+def test_invalid_lora_mode_raises(mesh_device: ttnn.MeshDevice, expect_error) -> None:
+ with expect_error(ValueError, "lora_mode"):
+ LoRALinear(64, 64, bias=False, mesh_device=mesh_device, lora_mode="bogus")
diff --git a/code/models/tt_dit/experimental/tests/test_pipeline_anisora.py b/code/models/tt_dit/experimental/tests/test_pipeline_anisora.py
new file mode 100644
index 0000000000000000000000000000000000000000..205a739d7fbd68fe9eba48493eb2167dd96cd881
--- /dev/null
+++ b/code/models/tt_dit/experimental/tests/test_pipeline_anisora.py
@@ -0,0 +1,227 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import itertools
+import os
+
+import numpy as np
+import PIL
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.tt_dit.experimental.pipelines.pipeline_anisora import AniSoraPipeline
+from models.tt_dit.pipelines.wan.pipeline_wan_i2v import ImagePrompt
+from models.tt_dit.utils.test import ring_params
+
+
+@pytest.mark.parametrize(
+ "no_prompt",
+ [{"1": True, "0": False}.get(os.environ.get("NO_PROMPT"), False)],
+)
+@pytest.mark.parametrize(
+ "mesh_device, mesh_shape, num_links, dynamic_load, device_params, topology, is_fsdp",
+ [
+ # BH Galaxy 4x8 Ring — sole supported config in the first AniSora PR.
+ [(4, 8), (4, 8), 2, False, ring_params, ttnn.Topology.Ring, False],
+ ],
+ ids=["bh_4x8sp1tp0_ring"],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ "width, height",
+ [
+ (832, 480),
+ (1280, 720),
+ ],
+ ids=[
+ "resolution_480p",
+ "resolution_720p",
+ ],
+)
+def test_pipeline_inference(
+ mesh_device,
+ mesh_shape,
+ num_links,
+ dynamic_load,
+ topology,
+ width,
+ height,
+ is_fsdp,
+ no_prompt,
+):
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ pil_image = PIL.Image.open(os.environ.get("PROMPT_IMAGE", "./prompt_image.png"))
+ image_prompt = [ImagePrompt(image=pil_image, frame_pos=0)]
+ negative_prompt = ""
+
+ # AniSora upstream defaults (anisoraV3.2/wan/configs/wan_i2v_A14B.py):
+ # sample_steps=40, boundary=0.9, sample_guide_scale=(3.5, 3.5).
+ num_frames = 81
+ num_inference_steps = int(os.environ.get("NUM_STEPS", "40"))
+ guidance_scale = 3.5
+ guidance_scale_2 = 3.5
+
+ pipeline = AniSoraPipeline.create_pipeline(
+ mesh_device=mesh_device,
+ height=height,
+ width=width,
+ num_frames=num_frames,
+ config_overrides={
+ "num_links": num_links,
+ "dynamic_load": dynamic_load,
+ "topology": topology,
+ "is_fsdp": is_fsdp,
+ },
+ )
+
+ prompt = "An anime girl smiling, soft lighting, cinematic."
+
+ def run(*, prompt, number, seed):
+ logger.info(f"Running AniSora inference with prompt: '{prompt}'")
+ logger.info(f"Parameters: {height}x{width}, {num_frames} frames, {num_inference_steps} steps")
+
+ with torch.no_grad():
+ result = pipeline(
+ prompts=[prompt],
+ image_prompt=image_prompt,
+ negative_prompts=[negative_prompt],
+ num_inference_steps=num_inference_steps,
+ seed=seed,
+ guidance_scale=guidance_scale,
+ guidance_scale_2=guidance_scale_2,
+ output_type="uint8",
+ )
+
+ if hasattr(result, "frames"):
+ frames = result.frames
+ else:
+ frames = result[0] if isinstance(result, tuple) else result
+
+ logger.info("AniSora inference completed successfully")
+ logger.info(f" Output shape: {frames.shape if hasattr(frames, 'shape') else 'Unknown'}")
+ logger.info(f" Output type: {type(frames)}")
+
+ if isinstance(frames, np.ndarray):
+ logger.info(f" Video data range: [{frames.min():.3f}, {frames.max():.3f}]")
+ elif isinstance(frames, torch.Tensor):
+ logger.info(f" Video data range: [{frames.min().item():.3f}, {frames.max().item():.3f}]")
+
+ frames = frames[0]
+ output_filename = f"wan_anisora_i2v_{width}x{height}_{number}.mp4"
+ try:
+ from models.tt_dit.utils.video import export_to_video
+
+ export_to_video(frames, output_filename, fps=16)
+ logger.info(f"Saved video to: {output_filename}")
+ except ImportError:
+ logger.info("Could not export video - imageio_ffmpeg not available")
+
+ if no_prompt:
+ run(prompt=prompt, number=0, seed=42)
+ else:
+ for i in itertools.count():
+ new_prompt = input("Enter the input prompt, or q to exit: ")
+ if new_prompt:
+ prompt = new_prompt
+ if prompt[0] == "q":
+ break
+ run(prompt=prompt, number=i, seed=i)
+
+
+# ---------------------------------------------------------------------------
+# Random-weight smoke test
+# ---------------------------------------------------------------------------
+# Runs the full pipeline end-to-end without downloading the AniSora safetensors
+# or the two ~28 GB transformer subfolders from Wan-AI. Tokenizer / UMT5 text
+# encoder / VAE / scheduler still come from Wan-AI/Wan2.2-I2V-A14B-Diffusers
+# (~12 GB total) and require TT_DIT_ALLOW_HF_DOWNLOAD=1 the first time.
+# Output frames are garbage by design — this only validates compile / mesh /
+# shape / CCL plumbing, not numeric quality. Uses 4 sampling steps to keep the
+# smoke test short; the 40-step real-weights config is exercised above.
+
+
+@pytest.mark.parametrize(
+ "mesh_device, mesh_shape, num_links, dynamic_load, device_params, topology, is_fsdp",
+ [
+ [(4, 8), (4, 8), 2, False, ring_params, ttnn.Topology.Ring, False],
+ ],
+ ids=["bh_4x8sp1tp0_ring"],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ "width, height",
+ [
+ (832, 480),
+ ],
+ ids=["resolution_480p"],
+)
+def test_pipeline_inference_random_weights(
+ mesh_device,
+ mesh_shape,
+ num_links,
+ dynamic_load,
+ topology,
+ width,
+ height,
+ is_fsdp,
+):
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ pil_image = PIL.Image.open(os.environ.get("PROMPT_IMAGE", "./prompt_image.png"))
+ image_prompt = [ImagePrompt(image=pil_image, frame_pos=0)]
+
+ num_frames = 81
+ num_inference_steps = 4
+
+ os.environ["TT_DIT_RANDOM_WEIGHTS"] = "1"
+ try:
+ pipeline = AniSoraPipeline.create_pipeline(
+ mesh_device=mesh_device,
+ height=height,
+ width=width,
+ num_frames=num_frames,
+ config_overrides={
+ "num_links": num_links,
+ "dynamic_load": dynamic_load,
+ "topology": topology,
+ "is_fsdp": is_fsdp,
+ },
+ )
+ finally:
+ os.environ.pop("TT_DIT_RANDOM_WEIGHTS", None)
+
+ prompt = "smoke test — random weights, output is meaningless"
+ logger.info(f"Random-weight smoke test: {height}x{width}, {num_frames} frames, {num_inference_steps} steps")
+
+ with torch.no_grad():
+ result = pipeline(
+ prompts=[prompt],
+ image_prompt=image_prompt,
+ negative_prompts=[""],
+ num_inference_steps=num_inference_steps,
+ seed=42,
+ guidance_scale=3.5,
+ guidance_scale_2=3.5,
+ output_type="uint8",
+ )
+
+ if hasattr(result, "frames"):
+ frames = result.frames
+ else:
+ frames = result[0] if isinstance(result, tuple) else result
+
+ if isinstance(frames, np.ndarray):
+ rng_str = f"[{frames.min():.3f}, {frames.max():.3f}]"
+ elif isinstance(frames, torch.Tensor):
+ rng_str = f"[{frames.min().item():.3f}, {frames.max().item():.3f}]"
+ else:
+ rng_str = "n/a"
+
+ shape = frames.shape if hasattr(frames, "shape") else "Unknown"
+ logger.info(f"Random-weight smoke test PASSED. Output shape={shape}, range={rng_str}")
diff --git a/code/models/tt_dit/experimental/tests/test_pipeline_lora.py b/code/models/tt_dit/experimental/tests/test_pipeline_lora.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d2723e88c58cfafff706509876c32adc5ad1086
--- /dev/null
+++ b/code/models/tt_dit/experimental/tests/test_pipeline_lora.py
@@ -0,0 +1,300 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""End-to-end test for Wan2.2 I2V with LoRA adapters fused into the base.
+
+Reads adapter paths via env vars (compatible with the previous experimental
+test): ``LORA_HIGH_PATH``, ``LORA_LOW_PATH``, ``LORA_SCALE``. Set
+``LORA_STACK_HIGH`` and/or ``LORA_STACK_LOW`` to a comma-separated list of
+``path[:scale]`` entries to exercise multi-LoRA stacking. If both single-LoRA
+and stack env vars are set, the stack form wins.
+"""
+import itertools
+import os
+import statistics
+from typing import List, Tuple
+
+import numpy as np
+import PIL
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.perf.benchmarking_utils import BenchmarkProfiler
+from models.tt_dit.experimental.pipelines.pipeline_wan_lora import LoRASpec, WanPipelineI2VLora
+from models.tt_dit.pipelines.events import profiler_event_callback
+from models.tt_dit.pipelines.wan.pipeline_wan_i2v import ImagePrompt
+from models.tt_dit.utils.test import line_params, ring_params_8k
+
+# Only the 4x32 row traces, so only it needs the trace region carved out of DRAM.
+DEVICE_PARAMS = {"trace_region_size": 150000000}
+
+
+def _rank() -> int:
+ return int(ttnn.distributed_context_get_rank()) if ttnn.using_distributed_env() else 0
+
+
+def create_fractal_image(width: int, height: int) -> PIL.Image.Image:
+ c = np.linspace(-2.0, 1.0, width)[None, :] + 1j * np.linspace(-1.5, 1.5, height)[:, None]
+ z = np.zeros_like(c)
+ img = np.zeros(c.shape, dtype=np.uint8)
+ for i in range(32):
+ z = z * z + c
+ img[(img == 0) & (np.abs(z) > 2)] = 255 - 8 * i
+ return PIL.Image.fromarray(np.dstack((img, np.roll(img, width // 10, 1), np.roll(img, height // 10, 0))), "RGB")
+
+
+def _parse_stack(env_val: str) -> List[LoRASpec]:
+ """Parse ``path[:scale],path[:scale]`` into a LoRASpec list."""
+ out: List[LoRASpec] = []
+ for entry in env_val.split(","):
+ entry = entry.strip()
+ if not entry:
+ continue
+ if ":" in entry:
+ path, scale_str = entry.rsplit(":", 1)
+ out.append(LoRASpec(path.strip(), float(scale_str)))
+ else:
+ out.append(LoRASpec(entry))
+ return out
+
+
+def _resolve_lora_args() -> Tuple[List[LoRASpec], List[LoRASpec], float]:
+ stack_high = os.environ.get("LORA_STACK_HIGH")
+ stack_low = os.environ.get("LORA_STACK_LOW")
+ single_high = os.environ.get("LORA_HIGH_PATH")
+ single_low = os.environ.get("LORA_LOW_PATH")
+ scale = float(os.environ.get("LORA_SCALE", "1.0"))
+
+ if stack_high or stack_low:
+ return (
+ _parse_stack(stack_high) if stack_high else [],
+ _parse_stack(stack_low) if stack_low else [],
+ scale,
+ )
+
+ high = [LoRASpec(single_high, scale)] if single_high else []
+ low = [LoRASpec(single_low, scale)] if single_low else []
+ return high, low, scale
+
+
+@pytest.mark.parametrize(
+ "no_prompt",
+ [{"1": True, "0": False}.get(os.environ.get("NO_PROMPT"), False)],
+)
+@pytest.mark.parametrize(
+ "mesh_device, mesh_shape, num_links, dynamic_load, device_params, topology, is_fsdp",
+ [
+ [(2, 4), (2, 4), 2, True, line_params, ttnn.Topology.Linear, False],
+ [(4, 8), (4, 8), 2, False, ring_params_8k, ttnn.Topology.Ring, False],
+ # ring_params_8k adds the fabric router config test_performance_wan uses at 4x32.
+ # [(4, 32), (4, 32), 2, False, {**DEVICE_PARAMS, **ring_params}, ttnn.Topology.Ring, False],
+ [(4, 32), (4, 32), 2, False, {**DEVICE_PARAMS, **ring_params_8k}, ttnn.Topology.Ring, False],
+ ],
+ ids=["bh_2x4sp1tp0", "bh_4x8sp1tp0_ring", "bh_4x32sp1tp0_ring"],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ "width, height",
+ [
+ (832, 480),
+ (1280, 720),
+ ],
+ ids=[
+ "resolution_480p",
+ "resolution_720p",
+ ],
+)
+def test_pipeline_inference(
+ mesh_device,
+ mesh_shape,
+ num_links,
+ dynamic_load,
+ topology,
+ width,
+ height,
+ is_fsdp,
+ no_prompt,
+):
+ lora_high, lora_low, scale = _resolve_lora_args()
+ if not lora_high and not lora_low:
+ pytest.skip(
+ "Set LORA_HIGH_PATH / LORA_LOW_PATH (single-LoRA) or "
+ "LORA_STACK_HIGH / LORA_STACK_LOW (multi-LoRA) to run."
+ )
+
+ # Trace on 4x32 only, matching test_performance_wan; TEST_TRACED overrides either way.
+ traced = True if mesh_shape == (4, 32) else False
+ logger.info(f"traced={traced} (mesh_shape={mesh_shape})")
+
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ prompt_image_path = os.environ.get("PROMPT_IMAGE")
+ pil_image = PIL.Image.open(prompt_image_path) if prompt_image_path else create_fractal_image(width, height)
+ image_prompt = [ImagePrompt(image=pil_image, frame_pos=0)]
+ negative_prompt = ""
+
+ num_frames = 81
+ num_inference_steps = int(os.environ.get("NUM_STEPS", "4"))
+ guidance_scale = float(os.environ.get("GUIDANCE_SCALE", "1"))
+ guidance_scale_2 = float(os.environ.get("GUIDANCE_SCALE_2", str(guidance_scale)))
+ boundary_ratio = float(os.environ.get("BOUNDARY_RATIO", "0.875"))
+
+ # A scale of 1.0 is a no-op lerp, so the unconditional pass is pure waste: skip CFG
+ # entirely and halve the denoising cost. Guidance-distilled LoRAs want exactly this.
+ cfg_enabled = guidance_scale > 1 or guidance_scale_2 > 1
+ logger.info(f"cfg_enabled={cfg_enabled} (guidance_scale={guidance_scale}, guidance_scale_2={guidance_scale_2})")
+
+ pipeline = WanPipelineI2VLora(
+ device=mesh_device,
+ config=WanPipelineI2VLora.default_config(
+ mesh_device=mesh_device,
+ height=height,
+ width=width,
+ num_frames=num_frames,
+ cfg_enabled=cfg_enabled,
+ config_overrides={
+ "num_links": num_links,
+ "dynamic_load": dynamic_load,
+ "topology": topology,
+ "is_fsdp": is_fsdp,
+ "boundary_ratio": boundary_ratio,
+ },
+ ),
+ lora_high=lora_high if lora_high else None,
+ lora_low=lora_low if lora_low else None,
+ )
+
+ # prompt = os.environ.get("PROMPT", "A golden retriever running on a sandy beach, waves in the background")
+ prompt = os.environ.get("PROMPT", "A cat running on a sandy beach, waves in the background")
+
+ profiler = BenchmarkProfiler()
+
+ def run(*, prompt, number, seed, profiler_iteration=None):
+ logger.info(f"Running LoRA inference with prompt: '{prompt}'")
+ logger.info(
+ f"Parameters: {height}x{width}, {num_frames} frames, {num_inference_steps} steps, "
+ f"scale={scale}, stack_high={len(lora_high)}, stack_low={len(lora_low)}"
+ )
+
+ call_kwargs = dict(
+ prompts=[prompt],
+ image_prompt=image_prompt,
+ negative_prompts=[negative_prompt],
+ num_inference_steps=num_inference_steps,
+ seed=seed,
+ guidance_scale=guidance_scale,
+ guidance_scale_2=guidance_scale_2,
+ output_type="uint8",
+ traced=traced,
+ )
+ if profiler_iteration is not None:
+ call_kwargs["on_event"] = profiler_event_callback(profiler, profiler_iteration)
+
+ with torch.no_grad():
+ result = pipeline(**call_kwargs)
+
+ if hasattr(result, "frames"):
+ frames = result.frames
+ else:
+ frames = result[0] if isinstance(result, tuple) else result
+
+ logger.info(f" Output shape: {frames.shape if hasattr(frames, 'shape') else 'Unknown'}")
+ if isinstance(frames, np.ndarray):
+ logger.info(f" Video data range: [{frames.min():.3f}, {frames.max():.3f}]")
+ elif isinstance(frames, torch.Tensor):
+ logger.info(f" Video data range: [{frames.min().item():.3f}, {frames.max().item():.3f}]")
+
+ frames = frames[0]
+ # Every rank holds the same gathered video, so only rank 0 writes it out.
+ if _rank() == 0:
+ output_filename = f"wan_lora_i2v_{width}x{height}_{number}.mp4"
+ try:
+ from models.tt_dit.utils.video import export_to_video
+
+ export_to_video(frames, output_filename, fps=16)
+ logger.info(f"Saved video to: {output_filename}")
+ except ImportError:
+ logger.info("Could not export video - imageio_ffmpeg not available")
+ else:
+ logger.info(f"Skipping video export on rank {_rank()}")
+
+ return frames
+
+ if no_prompt:
+ # Warmup run (not included in stats)
+ logger.info("Running warmup iteration...")
+ with profiler("warmup", iteration=0):
+ run(prompt=prompt, number=-1, seed=0)
+ logger.info(f"Warmup completed in {profiler.get_duration('warmup', 0):.2f}s")
+
+ # Timed performance run
+ num_perf_runs = 1
+ ttnn.synchronize_device(mesh_device)
+ # Line up all hosts so the measured window is comparable across ranks.
+ ttnn.distributed_context_barrier()
+
+ for i in range(num_perf_runs):
+ logger.info(f"Performance run {i+1}/{num_perf_runs}...")
+ with profiler("run", iteration=i):
+ run(prompt=prompt, number=i, seed=42, profiler_iteration=i)
+ logger.info(f" Run {i+1} completed in {profiler.get_duration('run', i):.2f}s")
+
+ # Collect timing stats. "prepare_latents" is the I2V image-conditioning
+ # section: host preprocess/upload of the conditioned frames plus the VAE encode.
+ encoder_times = [profiler.get_duration("encoder", i) for i in range(num_perf_runs)]
+ image_encode_times = [profiler.get_duration("prepare_latents", i) for i in range(num_perf_runs)]
+ denoising_times = [profiler.get_duration("denoising", i) for i in range(num_perf_runs)]
+ vae_times = [profiler.get_duration("vae", i) for i in range(num_perf_runs)]
+ total_times = [profiler.get_duration("run", i) for i in range(num_perf_runs)]
+
+ def fmt_stats(times):
+ if not times:
+ return "No data available"
+ mean = statistics.mean(times)
+ if len(times) > 1:
+ std = statistics.stdev(times)
+ return f"Mean: {mean:8.3f}s | Std: {std:7.3f}s | Min: {min(times):8.3f}s | Max: {max(times):8.3f}s"
+ return f"{mean:8.3f}s"
+
+ if _rank() == 0:
+ sp_factor = tuple(mesh_device.shape)[1]
+ tp_factor = tuple(mesh_device.shape)[0]
+ print("\n" + "=" * 80)
+ print("WAN 2.2 I2V LoRA — PERFORMANCE RESULTS")
+ print("=" * 80)
+ print(f" Resolution: {width}x{height}")
+ print(f" Frames: {num_frames}")
+ print(f" Denoising steps: {num_inference_steps}")
+ print(f" Mesh shape: {tuple(mesh_device.shape)}")
+ print(f" SP factor: {sp_factor} TP factor: {tp_factor}")
+ print(f" Topology: {topology}")
+ print(f" FSDP: {is_fsdp}")
+ print(f" Num links: {num_links}")
+ print(f" LoRA high: {len(lora_high)} adapter(s)")
+ print(f" LoRA low: {len(lora_low)} adapter(s)")
+ print(f" Perf runs: {num_perf_runs}")
+ print("-" * 80)
+ print(f" {'Text Encoding':25s} | {fmt_stats(encoder_times)}")
+ print(f" {'Image Encoding (total)':25s} | {fmt_stats(image_encode_times)}")
+ print(f" {'Denoising':25s} | {fmt_stats(denoising_times)}")
+ print(f" {'VAE Decoding':25s} | {fmt_stats(vae_times)}")
+ print(f" {'Total Pipeline':25s} | {fmt_stats(total_times)}")
+ print("=" * 80)
+ else:
+ logger.info(f"Skipping performance summary on rank {_rank()}")
+ else:
+ for i in itertools.count():
+ new_prompt = input("Enter the input prompt, or q to exit: ")
+ if new_prompt:
+ prompt = new_prompt
+ if prompt[0] == "q":
+ break
+ run(prompt=prompt, number=i, seed=i)
+
+ if traced:
+ # Release before the mesh fixture tears down, as test_performance_wan does.
+ pipeline.release_traces()
diff --git a/code/models/tt_dit/experimental/tests/test_pipeline_wan_distill_i2v.py b/code/models/tt_dit/experimental/tests/test_pipeline_wan_distill_i2v.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca2f1be0a5bfff099af6fa0ffa5e874c98e27127
--- /dev/null
+++ b/code/models/tt_dit/experimental/tests/test_pipeline_wan_distill_i2v.py
@@ -0,0 +1,226 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+import itertools
+import os
+
+import numpy as np
+import PIL
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.tt_dit.experimental.pipelines.pipeline_wan_distill import WanDistillPipelineI2V
+from models.tt_dit.pipelines.wan.pipeline_wan_i2v import ImagePrompt
+from models.tt_dit.utils.test import ring_params
+
+
+@pytest.mark.parametrize(
+ "no_prompt",
+ [{"1": True, "0": False}.get(os.environ.get("NO_PROMPT"), False)],
+)
+@pytest.mark.parametrize(
+ "mesh_device, mesh_shape, num_links, dynamic_load, device_params, topology, is_fsdp",
+ [
+ # BH Galaxy 4x8 Ring — sole supported config in the first Distill PR.
+ [(4, 8), (4, 8), 2, False, ring_params, ttnn.Topology.Ring, False],
+ ],
+ ids=["bh_4x8sp1tp0_ring"],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ "width, height",
+ [
+ (832, 480),
+ (1280, 720),
+ ],
+ ids=[
+ "resolution_480p",
+ "resolution_720p",
+ ],
+)
+def test_pipeline_inference(
+ mesh_device,
+ mesh_shape,
+ num_links,
+ dynamic_load,
+ topology,
+ width,
+ height,
+ is_fsdp,
+ no_prompt,
+):
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ pil_image = PIL.Image.open(os.environ.get("PROMPT_IMAGE", "./prompt_image.png"))
+ image_prompt = [ImagePrompt(image=pil_image, frame_pos=0)]
+ # Distill bakes CFG in; negative_prompt is unused when guidance_scale == 1.0,
+ # but we keep one for parity with the base I2V test.
+ negative_prompt = ""
+
+ num_frames = 81
+ num_inference_steps = 4
+ guidance_scale = 1.0
+ guidance_scale_2 = 1.0
+
+ pipeline = WanDistillPipelineI2V.create_pipeline(
+ mesh_device=mesh_device,
+ height=height,
+ width=width,
+ num_frames=num_frames,
+ config_overrides={
+ "num_links": num_links,
+ "dynamic_load": dynamic_load,
+ "topology": topology,
+ "is_fsdp": is_fsdp,
+ },
+ )
+
+ prompt = "The cat in the hat runs up the hill to the house."
+
+ def run(*, prompt, number, seed):
+ logger.info(f"Running distill inference with prompt: '{prompt}'")
+ logger.info(f"Parameters: {height}x{width}, {num_frames} frames, {num_inference_steps} steps")
+
+ with torch.no_grad():
+ result = pipeline(
+ prompts=[prompt],
+ image_prompt=image_prompt,
+ negative_prompts=[negative_prompt],
+ num_inference_steps=num_inference_steps,
+ seed=seed,
+ guidance_scale=guidance_scale,
+ guidance_scale_2=guidance_scale_2,
+ output_type="uint8",
+ )
+
+ if hasattr(result, "frames"):
+ frames = result.frames
+ else:
+ frames = result[0] if isinstance(result, tuple) else result
+
+ logger.info("Distill inference completed successfully")
+ logger.info(f" Output shape: {frames.shape if hasattr(frames, 'shape') else 'Unknown'}")
+ logger.info(f" Output type: {type(frames)}")
+
+ if isinstance(frames, np.ndarray):
+ logger.info(f" Video data range: [{frames.min():.3f}, {frames.max():.3f}]")
+ elif isinstance(frames, torch.Tensor):
+ logger.info(f" Video data range: [{frames.min().item():.3f}, {frames.max().item():.3f}]")
+
+ frames = frames[0]
+ output_filename = f"wan_distill_i2v_{width}x{height}_{number}.mp4"
+ try:
+ from models.tt_dit.utils.video import export_to_video
+
+ export_to_video(frames, output_filename, fps=16)
+ logger.info(f"Saved video to: {output_filename}")
+ except ImportError:
+ logger.info("Could not export video - imageio_ffmpeg not available")
+
+ if no_prompt:
+ run(prompt=prompt, number=0, seed=42)
+ else:
+ for i in itertools.count():
+ new_prompt = input("Enter the input prompt, or q to exit: ")
+ if new_prompt:
+ prompt = new_prompt
+ if prompt[0] == "q":
+ break
+ run(prompt=prompt, number=i, seed=i)
+
+
+# ---------------------------------------------------------------------------
+# Random-weight smoke test
+# ---------------------------------------------------------------------------
+# Runs the full pipeline end-to-end without downloading the lightx2v safetensors
+# or the two ~28 GB transformer subfolders from Wan-AI. Tokenizer / UMT5 text
+# encoder / VAE / scheduler still come from Wan-AI/Wan2.2-I2V-A14B-Diffusers
+# (~12 GB total) and require TT_DIT_ALLOW_HF_DOWNLOAD=1 the first time.
+# Output frames are garbage by design — this only validates compile / mesh /
+# shape / CCL plumbing, not numeric quality.
+
+
+@pytest.mark.parametrize(
+ "mesh_device, mesh_shape, num_links, dynamic_load, device_params, topology, is_fsdp",
+ [
+ [(4, 8), (4, 8), 2, False, ring_params, ttnn.Topology.Ring, False],
+ ],
+ ids=["bh_4x8sp1tp0_ring"],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ "width, height",
+ [
+ (832, 480),
+ ],
+ ids=["resolution_480p"],
+)
+def test_pipeline_inference_random_weights(
+ mesh_device,
+ mesh_shape,
+ num_links,
+ dynamic_load,
+ topology,
+ width,
+ height,
+ is_fsdp,
+):
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ pil_image = PIL.Image.open(os.environ.get("PROMPT_IMAGE", "./prompt_image.png"))
+ image_prompt = [ImagePrompt(image=pil_image, frame_pos=0)]
+
+ num_frames = 81
+ num_inference_steps = 4
+
+ os.environ["TT_DIT_RANDOM_WEIGHTS"] = "1"
+ try:
+ pipeline = WanDistillPipelineI2V.create_pipeline(
+ mesh_device=mesh_device,
+ height=height,
+ width=width,
+ num_frames=num_frames,
+ config_overrides={
+ "num_links": num_links,
+ "dynamic_load": dynamic_load,
+ "topology": topology,
+ "is_fsdp": is_fsdp,
+ },
+ )
+ finally:
+ os.environ.pop("TT_DIT_RANDOM_WEIGHTS", None)
+
+ prompt = "smoke test — random weights, output is meaningless"
+ logger.info(f"Random-weight smoke test: {height}x{width}, {num_frames} frames, {num_inference_steps} steps")
+
+ with torch.no_grad():
+ result = pipeline(
+ prompts=[prompt],
+ image_prompt=image_prompt,
+ negative_prompts=[""],
+ num_inference_steps=num_inference_steps,
+ seed=42,
+ guidance_scale=1.0,
+ guidance_scale_2=1.0,
+ output_type="uint8",
+ )
+
+ if hasattr(result, "frames"):
+ frames = result.frames
+ else:
+ frames = result[0] if isinstance(result, tuple) else result
+
+ if isinstance(frames, np.ndarray):
+ rng_str = f"[{frames.min():.3f}, {frames.max():.3f}]"
+ elif isinstance(frames, torch.Tensor):
+ rng_str = f"[{frames.min().item():.3f}, {frames.max().item():.3f}]"
+ else:
+ rng_str = "n/a"
+
+ shape = frames.shape if hasattr(frames, "shape") else "Unknown"
+ logger.info(f"Random-weight smoke test PASSED. Output shape={shape}, range={rng_str}")
diff --git a/code/models/tt_dit/experimental/tests/test_pipeline_wan_svi.py b/code/models/tt_dit/experimental/tests/test_pipeline_wan_svi.py
new file mode 100644
index 0000000000000000000000000000000000000000..c3f0375c4ff962094209b9d572edf5fdc5b24eeb
--- /dev/null
+++ b/code/models/tt_dit/experimental/tests/test_pipeline_wan_svi.py
@@ -0,0 +1,273 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""End-to-end test for Wan2.2 I2V with SVI 2.0 Pro.
+
+Required env vars (or the test is skipped):
+- ``SVI_HIGH_PATH`` / ``SVI_LOW_PATH``: SVI LoRA safetensors (high/low noise)
+- ``LIGHTX2V_HIGH_PATH`` / ``LIGHTX2V_LOW_PATH``: required only when regime='comfyui'
+
+Normal optional env vars:
+- ``PROMPT_IMAGE``: anchor image for every clip (default: ``./prompt_image.png``)
+- ``PROMPT``: text prompt for generation. Pass one string for all clips, or a
+ semicolon-separated list with exactly ``SVI_NUM_CLIPS`` entries for per-clip
+ prompts.
+- ``SVI_NUM_CLIPS``: number of 81-frame clips to chain (default: ``2``)
+
+Example per-clip prompt format:
+``PROMPT="clip 1 prompt;clip 2 prompt;clip 3 prompt"`` requires
+``SVI_NUM_CLIPS=3``. Example invocation::
+
+ SVI_HIGH_PATH=/path/svi_high.safetensors \\
+ SVI_LOW_PATH=/path/svi_low.safetensors \\
+ LIGHTX2V_HIGH_PATH=/path/lx2v_high.safetensors \\
+ LIGHTX2V_LOW_PATH=/path/lx2v_low.safetensors \\
+ SVI_NUM_CLIPS=10 PROMPT_IMAGE=./example.png \\
+ pytest models/tt_dit/experimental/tests/test_pipeline_wan_svi.py \\
+ -v -k "comfyui and bh_2x4" -s
+
+See ``experimental/models/Wan2_2_SVI.md`` for advanced knobs (steps, CFG, seed,
+motion-latent handoff, overlap) and the upstream-workflow comparison.
+"""
+import os
+from dataclasses import dataclass
+from typing import List, Optional, Union
+
+import pytest
+import torch
+from loguru import logger
+from PIL import Image
+
+import ttnn
+from models.tt_dit.experimental.pipelines.pipeline_wan_svi import WanPipelineSVI
+from models.tt_dit.utils.test import line_params, ring_params
+
+PromptArg = Union[str, List[str]]
+FloatArg = Union[float, List[float]]
+
+
+_DEFAULT_PROMPT = "A golden retriever running on a sandy beach, waves in the background"
+
+
+@dataclass(frozen=True)
+class _SVIRunConfig:
+ regime: str
+ svi_high: str
+ svi_low: str
+ lightx2v_high: Optional[str]
+ lightx2v_low: Optional[str]
+ anchor_image: Image.Image
+ prompt: PromptArg
+ num_clips: int
+ num_motion_latent: int
+ num_overlap_frame: int
+ base_seed: int
+ num_inference_steps: int
+ guidance_scale: FloatArg
+ guidance_scale_2: FloatArg
+ num_frames: int
+ width: int
+ height: int
+ output_basename: str
+
+
+def _parse_per_clip_env(env_val: str, num_clips: int, name: str, cast):
+ """Parse a scalar or semicolon-separated per-clip env var."""
+ parts = [p.strip() for p in env_val.split(";")]
+ if len(parts) == 1:
+ return cast(parts[0])
+
+ if len(parts) != num_clips:
+ pytest.fail(f"{name} has {len(parts)} entries but SVI_NUM_CLIPS={num_clips}")
+ return [cast(p) for p in parts]
+
+
+def _read_svi_config(regime: str, width: int, height: int) -> _SVIRunConfig:
+ svi_high = os.environ.get("SVI_HIGH_PATH")
+ svi_low = os.environ.get("SVI_LOW_PATH")
+ if not svi_high or not svi_low:
+ pytest.skip("SVI_HIGH_PATH and SVI_LOW_PATH env vars are required for this test.")
+
+ lightx2v_high = os.environ.get("LIGHTX2V_HIGH_PATH")
+ lightx2v_low = os.environ.get("LIGHTX2V_LOW_PATH")
+ if regime == "comfyui" and (not lightx2v_high or not lightx2v_low):
+ pytest.skip("regime='comfyui' requires LIGHTX2V_HIGH_PATH / LIGHTX2V_LOW_PATH env vars.")
+
+ num_clips = int(os.environ.get("SVI_NUM_CLIPS", "2"))
+ default_steps = "50" if regime == "python" else "6"
+ default_cfg = "5.0" if regime == "python" else "1.5"
+ guidance_scale_env = os.environ.get("GUIDANCE_SCALE", default_cfg)
+
+ prompt = _parse_per_clip_env(os.environ.get("PROMPT", _DEFAULT_PROMPT), num_clips, "PROMPT", str)
+ guidance_scale = _parse_per_clip_env(guidance_scale_env, num_clips, "GUIDANCE_SCALE", float)
+ guidance_scale_2 = _parse_per_clip_env(
+ os.environ.get("GUIDANCE_SCALE_2", guidance_scale_env),
+ num_clips,
+ "GUIDANCE_SCALE_2",
+ float,
+ )
+
+ return _SVIRunConfig(
+ regime=regime,
+ svi_high=svi_high,
+ svi_low=svi_low,
+ lightx2v_high=lightx2v_high,
+ lightx2v_low=lightx2v_low,
+ anchor_image=Image.open(os.environ.get("PROMPT_IMAGE", "./prompt_image.png")),
+ prompt=prompt,
+ num_clips=num_clips,
+ num_motion_latent=int(os.environ.get("SVI_NUM_MOTION_LATENT", "1")),
+ num_overlap_frame=int(os.environ.get("SVI_NUM_OVERLAP_FRAME", "4")),
+ base_seed=int(os.environ.get("SVI_SEED", "0")),
+ num_inference_steps=int(os.environ.get("NUM_STEPS", default_steps)),
+ guidance_scale=guidance_scale,
+ guidance_scale_2=guidance_scale_2,
+ num_frames=81,
+ width=width,
+ height=height,
+ output_basename=f"wan_svi_{regime}_{num_clips}clips_{width}x{height}",
+ )
+
+
+def _pipeline_kwargs(config: _SVIRunConfig):
+ """The SVI-specific half of the constructor arguments (the config is built separately)."""
+ kwargs = dict(
+ svi_high=config.svi_high,
+ svi_low=config.svi_low,
+ regime=config.regime,
+ num_motion_latent=config.num_motion_latent,
+ num_overlap_frame=config.num_overlap_frame,
+ )
+ if config.regime == "comfyui":
+ kwargs["lightx2v_high"] = config.lightx2v_high
+ kwargs["lightx2v_low"] = config.lightx2v_low
+ return kwargs
+
+
+def _assert_video(video: torch.Tensor, *, num_clips: int, num_frames: int, num_overlap_frame: int) -> None:
+ expected_t = num_clips * num_frames - (num_clips - 1) * num_overlap_frame
+ actual_t = video.shape[0]
+ logger.info(f"Concat shape: {tuple(video.shape)} (expected T={expected_t})")
+ assert actual_t == expected_t, f"Expected {expected_t} frames, got {actual_t}"
+ assert not torch.isnan(video.float()).any(), "video contains NaN"
+
+
+def _save_outputs(video: torch.Tensor, output_basename: str) -> None:
+ video = video.detach()
+ torch.save(video, f"{output_basename}.pt")
+ logger.info(f"Saved raw tensor to: {output_basename}.pt")
+
+ try:
+ from models.tt_dit.utils.video import export_to_video
+
+ # output_type='pt_with_last_latent' decodes frames as (T, C, H, W) float;
+ # export_to_video wants (T, H, W, C) uint8.
+ arr = video
+ if arr.ndim == 4 and arr.shape[1] in (1, 3):
+ arr = arr.permute(0, 2, 3, 1).contiguous()
+ if arr.dtype != torch.uint8:
+ arr = arr.float()
+ if arr.min().item() < 0:
+ arr = (arr + 1.0) * 127.5
+ elif arr.max().item() <= 1.0 + 1e-3:
+ arr = arr * 255.0
+ arr = arr.clamp(0, 255).to(torch.uint8)
+ export_to_video(arr.numpy(), f"{output_basename}.mp4", fps=16)
+ logger.info(f"Saved video to: {output_basename}.mp4")
+ except ImportError:
+ logger.info("Could not export video - imageio_ffmpeg not available")
+
+
+def _touch(path) -> str:
+ path.write_bytes(b"")
+ return str(path)
+
+
+@pytest.mark.parametrize(
+ "mesh_device, mesh_shape, num_links, dynamic_load, device_params, topology, is_fsdp",
+ [
+ [(2, 4), (2, 4), 2, True, line_params, ttnn.Topology.Linear, False],
+ [(4, 8), (4, 8), 2, False, ring_params, ttnn.Topology.Ring, False],
+ ],
+ ids=["bh_2x4sp1tp0", "bh_4x8sp1tp0_ring"],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ "width, height",
+ [
+ (832, 480),
+ ],
+ ids=["resolution_480p"],
+)
+@pytest.mark.parametrize(
+ "regime",
+ ["python", "comfyui"],
+)
+def test_long_video(
+ mesh_device,
+ mesh_shape,
+ num_links,
+ dynamic_load,
+ topology,
+ width,
+ height,
+ is_fsdp,
+ regime,
+):
+ config = _read_svi_config(regime, width, height)
+ mesh_device = mesh_device.create_submesh(ttnn.MeshShape(*mesh_shape))
+ pipeline = WanPipelineSVI(
+ device=mesh_device,
+ config=WanPipelineSVI.default_config(
+ mesh_device=mesh_device,
+ height=config.height,
+ width=config.width,
+ num_frames=config.num_frames,
+ config_overrides={
+ "num_links": num_links,
+ "dynamic_load": dynamic_load,
+ "topology": topology,
+ "is_fsdp": is_fsdp,
+ },
+ ),
+ **_pipeline_kwargs(config),
+ )
+
+ logger.info(
+ f"SVI run: {config.num_clips} clips × {config.num_frames} frames at {width}x{height}, "
+ f"steps={config.num_inference_steps}, motion={config.num_motion_latent}, "
+ f"overlap={config.num_overlap_frame}"
+ )
+
+ with torch.no_grad():
+ video = pipeline.generate_long_video(
+ prompt=config.prompt,
+ anchor_image=config.anchor_image,
+ num_clips=config.num_clips,
+ base_seed=config.base_seed,
+ num_inference_steps=config.num_inference_steps,
+ guidance_scale=config.guidance_scale,
+ guidance_scale_2=config.guidance_scale_2,
+ partial_output_path=f"{config.output_basename}.partial.pt",
+ )
+
+ _assert_video(
+ video,
+ num_clips=config.num_clips,
+ num_frames=config.num_frames,
+ num_overlap_frame=config.num_overlap_frame,
+ )
+ _save_outputs(video, config.output_basename)
+
+
+def test_comfyui_regime_requires_lightx2v(tmp_path, expect_error):
+ """regime='comfyui' must demand LightX2V LoRA paths."""
+ with expect_error(ValueError, "lightx2v_high"):
+ WanPipelineSVI(
+ device=None,
+ config=None,
+ svi_high=_touch(tmp_path / "svi_high.safetensors"),
+ svi_low=_touch(tmp_path / "svi_low.safetensors"),
+ regime="comfyui",
+ )
diff --git a/code/models/tt_dit/experimental/utils/__init__.py b/code/models/tt_dit/experimental/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/code/models/tt_dit/experimental/utils/lightx2v_loader.py b/code/models/tt_dit/experimental/utils/lightx2v_loader.py
new file mode 100644
index 0000000000000000000000000000000000000000..3e0e2b002f18dc22de21e3a66d7d12c53a8f1aa0
--- /dev/null
+++ b/code/models/tt_dit/experimental/utils/lightx2v_loader.py
@@ -0,0 +1,199 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Loader for lightx2v Wan2.2 distilled weights.
+
+The ``lightx2v/Wan2.2-Distill-Models`` HF repo ships flat ``.safetensors`` files
+containing only the DiT weights (no ``transformer/``, ``vae/``, ``text_encoder/``
+subfolders). The keys use the *original Wan* naming
+(``blocks.X.self_attn.q.weight``, ``blocks.X.modulation``, ``head.head.weight``,
+``time_embedding.0.weight``, ...), but tt_dit's ``WanTransformer3DModel``
+consumes the *diffusers* layout (``blocks.X.attn1.to_q.weight``,
+``blocks.X.scale_shift_table``, ``proj_out.weight``,
+``condition_embedder.time_embedder.linear_1.weight``, ...).
+
+:func:`load_lightx2v_state_dict` applies :func:`rename_lightx2v_to_diffusers`
+to the loaded state dict using :func:`rename_substate` from the DiT library,
+so the returned dict matches what ``WanTransformer3DModel._prepare_torch_state``
+expects.
+"""
+from __future__ import annotations
+
+import os
+from pathlib import Path
+from typing import Mapping
+
+import torch
+from loguru import logger
+from safetensors.torch import load_file
+
+from models.tt_dit.utils.substate import rename_substate
+
+# Per-block prefix renames (lightx2v → diffusers), applied under each
+# ``blocks..`` subtree. Uses rename_substate for prefix-based renaming.
+_BLOCK_PREFIX_REMAP: list[tuple[str, str]] = [
+ # self-attention
+ ("self_attn.q", "attn1.to_q"),
+ ("self_attn.k", "attn1.to_k"),
+ ("self_attn.v", "attn1.to_v"),
+ ("self_attn.o", "attn1.to_out.0"),
+ ("self_attn.norm_q", "attn1.norm_q"),
+ ("self_attn.norm_k", "attn1.norm_k"),
+ # cross-attention
+ ("cross_attn.q", "attn2.to_q"),
+ ("cross_attn.k", "attn2.to_k"),
+ ("cross_attn.v", "attn2.to_v"),
+ ("cross_attn.o", "attn2.to_out.0"),
+ ("cross_attn.norm_q", "attn2.norm_q"),
+ ("cross_attn.norm_k", "attn2.norm_k"),
+ # feedforward
+ ("ffn.0", "ffn.net.0.proj"),
+ ("ffn.2", "ffn.net.2"),
+ # cross-attn input norm
+ ("norm3", "norm2"),
+]
+
+# Single-key renames that don't fit the prefix pattern (leaf key → leaf key).
+_BLOCK_KEY_REMAP: dict[str, str] = {
+ "modulation": "scale_shift_table",
+}
+
+# Top-level prefix renames (lightx2v → diffusers).
+_TOP_LEVEL_PREFIX_REMAP: list[tuple[str, str]] = [
+ ("text_embedding.0", "condition_embedder.text_embedder.linear_1"),
+ ("text_embedding.2", "condition_embedder.text_embedder.linear_2"),
+ ("time_embedding.0", "condition_embedder.time_embedder.linear_1"),
+ ("time_embedding.2", "condition_embedder.time_embedder.linear_2"),
+ ("time_projection.1", "condition_embedder.time_proj"),
+ ("head.head", "proj_out"),
+]
+
+_TOP_LEVEL_KEY_REMAP: dict[str, str] = {
+ "head.modulation": "scale_shift_table",
+}
+
+
+def _detect_num_blocks(state: dict[str, torch.Tensor]) -> int:
+ max_idx = -1
+ for k in state:
+ if k.startswith("blocks."):
+ parts = k.split(".")
+ if len(parts) >= 2 and parts[1].isdigit():
+ max_idx = max(max_idx, int(parts[1]))
+ return max_idx + 1
+
+
+def rename_lightx2v_to_diffusers(state: dict[str, torch.Tensor]) -> None:
+ """Rename all keys in *state* from lightx2v/native-Wan to diffusers layout, in place.
+
+ Uses :func:`rename_substate` from the DiT library for prefix-based renames.
+ """
+ num_blocks = _detect_num_blocks(state)
+
+ for i in range(num_blocks):
+ for src, dst in _BLOCK_PREFIX_REMAP:
+ rename_substate(state, f"blocks.{i}.{src}", f"blocks.{i}.{dst}")
+ for src, dst in _BLOCK_KEY_REMAP.items():
+ old_key = f"blocks.{i}.{src}"
+ if old_key in state:
+ state[f"blocks.{i}.{dst}"] = state.pop(old_key)
+
+ for src, dst in _TOP_LEVEL_PREFIX_REMAP:
+ rename_substate(state, src, dst)
+
+ for src, dst in _TOP_LEVEL_KEY_REMAP.items():
+ if src in state:
+ state[dst] = state.pop(src)
+
+
+def wan_lightx2v_to_diffusers_key(key: str) -> str:
+ """Rename one lightx2v key to its diffusers-canonical equivalent.
+
+ Convenience wrapper kept for callers that need per-key conversion
+ (e.g. LoRA key targeting). Internally builds a single-key dict and
+ applies :func:`rename_lightx2v_to_diffusers`.
+ """
+ tmp = {key: torch.empty(0)}
+ rename_lightx2v_to_diffusers(tmp)
+ return next(iter(tmp))
+
+
+class WeightsNotFoundError(FileNotFoundError):
+ """Raised when a lightx2v weight file is not on disk and downloads are disabled."""
+
+
+def _hf_cache_root() -> Path:
+ return Path(os.environ.get("HF_HOME") or Path.home() / ".cache" / "huggingface")
+
+
+def _resolve_path(
+ repo_id: str,
+ filename: str,
+ *,
+ allow_download: bool,
+ local_dir: str | None,
+) -> Path:
+ if local_dir is not None:
+ candidate = Path(local_dir) / filename
+ if candidate.is_file():
+ return candidate
+
+ repo_cache = _hf_cache_root() / "hub" / f"models--{repo_id.replace('/', '--')}" / "snapshots"
+ if repo_cache.is_dir():
+ for snap in sorted(repo_cache.iterdir(), reverse=True):
+ cand = snap / filename
+ if cand.is_file():
+ return cand
+
+ if not allow_download:
+ raise WeightsNotFoundError(
+ f"lightx2v weights '{filename}' not found for repo '{repo_id}'.\n"
+ f"Searched: {repo_cache}\n"
+ f"To download from HuggingFace, re-run with TT_DIT_ALLOW_HF_DOWNLOAD=1 "
+ f"or pass allow_download=True to the pipeline.\n"
+ f"Alternatively, place the file under a snapshot directory in: {repo_cache}"
+ )
+
+ from huggingface_hub import hf_hub_download
+
+ logger.info(f"Downloading lightx2v weights from HuggingFace: {repo_id}/{filename}")
+ return Path(hf_hub_download(repo_id=repo_id, filename=filename))
+
+
+def load_lightx2v_state_dict(
+ repo_id: str,
+ filename: str,
+ *,
+ allow_download: bool = False,
+ local_dir: str | None = None,
+ key_remap: Mapping[str, str] | None = None,
+) -> dict[str, torch.Tensor]:
+ """Load a lightx2v safetensors file and return a torch state dict.
+
+ Args:
+ repo_id: HuggingFace repo, e.g. ``"lightx2v/Wan2.2-Distill-Models"``.
+ filename: File within the repo, e.g.
+ ``"wan2.2_i2v_A14b_high_noise_lightx2v_4step.safetensors"``.
+ allow_download: When ``False`` (default), the file must already exist
+ in ``local_dir`` or the HF cache. When ``True``, missing files are
+ fetched via ``huggingface_hub.hf_hub_download``.
+ local_dir: Optional directory to check before the HF cache.
+ key_remap: Flat-dict overrides applied after the lightx2v→diffusers
+ rename. Useful for one-off renames in tests.
+
+ Returns:
+ State dict with diffusers-canonical keys ready for
+ ``WanTransformer3DModel.load_torch_state_dict``.
+ """
+ path = _resolve_path(repo_id, filename, allow_download=allow_download, local_dir=local_dir)
+ logger.info(f"Loading lightx2v state dict from '{path}'")
+ state = load_file(str(path))
+
+ rename_lightx2v_to_diffusers(state)
+
+ if key_remap:
+ for old_k, new_k in key_remap.items():
+ if old_k in state:
+ state[new_k] = state.pop(old_k)
+ return state
diff --git a/code/models/tt_dit/models/audio_vae/minimax_h3/.gitkeep b/code/models/tt_dit/models/audio_vae/minimax_h3/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/code/models/tt_dit/models/audio_vae/minimax_h3/blockings_minimax_h3_audio.py b/code/models/tt_dit/models/audio_vae/minimax_h3/blockings_minimax_h3_audio.py
new file mode 100644
index 0000000000000000000000000000000000000000..44e57a113aa7ba2267b1e9cc17880349b67c3017
--- /dev/null
+++ b/code/models/tt_dit/models/audio_vae/minimax_h3/blockings_minimax_h3_audio.py
@@ -0,0 +1,117 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""conv3d blocking stubs for the MiniMax-H3 audio VAE.
+
+``_FP32_BLOCKINGS`` in ``utils/conv3d.py`` is tuned for the **LTX** vocoder's channel
+schedule (1536 -> 768 -> ... -> 24). H3's is different at both ends -- the decoder runs
+1024 -> 512 -> ... -> **8** and the encoder 64 -> ... -> 2048 -- so every H3 audio conv
+misses the table and falls back to ``(32, 32, 1, 1, 1)``. For a 1-D conv over a long time
+axis (a 10 s clip decodes to 324000 samples) a ``T_out_block`` of 1 means one output frame
+per work unit, and the conv3d DRAM slicer cannot find a workable configuration at all.
+
+These are **stubs**, shaped after the LTX audio entries which have the same ``(k, 1, 1)``
+form: ``C_out_block`` 32, ``H``/``W`` 1, and a ``T_out_block`` in the range LTX's tuned
+values occupy (4-64). ``bruteforce_conv3d_sweep.py`` is the tool for real tuning, and that
+is a performance-pass job -- these exist only so the correctness gates can run.
+"""
+
+from __future__ import annotations
+
+from ....layers.audio_ops import DEFAULT_MAX_C_IN_BLOCK
+from ....utils.conv3d import _FP32_BLOCKINGS, aligned_channels
+
+# 16 overshoots L1 by 1.26x (1979264 B against 1572864 B) at the widest audio convs.
+T_OUT_BLOCK = 8
+C_OUT_BLOCK = 32
+
+# ``C_in_block`` is not only a performance knob: conv3d accumulates one partial sum per block over the
+# reduction and those partials are rounded, so a narrower block is measurably less accurate *in isolation*
+# -- on `conv_pre` (Cin 2048 x k 7) the error falls monotonically, 2.40e-03 at 32, 1.86e-03 at 128,
+# 1.63e-03 at 512, and with operand splitting on, 512 is 1.48x better than 128.
+#
+# **That does not transfer end to end, so 128 stays.** Measured with every other lever on: 128 gives
+# 3.20 % chain RMSE, 256 gives 3.31 % (no better), and 512 fails outright
+# (`program.cpp:1706`). The reason is structural -- the chain is dominated by the 126 AMP convs at
+# Cin 8-512, where the block cannot widen anyway, so only `conv_pre` and `dec_in_proj` would gain and
+# there are two of them against 126. Pass ``max_c_in_block`` to re-sweep. A non-default cap changes
+# the prepared weight *bytes* (`prepare_conv3d_weight_state` blocks by ``C_in_block``) with an
+# unchanged file set, which is why `weights_variant` folds it into the device-weight cache key.
+
+
+def _c_in_block(in_channels: int, max_c_in_block: int = DEFAULT_MAX_C_IN_BLOCK) -> int:
+ """Largest 32-multiple <= ``max_c_in_block`` that divides ``in_channels`` evenly.
+
+ The kernel requires ``C_in_block`` to be a multiple of the tile width and to divide the
+ padded input channel count.
+ """
+ aligned = aligned_channels(in_channels)
+ for block in range(min(max_c_in_block, aligned) // 32 * 32, 0, -32):
+ if aligned % block == 0:
+ return block
+ return 32
+
+
+def h3_audio_channel_widths(
+ *,
+ encoder_dim: int = 64,
+ encoder_rates: tuple[int, ...] = (2, 4, 4, 5, 5),
+ latent_dim: int = 2048,
+ latent_channels: int = 32,
+ decoder_dim: int = 1024,
+ decoder_rates: tuple[int, ...] = (5, 5, 2, 2, 2, 2, 2),
+) -> set[tuple[int, int]]:
+ """Every ``(in_channels, out_channels)`` pair the audio VAE's convolutions use."""
+ pairs: set[tuple[int, int]] = set()
+
+ # Encoder: conv_in, then per stage three residual units at dim//2 plus a strided
+ # channel-doubling conv.
+ pairs.add((aligned_channels(1), encoder_dim))
+ dim = encoder_dim
+ for stride in encoder_rates:
+ inner = dim
+ dim *= 2
+ pairs.add((inner, inner)) # residual unit k7 and k1
+ pairs.add((inner, dim)) # strided conv
+ pairs.add((dim, latent_dim))
+
+ # Decoder: conv_pre, the transposed upsamplers' inner convs, the AMP blocks, conv_post.
+ pairs.add((latent_channels, latent_dim)) # dec_in_proj
+ pairs.add((latent_dim, decoder_dim)) # conv_pre
+ channels = decoder_dim
+ for _ in decoder_rates:
+ nxt = channels // 2
+ pairs.add((channels, nxt)) # upsampler inner conv
+ pairs.add((aligned_channels(nxt), aligned_channels(nxt))) # AMP convs
+ channels = nxt
+ pairs.add((aligned_channels(channels), aligned_channels(1))) # conv_post -> mono
+ return pairs
+
+
+def register_h3_audio_blockings(*, max_c_in_block: int = DEFAULT_MAX_C_IN_BLOCK, **config) -> int:
+ """Seed ``_FP32_BLOCKINGS`` for every H3 audio conv shape. Returns the number added.
+
+ ``setdefault``, so a swept value that later lands in ``conv3d.py`` wins over these.
+ Kernels cover every size the model uses: 1 and 3 (projections), 4/7/8/9/10/11 (AMP
+ blocks, strided encoder convs and transposed upsamplers).
+ """
+ kernels = (1, 3, 4, 7, 8, 9, 10, 11)
+ added = 0
+ for in_channels, out_channels in h3_audio_channel_widths(**config):
+ for kernel in kernels:
+ key = (aligned_channels(in_channels), max(32, out_channels), (kernel, 1, 1))
+ blocking = (_c_in_block(in_channels, max_c_in_block), C_OUT_BLOCK, T_OUT_BLOCK, 1, 1)
+ existing = _FP32_BLOCKINGS.get(key)
+ if existing is None:
+ _FP32_BLOCKINGS[key] = blocking
+ added += 1
+ elif existing[2] > T_OUT_BLOCK:
+ # A few of H3's (C_in, C_out, kernel) triples coincide with LTX's -- e.g.
+ # (128, 64, (4,1,1)) is LTX's ups[3] -- and LTX's tuned T_out_block of 32 is
+ # sized for its tensors, not H3's: at H3's widths it overshoots L1 (1979264 B
+ # against 1572864 B). Cap the temporal block for the shapes H3 actually uses
+ # rather than deferring to an entry tuned for a different model.
+ _FP32_BLOCKINGS[key] = (existing[0], existing[1], T_OUT_BLOCK, existing[3], existing[4])
+ added += 1
+ return added
diff --git a/code/models/tt_dit/models/audio_vae/minimax_h3/convert_minimax_h3_audio.py b/code/models/tt_dit/models/audio_vae/minimax_h3/convert_minimax_h3_audio.py
new file mode 100644
index 0000000000000000000000000000000000000000..65fb911df56e2efc1b75aeb5105a2ce9447e5a9d
--- /dev/null
+++ b/code/models/tt_dit/models/audio_vae/minimax_h3/convert_minimax_h3_audio.py
@@ -0,0 +1,160 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Torch-side conversion of the MiniMax-H3 audio VAE checkpoint. No ``ttnn`` here.
+
+``tt_dit`` has no weight-norm support anywhere -- ``git grep weight_g`` over
+``models/tt_dit`` returns nothing -- because the contract is that weight norm is removed
+before a state dict reaches it. The H3 audio checkpoint stores ``weight_g``/``weight_v``
+for all ~172 of its convolutions, so that fusion has to happen somewhere, and a single
+torch-side conversion boundary is the right place: it is orthogonal to every conv class,
+it would otherwise be duplicated across four of them, and it is testable with **zero
+device time**.
+
+The fusion rule is uniform because ``torch.nn.utils.weight_norm`` is used at its default
+``dim=0`` everywhere::
+
+ weight = weight_g * weight_v / ||weight_v|| (L2 over all axes but axis 0)
+
+The trap is that for ``ConvTranspose1d`` axis 0 is **``in_channels``**, not
+``out_channels``, because its weight is stored ``(in, out, k)``. Fusing over the wrong
+axis yields a well-formed decoder that sounds subtly wrong, which is the kind of bug that
+gets misattributed to precision for days -- hence
+:func:`assert_weight_norm_axes_consistent`.
+
+Also folded here, so the device side stays simple:
+
+* ``attn.q_bias`` / ``attn.zero_k_bias`` / ``attn.v_bias`` -> one ``attn.qkv.bias``. The
+ reference passes ``cat(q_bias, zero_k_bias, v_bias)`` to a single ``F.linear``; the k
+ third is a frozen zero, which is asserted before being discarded.
+* ``activations.{2i}`` -> ``acts1.{i}`` and ``activations.{2i+1}`` -> ``acts2.{i}``, which
+ is the layout ``vocoder_ltx.AMPBlock1`` already expects.
+* ``ups.{i}.0`` -> ``ups.{i}`` and ``activation_post`` -> ``act_post``.
+"""
+
+from __future__ import annotations
+
+import re
+
+import torch
+
+
+def fuse_weight_norm(weight_g: torch.Tensor, weight_v: torch.Tensor) -> torch.Tensor:
+ """``weight_g * weight_v / ||weight_v||``, L2-reduced over every axis but axis 0.
+
+ Matches ``torch.nn.utils.weight_norm`` at its default ``dim=0`` for both ``Conv1d``
+ (axis 0 = out_channels) and ``ConvTranspose1d`` (axis 0 = **in_channels**). The result
+ keeps the checkpoint's own layout; transposing is the conv class's job, not ours.
+ """
+ norm = weight_v.flatten(1).norm(dim=1).view(-1, *([1] * (weight_v.dim() - 1)))
+ return weight_g * weight_v / norm
+
+
+def assert_weight_norm_axes_consistent(state: dict[str, torch.Tensor]) -> None:
+ """Guard the axis-0 assumption on every weight-normed conv in the checkpoint.
+
+ For each ``weight_g``/``weight_v`` pair, ``weight_g`` must be a per-axis-0 scalar --
+ shape ``(axis0, 1, 1)`` -- and any sibling ``bias`` must be length ``out_channels``.
+ For a ``Conv1d`` that is ``weight_v.shape[0]``; for a ``ConvTranspose1d`` it is
+ ``weight_v.shape[1]``. A bias that matches neither means the layout is not what this
+ converter assumes.
+ """
+ for key in sorted(k for k in state if k.endswith(".weight_g")):
+ prefix = key[: -len(".weight_g")]
+ weight_g = state[key]
+ weight_v = state[f"{prefix}.weight_v"]
+ assert (
+ weight_g.shape[0] == weight_v.shape[0]
+ ), f"{prefix}: weight_g axis 0 is {weight_g.shape[0]} but weight_v axis 0 is {weight_v.shape[0]}"
+ assert all(
+ size == 1 for size in weight_g.shape[1:]
+ ), f"{prefix}: weight_g {tuple(weight_g.shape)} is not a per-axis-0 scalar, so dim=0 is the wrong axis"
+ bias = state.get(f"{prefix}.bias")
+ if bias is not None:
+ assert bias.shape[0] in (weight_v.shape[0], weight_v.shape[1]), (
+ f"{prefix}: bias {bias.shape[0]} matches neither weight_v axis "
+ f"({weight_v.shape[0]}, {weight_v.shape[1]})"
+ )
+
+
+def fuse_all_weight_norms(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """Replace every ``weight_g``/``weight_v`` pair with a single fused ``weight``."""
+ assert_weight_norm_axes_consistent(state)
+ fused = {}
+ for key, value in state.items():
+ if key.endswith(".weight_v"):
+ prefix = key[: -len(".weight_v")]
+ fused[f"{prefix}.weight"] = fuse_weight_norm(state[f"{prefix}.weight_g"], value)
+ elif key.endswith(".weight_g"):
+ continue
+ else:
+ fused[key] = value
+ return fused
+
+
+def fuse_attention_biases(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """Fold ``q_bias`` / ``zero_k_bias`` / ``v_bias`` into one ``qkv.bias``.
+
+ The reference builds ``cat(q_bias, zero_k_bias, v_bias)`` for a single fused
+ projection. ``zero_k_bias`` is a frozen zero buffer; that is asserted rather than
+ assumed, so a future checkpoint that unfreezes it fails loudly instead of silently
+ losing a bias term.
+ """
+ out = dict(state)
+ prefixes = {k[: -len(".q_bias")] for k in state if k.endswith(".q_bias")}
+ for prefix in sorted(prefixes):
+ q_bias = out.pop(f"{prefix}.q_bias")
+ k_bias = out.pop(f"{prefix}.zero_k_bias", None)
+ v_bias = out.pop(f"{prefix}.v_bias")
+ if k_bias is None:
+ k_bias = torch.zeros_like(q_bias)
+ else:
+ assert (
+ torch.count_nonzero(k_bias) == 0
+ ), f"{prefix}.zero_k_bias is not all zero -- it is a real parameter and cannot be dropped"
+ out[f"{prefix}.qkv.bias"] = torch.cat([q_bias, k_bias, v_bias], dim=0)
+ return out
+
+
+_ACTIVATION_RE = re.compile(r"^(?P.*?)activations\.(?P\d+)\.(?P.*)$")
+
+
+def remap_amp_activations(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """``activations.{2i}`` -> ``acts1.{i}``, ``activations.{2i+1}`` -> ``acts2.{i}``.
+
+ H3 stores one flat list of six activations per AMP block; ``vocoder_ltx.AMPBlock1``
+ holds them as two lists of three, interleaved. Getting this backwards produces a
+ plausible-looking decoder, so it is worth doing in one obvious place.
+ """
+ out = {}
+ for key, value in state.items():
+ match = _ACTIVATION_RE.match(key)
+ if match is None:
+ out[key] = value
+ continue
+ index = int(match["index"])
+ target = "acts1" if index % 2 == 0 else "acts2"
+ out[f"{match['prefix']}{target}.{index // 2}.{match['rest']}"] = value
+ return out
+
+
+def remap_decoder_names(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """``ups.{i}.0`` -> ``ups.{i}``, ``activation_post`` -> ``act_post``."""
+ out = {}
+ for key, value in state.items():
+ key = re.sub(r"(^|\.)ups\.(\d+)\.0\.", r"\1ups.\2.", key)
+ key = key.replace("activation_post.", "act_post.")
+ out[key] = value
+ return out
+
+
+def convert_minimax_h3_audio_state_dict(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """The whole conversion, in the order the steps depend on each other.
+
+ Weight norm is fused first so the later renames only ever move plain ``weight`` keys.
+ """
+ converted = fuse_all_weight_norms(state)
+ converted = fuse_attention_biases(converted)
+ converted = remap_amp_activations(converted)
+ return remap_decoder_names(converted)
diff --git a/code/models/tt_dit/models/audio_vae/minimax_h3/decoder_minimax_h3_audio.py b/code/models/tt_dit/models/audio_vae/minimax_h3/decoder_minimax_h3_audio.py
new file mode 100644
index 0000000000000000000000000000000000000000..e40ee87a68a820da0517e9dd16988515bc38d798
--- /dev/null
+++ b/code/models/tt_dit/models/audio_vae/minimax_h3/decoder_minimax_h3_audio.py
@@ -0,0 +1,171 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""MiniMax-H3 audio VAE decode: a 1x1 latent projection into a BigVGAN vocoder.
+
+Almost all of this already exists. ``audio_vae/vocoder_ltx.py``'s ``Vocoder`` is a
+BigVGAN-v2 AMP1 stack whose ``forward`` is *identical* to H3's reference
+``MiniMaxH3AudioBigVGANDecoder.forward`` -- ``conv_pre``, then per stage
+``ups[i]`` followed by the mean of three parallel AMP blocks, then ``act_post``,
+``conv_post`` and ``clamp(-1, 1)`` -- including the row-major
+``resblocks[i * num_kernels + j]`` ordering. Every loop in it is generic over
+``len(upsample_rates)``, so H3's **seven** stages need only configuration, not code.
+
+The channel schedule falls out of the same ``upsample_initial_channel // 2**i`` rule:
+1024 -> 512, 256, 128, 64, 32, 16, **8**, and ``conv_post`` takes that 8 to 1 mono
+channel. Stereo is carried as batch 2, so nothing here is stereo-aware.
+
+What this module adds is only:
+
+* ``dec_in_proj``, the 1x1 conv from 32 latent channels to the vocoder's 2048;
+* the wiring that keeps its output on device instead of round-tripping to host between
+ the projection and the vocoder.
+
+Naming is chosen so the converted checkpoint loads with no fixups: the vocoder is held as
+a child called ``decoder``, matching ``decoder.conv_pre`` / ``decoder.ups.N`` /
+``decoder.resblocks.N`` / ``decoder.act_post`` / ``decoder.conv_post``, and the projection
+is ``dec_in_proj``. The ``ups.{i}.0 -> ups.{i}`` and ``activations -> acts1/acts2`` remaps,
+and the weight-norm fusion, all happen in ``convert_minimax_h3_audio.py``.
+
+fp32 throughout: ``vocoder_ltx``'s docstring records that bf16 accumulation measurably
+degrades spectral metrics through its 108-conv chain, and H3's is longer still.
+"""
+
+from __future__ import annotations
+
+import torch
+
+import ttnn
+
+from ....layers.audio_ops import DEFAULT_MAX_C_IN_BLOCK, _AlignedOutConv1d
+from ....layers.module import Module
+from ....parallel.config import ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils.tensor import local_device_to_torch
+from ..vocoder_ltx import Vocoder
+from .blockings_minimax_h3_audio import register_h3_audio_blockings
+
+TILE_HEIGHT = 32
+
+
+class MiniMaxH3AudioDecoder(Module):
+ """``(B, 32, T)`` latents to ``(B, 1, T * 800)`` mono waveform at 32 kHz."""
+
+ def __init__(
+ self,
+ *,
+ latent_channels: int = 32,
+ latent_dim: int = 2048,
+ decoder_dim: int = 1024,
+ decoder_rates: tuple[int, ...] = (5, 5, 2, 2, 2, 2, 2),
+ decoder_kernel_sizes: tuple[int, ...] = (9, 9, 4, 4, 4, 4, 4),
+ resblock_kernel_sizes: tuple[int, ...] = (3, 7, 11),
+ resblock_dilation_sizes: tuple[tuple[int, ...], ...] = ((1, 3, 5), (1, 3, 5), (1, 3, 5)),
+ mesh_device: ttnn.MeshDevice,
+ dtype: ttnn.DataType = ttnn.float32,
+ parallel_config: ParallelFactor | None = None,
+ ccl_manager: CCLManager | None = None,
+ split_mode: str = "full",
+ max_c_in_block: int = DEFAULT_MAX_C_IN_BLOCK,
+ ) -> None:
+ super().__init__()
+ self.mesh_device = mesh_device
+ self.dtype = dtype
+ self.parallel_config = parallel_config
+ self.latent_channels = latent_channels
+ self.latent_dim = latent_dim
+ self.hop_length = 1
+ for rate in decoder_rates:
+ self.hop_length *= rate
+
+ # The conv3d precision levers default to accurate. H3-only: LTX constructs the same conv
+ # classes with its own fast defaults. Kept as attributes so the pipeline's device-weight
+ # cache key (`weights_variant`) reads the exact values this module was built with. The
+ # depthwise resample filters need no lever: their conv1d kernel is exact in fp32
+ # (see compute_depthwise_conv1d.cpp).
+ self.split_mode = split_mode
+ self.max_c_in_block = max_c_in_block
+
+ # H3's audio channel schedule differs from LTX's at both ends, so every conv misses
+ # _FP32_BLOCKINGS. Seed stubs before any conv is built; see that module for why stubs.
+ register_h3_audio_blockings(max_c_in_block=max_c_in_block)
+
+ # k1 conv, so no padding mode to get wrong. _AlignedOutConv1d rather than the base
+ # class per its own docstring: a non-32-multiple out count reaches conv3d and
+ # produces a buffer whose page size does not divide its length.
+ self.dec_in_proj = _AlignedOutConv1d(
+ latent_channels,
+ latent_dim,
+ kernel_size=1,
+ bias=True,
+ mesh_device=mesh_device,
+ dtype=dtype,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ split_mode=split_mode,
+ )
+ self.decoder = Vocoder(
+ resblock_kernel_sizes=list(resblock_kernel_sizes),
+ upsample_rates=list(decoder_rates),
+ upsample_kernel_sizes=list(decoder_kernel_sizes),
+ resblock_dilation_sizes=[list(d) for d in resblock_dilation_sizes],
+ upsample_initial_channel=decoder_dim,
+ in_channels=latent_dim,
+ out_channels=1,
+ use_tanh_at_final=False,
+ apply_final_activation=True,
+ use_bias_at_final=False, # H3's conv_post has no bias
+ mesh_device=mesh_device,
+ dtype=dtype,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ # H3-only opt-in: LTX's vocoder keeps its default single-conv weights.
+ split_mode=split_mode,
+ )
+
+ def _project_latents_device(self, latents_BCT: torch.Tensor) -> torch.Tensor:
+ """``(B, 32, T)`` -> ``(B, 2048, T)`` through ``dec_in_proj`` on device.
+
+ Torch in, torch out, matching the stage convention ``bwe_ltx.VocoderWithBWE``
+ uses (``_compute_mel_device``, ``_resample_device``): each stage owns its own
+ upload and readback, which keeps the stages independently testable.
+ """
+ x = latents_BCT.transpose(1, 2).float().contiguous() # (B, T, C)
+ t_pad = self._t_padding(x.shape[1])
+ if t_pad:
+ x = torch.nn.functional.pad(x, (0, 0, 0, t_pad))
+
+ x_device = ttnn.from_torch(x, device=self.mesh_device, layout=ttnn.ROW_MAJOR_LAYOUT, dtype=self.dtype)
+ projected_device = self.dec_in_proj(x_device)
+ projected = local_device_to_torch(projected_device).float()
+ if t_pad:
+ # Crop the alignment padding back off before handing T to the vocoder, which
+ # applies its own padding for its own sharding.
+ projected = projected[:, : x.shape[1] - t_pad]
+ return projected.transpose(1, 2).contiguous() # (B, 2048, T)
+
+ def forward(self, latents_BCT: torch.Tensor, *, traced: bool = False) -> torch.Tensor:
+ """``(B, latent_channels, T)`` torch in, ``(B, 1, T * hop_length)`` torch out.
+
+ ``traced`` replays a captured device graph for the vocoder instead of dispatching it
+ op by op. The vocoder is ~70 % host-bound, so this is its dominant lever -- unlike the
+ *visual* halves, which measure 1.00x traced because they are device-bound. Needs a
+ ``trace_region_size`` on the mesh device; the first call at a shape captures, later
+ calls replay.
+ """
+ _, channels, _ = latents_BCT.shape
+ assert channels == self.latent_channels, f"expected {self.latent_channels} latent channels, got {channels}"
+ projected = self._project_latents_device(latents_BCT)
+ return self.decoder.forward_BCT_traced(projected) if traced else self.decoder.forward_BCT(projected)
+
+ def _t_padding(self, num_frames: int) -> int:
+ """T padding needed for tile-aligned per-chip shards; zero when unsharded."""
+ if self.parallel_config is None or self.parallel_config.factor <= 1:
+ return 0
+ align = TILE_HEIGHT * self.parallel_config.factor
+ return (-num_frames) % align
+
+ def release_trace(self) -> None:
+ """Free the vocoder's captured traces; safe when none is active."""
+ self.decoder.release_trace()
diff --git a/code/models/tt_dit/models/audio_vae/minimax_h3/encoder_minimax_h3_audio.py b/code/models/tt_dit/models/audio_vae/minimax_h3/encoder_minimax_h3_audio.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3c68909ada24e2ce2d75f90c5f0b92377a1ea70
--- /dev/null
+++ b/code/models/tt_dit/models/audio_vae/minimax_h3/encoder_minimax_h3_audio.py
@@ -0,0 +1,405 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""MiniMax-H3 audio VAE encode: a DAC waveform encoder plus a causal-attention projection.
+
+``(B, 1, samples)`` at 32 kHz to a 40 Hz, 32-channel posterior -- a hop of
+``prod(encoder_rates) = 800``. Mono; stereo is batch 2.
+
+Structure mirrors the reference one-for-one so the converted checkpoint loads with no
+fixups. Everything nests under ``block`` ModuleLists because the reference uses
+``nn.Sequential``, which is why the checkpoint keys read
+``encoder.block.1.block.0.block.1.weight``.
+
+Reused rather than rewritten, all from ``layers/audio_ops.py``:
+
+* ``Snake`` with ``alpha_logscale=False`` -- the DAC activation stores ``alpha`` linearly,
+ unlike the decoder's ``SnakeBeta`` which is log-scale;
+* ``DilatedConv1d`` for the residual units' k=7 convs, whose ``eff_k // 2`` same-padding
+ already equals the reference's ``((7-1)*d)//2``;
+* ``_AlignedOutConv1d`` everywhere else, per its own docstring: a non-32-multiple channel
+ count reaching ``conv3d`` produces a buffer whose page size does not divide its length.
+
+The five strided channel-doubling convs pair ``kernel = 2 * stride`` with
+``padding = ceil(stride / 2)``, passed through ``Conv1dViaConv3d``'s optional ``padding``
+argument: the derived default ``eff_k // 2`` gives ``L/stride + 1`` instead of ``L/stride``.
+The default stands, so LTX call sites are unaffected.
+
+The reference's residual-unit shortcut crop is a no-op here -- the dilated conv's padding is
+exact-same, so the crop width is zero -- but the lengths are asserted rather than assumed.
+"""
+
+from __future__ import annotations
+
+import math
+
+import torch
+
+import ttnn
+
+from ....layers.audio_ops import DEFAULT_MAX_C_IN_BLOCK, Snake, _AlignedOutConv1d
+from ....layers.module import Module, ModuleList
+from ....layers.normalization import LayerNorm
+from ....parallel.config import ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils.tensor import local_device_to_torch
+from ..vocoder_ltx import DilatedConv1d
+from .blockings_minimax_h3_audio import register_h3_audio_blockings
+
+
+def _snake_row_major(snake: Snake, x_BTC: ttnn.Tensor) -> ttnn.Tensor:
+ """Apply ``Snake`` and hand back ROW_MAJOR, which is what the next conv requires.
+
+ LTX only ever uses ``Snake``/``SnakeBeta`` inside ``Activation1d``, whose resamplers own
+ the layout. H3's DAC encoder applies a **bare** Snake between convolutions, and the
+ elementwise chain inside it returns TILE, so ``Conv1dViaConv3d``'s ROW_MAJOR assertion
+ fires without this.
+ """
+ return ttnn.to_layout(snake(x_BTC), ttnn.ROW_MAJOR_LAYOUT)
+
+
+class MiniMaxH3AudioResidualUnit(Module):
+ """``Snake -> dilated Conv1d(k=7) -> Snake -> Conv1d(k=1)``, plus a residual add."""
+
+ def __init__(
+ self,
+ dim: int,
+ dilation: int,
+ *,
+ mesh_device: ttnn.MeshDevice,
+ dtype: ttnn.DataType = ttnn.float32,
+ parallel_config: ParallelFactor | None = None,
+ ccl_manager: CCLManager | None = None,
+ split_mode: str = "off",
+ ) -> None:
+ super().__init__()
+ shared = dict(mesh_device=mesh_device, dtype=dtype, parallel_config=parallel_config, ccl_manager=ccl_manager)
+ # Conv-only levers: Snake takes neither, so they ride a separate dict from `shared`.
+ levers = dict(split_mode=split_mode)
+ self.block = ModuleList(
+ [
+ Snake(dim, alpha_logscale=False, **{k: v for k, v in shared.items() if k != "ccl_manager"}),
+ DilatedConv1d(dim, dim, kernel_size=7, dilation=dilation, **shared, **levers),
+ Snake(dim, alpha_logscale=False, **{k: v for k, v in shared.items() if k != "ccl_manager"}),
+ _AlignedOutConv1d(dim, dim, kernel_size=1, **shared, **levers),
+ ]
+ )
+
+ def forward(self, x_BTC: ttnn.Tensor) -> ttnn.Tensor:
+ residual = x_BTC
+ for layer in self.block:
+ x_BTC = _snake_row_major(layer, x_BTC) if isinstance(layer, Snake) else layer(x_BTC)
+ assert x_BTC.shape[1] == residual.shape[1], (
+ f"residual unit changed T from {residual.shape[1]} to {x_BTC.shape[1]}; "
+ "the reference's centre crop is only a no-op when the dilated conv is exact-same"
+ )
+ return ttnn.add(residual, x_BTC)
+
+
+class MiniMaxH3AudioEncoderBlock(Module):
+ """Three residual units at dilations 1/3/9, then a strided channel-doubling conv."""
+
+ def __init__(
+ self,
+ dim: int,
+ stride: int,
+ *,
+ mesh_device: ttnn.MeshDevice,
+ dtype: ttnn.DataType = ttnn.float32,
+ parallel_config: ParallelFactor | None = None,
+ ccl_manager: CCLManager | None = None,
+ split_mode: str = "off",
+ ) -> None:
+ super().__init__()
+ shared = dict(mesh_device=mesh_device, dtype=dtype, parallel_config=parallel_config, ccl_manager=ccl_manager)
+ levers = dict(split_mode=split_mode)
+ inner = dim // 2
+ self.stride = stride
+ self.block = ModuleList(
+ [
+ MiniMaxH3AudioResidualUnit(inner, 1, **shared, **levers),
+ MiniMaxH3AudioResidualUnit(inner, 3, **shared, **levers),
+ MiniMaxH3AudioResidualUnit(inner, 9, **shared, **levers),
+ Snake(inner, alpha_logscale=False, **{k: v for k, v in shared.items() if k != "ccl_manager"}),
+ # kernel = 2 * stride with padding = ceil(stride / 2) is what makes the output
+ # exactly L / stride; the class's default eff_k // 2 would give one extra frame.
+ _AlignedOutConv1d(
+ inner,
+ dim,
+ kernel_size=2 * stride,
+ stride=stride,
+ padding=math.ceil(stride / 2),
+ **shared,
+ **levers,
+ ),
+ ]
+ )
+
+ def forward(self, x_BTC: ttnn.Tensor) -> ttnn.Tensor:
+ expected_out = x_BTC.shape[1] // self.stride
+ for layer in self.block:
+ x_BTC = _snake_row_major(layer, x_BTC) if isinstance(layer, Snake) else layer(x_BTC)
+ assert (
+ x_BTC.shape[1] == expected_out
+ ), f"strided conv produced T={x_BTC.shape[1]}, expected {expected_out} -- check the padding override"
+ return x_BTC
+
+
+class MiniMaxH3AudioDACEncoder(Module):
+ """``conv(1 -> encoder_dim, k7) -> 5 encoder blocks -> Snake -> conv(k3)``."""
+
+ def __init__(
+ self,
+ *,
+ encoder_dim: int = 64,
+ encoder_rates: tuple[int, ...] = (2, 4, 4, 5, 5),
+ latent_dim: int = 2048,
+ mesh_device: ttnn.MeshDevice,
+ dtype: ttnn.DataType = ttnn.float32,
+ parallel_config: ParallelFactor | None = None,
+ ccl_manager: CCLManager | None = None,
+ split_mode: str = "off",
+ ) -> None:
+ super().__init__()
+ shared = dict(mesh_device=mesh_device, dtype=dtype, parallel_config=parallel_config, ccl_manager=ccl_manager)
+ no_ccl = {k: v for k, v in shared.items() if k != "ccl_manager"}
+ levers = dict(split_mode=split_mode)
+
+ layers: list[Module] = [_AlignedOutConv1d(1, encoder_dim, kernel_size=7, **shared, **levers)]
+ dim = encoder_dim
+ for stride in encoder_rates:
+ dim *= 2
+ layers.append(MiniMaxH3AudioEncoderBlock(dim, stride, **shared, **levers))
+ layers.append(Snake(dim, alpha_logscale=False, **no_ccl))
+ layers.append(_AlignedOutConv1d(dim, latent_dim, kernel_size=3, **shared, **levers))
+ self.block = ModuleList(layers)
+ self.hop_length = math.prod(encoder_rates)
+
+ def forward(self, x_BTC: ttnn.Tensor) -> ttnn.Tensor:
+ for layer in self.block:
+ x_BTC = _snake_row_major(layer, x_BTC) if isinstance(layer, Snake) else layer(x_BTC)
+ return x_BTC
+
+
+class MiniMaxH3AudioCausalAttention(Module):
+ """Causal attention that narrows ``in_dim`` to ``out_dim`` by pooling, not concatenating.
+
+ The heads are **mean-pooled away** rather than concatenated, and the surviving head
+ dimension is average-pooled from ``in_dim // num_heads`` down to ``out_dim``. Since
+ ``256 % 32 == 0``, that pool is exactly a mean over contiguous groups of 8, so it is a
+ reshape and a mean rather than anything adaptive.
+
+ The checkpoint stores one fused biasless ``qkv`` plus separate ``q_bias``/``v_bias`` and
+ a frozen-zero ``zero_k_bias``; the converter folds all three into ``qkv.bias``.
+ """
+
+ def __init__(
+ self,
+ in_dim: int,
+ out_dim: int,
+ num_heads: int,
+ *,
+ mesh_device: ttnn.MeshDevice,
+ dtype: ttnn.DataType = ttnn.float32,
+ ) -> None:
+ super().__init__()
+ from ....layers.linear import Linear
+
+ self.in_dim = in_dim
+ self.out_dim = out_dim
+ self.num_heads = num_heads
+ self.head_dim = in_dim // num_heads
+ assert (
+ self.head_dim % out_dim == 0
+ ), f"head_dim {self.head_dim} must be a whole multiple of out_dim {out_dim} for the pool to be a plain mean"
+ self.pool_group = self.head_dim // out_dim
+ self.mesh_device = mesh_device
+ self.dtype = dtype
+
+ self.qkv = Linear(in_dim, 3 * in_dim, bias=True, mesh_device=mesh_device, dtype=dtype)
+ self.proj = Linear(out_dim, out_dim, bias=True, mesh_device=mesh_device, dtype=dtype)
+
+ def forward(self, x_BSC: ttnn.Tensor) -> ttnn.Tensor:
+ batch, seq_len, _ = x_BSC.shape
+ # The qkv projection is 2048 -> 6144 over the whole latent sequence, and at a 10 s
+ # clip (405 frames, batch 2 for stereo) the default matmul blocking overshoots L1
+ # (1979264 B against 1572864 B). A smaller block trades a little throughput for a
+ # working shape; the real fix is a tuned config, which is a performance-pass job.
+ qkv = self.qkv(x_BSC, default_block_size=(4, 4, 4))
+ query, key, value = (
+ ttnn.permute(ttnn.reshape(part, (batch, seq_len, self.num_heads, self.head_dim)), (0, 2, 1, 3))
+ for part in ttnn.chunk(qkv, 3, dim=-1)
+ )
+ # SDPA is bf16-only (`sdpa_device_operation.cpp:43`), so this one op is a bf16 island
+ # inside the otherwise-fp32 audio path -- the same shape of compromise the visual
+ # encoder makes for GroupNorm.
+ query, key, value = (
+ ttnn.typecast(t, ttnn.bfloat16) if t.get_dtype() != ttnn.bfloat16 else t for t in (query, key, value)
+ )
+ attended = ttnn.transformer.scaled_dot_product_attention(query, key, value, is_causal=True)
+ if attended.get_dtype() != self.dtype:
+ attended = ttnn.typecast(attended, self.dtype)
+
+ # (B, heads, S, head_dim) -> mean over heads -> (B, S, head_dim)
+ pooled = ttnn.mean(ttnn.permute(attended, (0, 2, 1, 3)), dim=2)
+ pooled = ttnn.reshape(pooled, (batch, seq_len, self.out_dim, self.pool_group))
+ pooled = ttnn.mean(pooled, dim=3)
+ return self.proj(pooled)
+
+
+class MiniMaxH3AudioGeGluMlp(Module):
+ """Pre-norm GeGLU: ``w2(gelu_tanh(w0(norm(x))) * w1(norm(x)))``."""
+
+ def __init__(
+ self,
+ in_features: int,
+ hidden_features: int,
+ *,
+ mesh_device: ttnn.MeshDevice,
+ dtype: ttnn.DataType = ttnn.float32,
+ ) -> None:
+ super().__init__()
+ from ....layers.linear import Linear
+
+ self.norm = LayerNorm(in_features, norm_eps=1e-5, bias=True, mesh_device=mesh_device)
+ self.w0 = Linear(in_features, hidden_features, bias=True, mesh_device=mesh_device, dtype=dtype)
+ self.w1 = Linear(in_features, hidden_features, bias=True, mesh_device=mesh_device, dtype=dtype)
+ self.w2 = Linear(hidden_features, in_features, bias=True, mesh_device=mesh_device, dtype=dtype)
+
+ def forward(self, x: ttnn.Tensor) -> ttnn.Tensor:
+ normed = self.norm(x)
+ # nn.GELU(approximate="tanh")
+ gated = ttnn.gelu(self.w0(normed), fast_and_approximate_mode=True)
+ return self.w2(ttnn.mul(gated, self.w1(normed)))
+
+
+class MiniMaxH3AudioAttnProjection(Module):
+ """``pre_block``: ``proj(norm3(x)) + attn(norm1(x))``, then ``+ mlp(norm2(.))``.
+
+ Note the residual is a **projected** bypass, not ``x`` itself -- the block changes width
+ from ``latent_dim`` to ``latent_channels`` inside the residual, which is unusual and
+ easy to get wrong.
+ """
+
+ def __init__(
+ self,
+ in_dim: int,
+ out_dim: int,
+ num_heads: int,
+ *,
+ mlp_ratio: int = 2,
+ mesh_device: ttnn.MeshDevice,
+ dtype: ttnn.DataType = ttnn.float32,
+ ) -> None:
+ super().__init__()
+ from ....layers.linear import Linear
+
+ self.norm1 = LayerNorm(in_dim, norm_eps=1e-5, bias=True, mesh_device=mesh_device)
+ self.attn = MiniMaxH3AudioCausalAttention(in_dim, out_dim, num_heads, mesh_device=mesh_device, dtype=dtype)
+ self.proj = Linear(in_dim, out_dim, bias=True, mesh_device=mesh_device, dtype=dtype)
+ self.norm3 = LayerNorm(in_dim, norm_eps=1e-5, bias=True, mesh_device=mesh_device)
+ self.norm2 = LayerNorm(out_dim, norm_eps=1e-5, bias=True, mesh_device=mesh_device)
+ self.mlp = MiniMaxH3AudioGeGluMlp(out_dim, out_dim * mlp_ratio, mesh_device=mesh_device, dtype=dtype)
+
+ def forward(self, x: ttnn.Tensor) -> ttnn.Tensor:
+ hidden = ttnn.add(self.proj(self.norm3(x)), self.attn(self.norm1(x)))
+ return ttnn.add(hidden, self.mlp(self.norm2(hidden)))
+
+
+class MiniMaxH3AudioEncoder(Module):
+ """Waveform to posterior: DAC trunk, ``pre_block``, then ``mean_proj`` / ``logs_proj``.
+
+ Returns ``(mean, logs)`` as torch tensors. Sampling stays on host, matching how the
+ visual encoder leaves its posterior draw to the caller.
+ """
+
+ def __init__(
+ self,
+ *,
+ encoder_dim: int = 64,
+ encoder_rates: tuple[int, ...] = (2, 4, 4, 5, 5),
+ latent_dim: int = 2048,
+ latent_channels: int = 32,
+ num_attention_heads: int = 8,
+ mesh_device: ttnn.MeshDevice,
+ dtype: ttnn.DataType = ttnn.float32,
+ parallel_config: ParallelFactor | None = None,
+ ccl_manager: CCLManager | None = None,
+ split_mode: str = "full",
+ max_c_in_block: int = DEFAULT_MAX_C_IN_BLOCK,
+ ) -> None:
+ super().__init__()
+ self.mesh_device = mesh_device
+ self.dtype = dtype
+ self.latent_channels = latent_channels
+ self.hop_length = math.prod(encoder_rates)
+
+ # The precision levers default to accurate, same rationale as the decoder. H3-only: LTX
+ # constructs the same conv classes with its own fast defaults. Kept as attributes so the
+ # pipeline's device-weight cache key (`weights_variant`) reads the exact values this module
+ # was built with.
+ self.split_mode = split_mode
+ self.max_c_in_block = max_c_in_block
+
+ # Every H3 audio conv shape misses _FP32_BLOCKINGS; seed stubs before any conv is built.
+ register_h3_audio_blockings(max_c_in_block=max_c_in_block)
+
+ self.encoder = MiniMaxH3AudioDACEncoder(
+ encoder_dim=encoder_dim,
+ encoder_rates=encoder_rates,
+ latent_dim=latent_dim,
+ mesh_device=mesh_device,
+ dtype=dtype,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ split_mode=split_mode,
+ )
+ self.pre_block = MiniMaxH3AudioAttnProjection(
+ latent_dim, latent_channels, num_attention_heads, mesh_device=mesh_device, dtype=dtype
+ )
+ self.mean_proj = _AlignedOutConv1d(
+ latent_channels,
+ latent_channels,
+ kernel_size=1,
+ mesh_device=mesh_device,
+ dtype=dtype,
+ split_mode=split_mode,
+ )
+ self.logs_proj = _AlignedOutConv1d(
+ latent_channels,
+ latent_channels,
+ kernel_size=1,
+ mesh_device=mesh_device,
+ dtype=dtype,
+ split_mode=split_mode,
+ )
+
+ def forward(self, waveform_BCT: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
+ """``(B, 1, samples)`` torch in, ``(mean, logs)`` each ``(B, 32, samples/800)`` torch."""
+ _, channels, num_samples = waveform_BCT.shape
+ assert channels == 1, f"the audio VAE is mono; stereo is batch 2. Got {channels} channels"
+ assert (
+ num_samples % self.hop_length == 0
+ ), f"{num_samples} samples is not a whole number of {self.hop_length}-sample hops"
+
+ x = waveform_BCT.transpose(1, 2).float().contiguous() # (B, T, 1)
+ x_device = ttnn.from_torch(x, device=self.mesh_device, layout=ttnn.ROW_MAJOR_LAYOUT, dtype=self.dtype)
+
+ trunk = self.encoder(x_device)
+ # pre_block is a transformer block, so it wants TILE; the convs want ROW_MAJOR.
+ projected = self.pre_block(ttnn.to_layout(trunk, ttnn.TILE_LAYOUT))
+ projected = ttnn.to_layout(projected, ttnn.ROW_MAJOR_LAYOUT)
+
+ # The upload replicates and nothing here is fractured, so every device holds the same
+ # result: read back one. A bare ``ttnn.to_torch`` asserts ``buffers.size() == 1`` and so
+ # only works on a single-device mesh. Same fix as ``MiniMaxH3AudioDecoder.__call__``.
+ def read(tensor: ttnn.Tensor) -> torch.Tensor:
+ # See `MiniMaxH3AudioDecoder.__call__`: a storage slice keeps the parent's distribution
+ # metadata and the converter rejects it on a multi-host mesh. The helper reads a shard this
+ # host owns instead, which for a replicated tensor is the whole answer.
+ return local_device_to_torch(tensor).float()
+
+ mean = read(self.mean_proj(projected))
+ logs = read(self.logs_proj(projected))
+ return mean.transpose(1, 2).contiguous(), logs.transpose(1, 2).contiguous()
diff --git a/code/models/tt_dit/models/transformers/ltx/attention_ltx.py b/code/models/tt_dit/models/transformers/ltx/attention_ltx.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b75bc4afbb6eaf99461e6d3c78e93d5847b060a
--- /dev/null
+++ b/code/models/tt_dit/models/transformers/ltx/attention_ltx.py
@@ -0,0 +1,870 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import os
+
+import torch
+
+import ttnn
+from models.common.utility_functions import is_blackhole
+
+from ....layers.linear import ColParallelLinear, LoRAColParallelLinear, maybe_cast_activation, resolve_output_dtype
+from ....layers.module import Module
+from ....layers.normalization import DistributedRMSNorm
+from ....parallel.config import DiTParallelConfig
+from ....parallel.manager import CCLManager
+from ....utils.matmul import get_fabric_agmm_config, get_matmul_config
+from ....utils.substate import pop_substate, rename_substate
+from ....utils.tensor import bf16_tensor
+from .quant_config import LtxQuantProfile
+
+# to_gate_logits and to_q/to_qkv are both ColParallelLinear fed the SAME activation, and each fuses
+# its own TP all-gather of it (all_gather_minimal_matmul_async) — the activation crosses the fabric
+# twice. The fused gather barely overlaps its matmul, so gathering once explicitly and running both
+# projections as plain matmuls on the gathered tensor is the same math for one gather less.
+# Set to 0 to restore the double gather for an A/B.
+LTX_DEDUP_GATE_GATHER = os.environ.get("LTX_DEDUP_GATE_GATHER", "1") in ("1", "true", "True")
+
+
+class LTXAttention(Module):
+ # Map from (is_blackhole, sp_factor, tp_factor) -> (q_chunk_size, k_chunk_size)
+ sdpa_chunk_size_map = {
+ (False, 2, 4): (256, 256),
+ (False, 8, 4): (256, 256),
+ (True, 2, 2): (128, 512),
+ (True, 8, 4): (128, 512),
+ }
+
+ # V2A cross ring-SDPA q_chunk = the per-device audio Q (audio_N / sp_factor), keyed by
+ # (is_blackhole, sp, tp); assumes audio_N=256. A q_chunk wider than the Q shard pads the
+ # query rows and burns ~2x SDPA compute, so it must track sp, not be fixed per model.
+ # k_chunk reuses the self-attn ring value; misses fall back to the self-attn ring q_chunk.
+ # TODO: audio_N depends on video duration (ceil(round((num_frames/fps)*25), 32*sp)); derive
+ # q_chunk from the actual Q shard (q_BHNE.shape[2]) instead of hardcoding per mesh.
+ cross_ring_sdpa_q_chunk_map = {
+ (True, 4, 2): 64, # BH 2x4
+ (True, 8, 4): 32, # BH 4x8
+ }
+ default_sdpa_chunk_size = (256, 256)
+
+ # Per-stage ring-SDPA chunk, keyed by (is_blackhole, sp, tp, N); N is the SP-padded
+ # sequence length passed to the op. Misses fall back to sdpa_chunk_size_map.
+ ring_sdpa_chunk_by_n = {
+ (True, 8, 4, 9728): (96, 256),
+ (True, 8, 4, 38912): (192, 512),
+ }
+
+ # Per-shape cross-attn SDPA chunk, keyed by (is_blackhole, q_seq, kv_seq); seqs are
+ # the per-device Q shard and full K. Misses fall back to sdpa_program_config.
+ sdpa_chunk_by_shape = {
+ (True, 1216, 32): (128, 128), # video text cross-attn, stage 1
+ (True, 4864, 32): (192, 128), # video text cross-attn, stage 2
+ (True, 1216, 256): (128, 128), # audio->video cross-attn, stage 1
+ (True, 4864, 256): (192, 256), # audio->video cross-attn, stage 2
+ }
+
+ def __init__(
+ self,
+ *,
+ dim: int,
+ num_heads: int,
+ qk_norm: bool = True,
+ eps: float = 1e-6,
+ mesh_device: ttnn.MeshDevice,
+ ccl_manager: CCLManager | None = None,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool = False,
+ is_self: bool = True,
+ context_dim: int | None = None,
+ query_input_dim: int | None = None,
+ output_dim: int | None = None,
+ apply_gated_attention: bool = False,
+ quant_config: LtxQuantProfile | None = None,
+ lora_enabled: bool = False,
+ ) -> None:
+ super().__init__()
+
+ assert dim % num_heads == 0
+ self.dim = dim
+ self.num_heads = num_heads
+ self.head_dim = dim // num_heads
+ self.qk_norm = qk_norm
+ self.eps = eps
+ self.is_self = is_self
+ self.query_input_dim = query_input_dim or dim
+ self.output_dim = output_dim or dim
+
+ self.mesh_device = mesh_device
+ self.ccl_manager = ccl_manager
+ self.parallel_config = parallel_config
+
+ self.n_local_heads = self.num_heads // self.parallel_config.tensor_parallel.factor
+
+ fsdp_mesh_axis = self.parallel_config.sequence_parallel.mesh_axis if is_fsdp else None
+
+ rms_kwargs = {
+ "embedding_dim": dim,
+ "norm_eps": eps,
+ "norm_elementwise_affine": True,
+ "bias": False,
+ "mesh_device": mesh_device,
+ "mesh_axis": parallel_config.tensor_parallel.mesh_axis,
+ "ccl_manager": ccl_manager,
+ }
+
+ self.norm_q = DistributedRMSNorm(**rms_kwargs)
+ self.norm_k = DistributedRMSNorm(**rms_kwargs)
+
+ col_parallel_kwargs = {
+ "bias": True,
+ "mesh_device": mesh_device,
+ "mesh_axis": parallel_config.tensor_parallel.mesh_axis,
+ "fsdp_mesh_axis": fsdp_mesh_axis,
+ "ccl_manager": ccl_manager,
+ }
+
+ self.kv_input_dim = context_dim if (context_dim is not None and not is_self) else dim
+
+ # Per-linear precision comes entirely from the quant profile (None => bf16 everywhere, matching
+ # the unquantized model). The profile owns the to_out carve-out and the LTX_QUANT_ACTIVATIONS
+ # gating; here each role's linear just spreads the kwargs the profile hands it.
+ def qk(role):
+ return quant_config.linear_kwargs(role) if quant_config is not None else {}
+
+ # Fuse-mode LoRA lives in weight.data, so the chunked (to_qkv/to_kv) and
+ # fused-addcmul (to_out) paths work unchanged; runtime mode is unsupported here. The quant
+ # kwargs forward through LoRAColParallelLinear's **kwargs to the base linear.
+ ColCls = LoRAColParallelLinear if lora_enabled else ColParallelLinear
+
+ # Fuse the per-head gate into the QKV/Q matmul via variable-width chunks
+ tp_factor = parallel_config.tensor_parallel.factor
+ uses_fused_agmm = tp_factor > 1 and ccl_manager is not None and ccl_manager.topology == ttnn.Topology.Ring
+ # A fused linear carries a single weight dtype, but the quant profile holds the gate at bf16
+ # while quantizing qkv/q, so the two cannot share one matmul: fusion is bf16-only.
+ self.fuse_gate = apply_gated_attention and uses_fused_agmm and quant_config is None
+
+ # Gate is num_heads/TP columns per device (sub-tile); pad to a whole tile so it's a legal chunk.
+ self.gate_width_per_device = self.n_local_heads
+ self.gate_padded_per_device = ((self.n_local_heads + ttnn.TILE_SIZE - 1) // ttnn.TILE_SIZE) * ttnn.TILE_SIZE
+ gate_fused_width = self.gate_padded_per_device * tp_factor
+
+ if is_self:
+ if self.fuse_gate:
+ self.to_qkv = ColCls(
+ dim,
+ 3 * dim + gate_fused_width,
+ chunks=4,
+ chunk_sizes=[dim, dim, dim, gate_fused_width],
+ **col_parallel_kwargs,
+ **qk("qkv"),
+ )
+ else:
+ self.to_qkv = ColCls(dim, 3 * dim, chunks=3, **col_parallel_kwargs, **qk("qkv"))
+ else:
+ if self.fuse_gate:
+ self.to_q = ColCls(
+ self.query_input_dim,
+ dim + gate_fused_width,
+ chunks=2,
+ chunk_sizes=[dim, gate_fused_width],
+ **col_parallel_kwargs,
+ **qk("q"),
+ )
+ else:
+ self.to_q = ColCls(self.query_input_dim, dim, **col_parallel_kwargs, **qk("q"))
+ self.to_kv = ColCls(self.kv_input_dim, 2 * dim, chunks=2, **col_parallel_kwargs, **qk("kv"))
+
+ self.to_out = ColCls(
+ dim,
+ self.output_dim,
+ bias=True,
+ mesh_device=mesh_device,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ fsdp_mesh_axis=fsdp_mesh_axis,
+ ccl_manager=ccl_manager,
+ **qk("out"),
+ )
+
+ # Per-head gate, sharded on num_heads to match the SDPA-output head layout
+ self.apply_gated_attention = apply_gated_attention
+ if apply_gated_attention and not self.fuse_gate:
+ # Standalone gate weight stays bf16 (the model's working dtype) while consuming the shared
+ # bf8 activation, so its output is pinned back to bf16 like the quantized projections.
+ self.to_gate_logits = ColParallelLinear(
+ in_features=self.query_input_dim,
+ out_features=self.num_heads,
+ bias=True,
+ dtype=ttnn.bfloat16,
+ mesh_device=mesh_device,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ fsdp_mesh_axis=fsdp_mesh_axis,
+ ccl_manager=ccl_manager,
+ **qk("gate"),
+ )
+
+ self.dummy_joint_input = bf16_tensor(torch.zeros((1, self.n_local_heads, 0, self.head_dim)), device=mesh_device)
+
+ full_grid = self.mesh_device.compute_with_storage_grid_size()
+ self.sdpa_program_config = ttnn.SDPAProgramConfig(
+ compute_with_storage_grid_size=full_grid,
+ q_chunk_size=256,
+ k_chunk_size=256,
+ exp_approx_mode=False,
+ )
+
+ self.sdpa_worker_grid = (full_grid.x - 1, full_grid.y)
+ mesh_key = (
+ is_blackhole(),
+ self.parallel_config.sequence_parallel.factor,
+ self.parallel_config.tensor_parallel.factor,
+ )
+ ring_sdpa_chunk_size = self.sdpa_chunk_size_map.get(mesh_key, self.default_sdpa_chunk_size)
+ self.ring_sdpa_program_config = ttnn.SDPAProgramConfig(
+ compute_with_storage_grid_size=self.sdpa_worker_grid,
+ q_chunk_size=ring_sdpa_chunk_size[0],
+ k_chunk_size=ring_sdpa_chunk_size[1],
+ exp_approx_mode=False,
+ )
+ self._ring_pc_by_n = {
+ n: ttnn.SDPAProgramConfig(
+ compute_with_storage_grid_size=self.sdpa_worker_grid,
+ q_chunk_size=chunk[0],
+ k_chunk_size=chunk[1],
+ exp_approx_mode=False,
+ )
+ for (b, sp, tp, n), chunk in self.ring_sdpa_chunk_by_n.items()
+ if (b, sp, tp) == mesh_key
+ }
+ self._sdpa_pc_by_shape = {
+ (q, kv): ttnn.SDPAProgramConfig(
+ compute_with_storage_grid_size=full_grid,
+ q_chunk_size=chunk[0],
+ k_chunk_size=chunk[1],
+ exp_approx_mode=False,
+ )
+ for (b, q, kv), chunk in self.sdpa_chunk_by_shape.items()
+ if b == mesh_key[0]
+ }
+
+ # V2A cross ring SDPA: q_chunk matched to the per-device audio Q; k_chunk reuses the
+ # self-attn ring value.
+ cross_ring_q_chunk = self.cross_ring_sdpa_q_chunk_map.get(mesh_key, ring_sdpa_chunk_size[0])
+ self.cross_ring_sdpa_program_config = ttnn.SDPAProgramConfig(
+ compute_with_storage_grid_size=self.sdpa_worker_grid,
+ q_chunk_size=cross_ring_q_chunk,
+ k_chunk_size=ring_sdpa_chunk_size[1],
+ exp_approx_mode=False,
+ )
+
+ # All SDPA (ring + cross) runs HiFi2, matching the Wan attention config.
+ self.sdpa_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ self.mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi2,
+ math_approx_mode=False,
+ fp32_dest_acc_en=False,
+ )
+
+ self.rope_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ self.mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi4,
+ math_approx_mode=False,
+ fp32_dest_acc_en=True,
+ packer_l1_acc=True,
+ )
+
+ device_grid = self.mesh_device.compute_with_storage_grid_size()
+ self.core_grid = ttnn.CoreGrid(x=device_grid.x, y=device_grid.y)
+
+ # Attention QKV/out matmuls run HiFi2, matching the Wan attention config.
+ self.mm_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ self.mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi2,
+ math_approx_mode=True,
+ fp32_dest_acc_en=True,
+ packer_l1_acc=True,
+ )
+
+ # Under a quant preset, override the attention compute configs to the profile's fidelity.
+ # Self-attn additionally swaps in the ring-SDPA compute and narrows its SDPA inputs; cross-attn
+ # leaves SDPA and _sdpa_input_dtype unset, so forward's getattr(self, "_sdpa_input_dtype", None)
+ # keeps cross SDPA at bf16.
+ if quant_config is not None:
+ arch = self.mesh_device.arch()
+ self.mm_compute_kernel_config = quant_config.mm_compute_config(arch)
+ if self.is_self:
+ self.sdpa_compute_kernel_config, self._sdpa_input_dtype = quant_config.sdpa_self_config(arch)
+
+ def _prepare_torch_state(self, state: dict[str, torch.Tensor]) -> None:
+ rename_substate(state, "to_out.0", "to_out")
+ rename_substate(state, "q_norm", "norm_q")
+ rename_substate(state, "k_norm", "norm_k")
+
+ # Permute Q/K (and norm_q/norm_k) channels per head from checkpoint SPLIT rotation to
+ # the INTERLEAVED layout rotary_embedding_llama expects: even output lanes take the
+ # first half of each head's channels, odd lanes the second half.
+ D = self.head_dim
+ D_half = D // 2
+ perm = torch.empty(D, dtype=torch.long)
+ perm[0::2] = torch.arange(D_half)
+ perm[1::2] = torch.arange(D_half, D)
+
+ def _permute_qk(t: torch.Tensor) -> torch.Tensor:
+ rest = t.shape[1:]
+ t = t.reshape(self.num_heads, D, *rest).index_select(1, perm)
+ return t.reshape(self.num_heads * D, *rest)
+
+ for nk in ("norm_q.weight", "norm_k.weight"):
+ if nk in state:
+ state[nk] = _permute_qk(state[nk])
+
+ def _interleave_heads(tensors: list[torch.Tensor]):
+ n_dev = self.parallel_config.tensor_parallel.factor
+ tensors = [t.T for t in tensors]
+ tensors = [t.reshape(t.shape[0], n_dev, self.n_local_heads, self.head_dim) for t in tensors]
+ merged = torch.cat(tensors, dim=2)
+ merged = merged.reshape(merged.shape[0], len(tensors) * self.num_heads * self.head_dim)
+ return merged.T
+
+ def _pad_gate_to_tile(t: torch.Tensor) -> torch.Tensor:
+ """Zero-pad each device's gate block from n_local_heads up to a whole tile (device-major)."""
+ n_dev = self.parallel_config.tensor_parallel.factor
+ pad = self.gate_padded_per_device - self.n_local_heads
+ if pad == 0:
+ return t
+ blocks = []
+ for d in range(n_dev):
+ blocks.append(t[d * self.n_local_heads : (d + 1) * self.n_local_heads])
+ blocks.append(torch.zeros(pad, *t.shape[1:], dtype=t.dtype))
+ return torch.cat(blocks, dim=0)
+
+ def _gate_state_or_zeros(reference: torch.Tensor, want_bias: bool) -> dict[str, torch.Tensor]:
+ """Gate substate, zero-filled when the checkpoint has none (zeros -> identity gating)."""
+ g = pop_substate(state, "to_gate_logits")
+ if "weight" not in g:
+ g["weight"] = torch.zeros(self.num_heads, reference.shape[1], dtype=reference.dtype)
+ if want_bias and "bias" not in g:
+ g["bias"] = torch.zeros(self.num_heads, dtype=reference.dtype)
+ return g
+
+ def _interleave_device_major(tensors: list[torch.Tensor]):
+ """Like _interleave_heads but for per-device widths that differ (q/k/v head_dim vs gate)."""
+ n_dev = self.parallel_config.tensor_parallel.factor
+ reshaped = []
+ for t in tensors:
+ t = t.T # [in, out]
+ assert t.shape[1] % n_dev == 0, f"projection width {t.shape[1]} not divisible by TP={n_dev}"
+ reshaped.append(t.reshape(t.shape[0], n_dev, t.shape[1] // n_dev))
+ merged = torch.cat(reshaped, dim=2)
+ return merged.reshape(merged.shape[0], -1).T
+
+ if self.is_self:
+ q_state = pop_substate(state, "to_q")
+ k_state = pop_substate(state, "to_k")
+ v_state = pop_substate(state, "to_v")
+
+ q_state["weight"] = _permute_qk(q_state["weight"])
+ k_state["weight"] = _permute_qk(k_state["weight"])
+ if "bias" in q_state:
+ q_state["bias"] = _permute_qk(q_state["bias"])
+ if "bias" in k_state:
+ k_state["bias"] = _permute_qk(k_state["bias"])
+
+ if self.fuse_gate:
+ # Fold the gate in as a 4th chunk, device-major: device d holds [q_d | k_d | v_d | gate_d].
+ g_state = _gate_state_or_zeros(q_state["weight"], "bias" in q_state)
+ g_state = {k: _pad_gate_to_tile(v) for k, v in g_state.items()}
+ state["to_qkv.weight"] = _interleave_device_major(
+ [q_state["weight"], k_state["weight"], v_state["weight"], g_state["weight"]]
+ )
+ if "bias" in q_state:
+ bias = _interleave_device_major(
+ [
+ q_state["bias"].unsqueeze(-1),
+ k_state["bias"].unsqueeze(-1),
+ v_state["bias"].unsqueeze(-1),
+ g_state["bias"].unsqueeze(-1),
+ ]
+ )
+ state["to_qkv.bias"] = bias.squeeze(-1)
+ else:
+ state["to_qkv.weight"] = _interleave_heads([q_state["weight"], k_state["weight"], v_state["weight"]])
+ if "bias" in q_state:
+ bias = _interleave_heads(
+ [q_state["bias"].unsqueeze(-1), k_state["bias"].unsqueeze(-1), v_state["bias"].unsqueeze(-1)]
+ )
+ state["to_qkv.bias"] = bias.squeeze(-1)
+ else:
+ k_state = pop_substate(state, "to_k")
+ v_state = pop_substate(state, "to_v")
+
+ k_state["weight"] = _permute_qk(k_state["weight"])
+ if "bias" in k_state:
+ k_state["bias"] = _permute_qk(k_state["bias"])
+
+ if "to_q.weight" in state:
+ state["to_q.weight"] = _permute_qk(state["to_q.weight"])
+ if "to_q.bias" in state:
+ state["to_q.bias"] = _permute_qk(state["to_q.bias"])
+
+ if self.fuse_gate and "to_q.weight" in state:
+ # Cross-attn: gate rides along with Q as a 2nd chunk.
+ g_state = _gate_state_or_zeros(state["to_q.weight"], "to_q.bias" in state)
+ g_state = {k: _pad_gate_to_tile(v) for k, v in g_state.items()}
+ state["to_q.weight"] = _interleave_device_major([state["to_q.weight"], g_state["weight"]])
+ if "to_q.bias" in state and "bias" in g_state:
+ bias = _interleave_device_major([state["to_q.bias"].unsqueeze(-1), g_state["bias"].unsqueeze(-1)])
+ state["to_q.bias"] = bias.squeeze(-1)
+
+ state["to_kv.weight"] = _interleave_heads([k_state["weight"], v_state["weight"]])
+ if "bias" in k_state:
+ bias = _interleave_heads([k_state["bias"].unsqueeze(-1), v_state["bias"].unsqueeze(-1)])
+ state["to_kv.bias"] = bias.squeeze(-1)
+
+ def _to_out_fused_addcmul(
+ self,
+ x: ttnn.Tensor,
+ addcmul_residual: ttnn.Tensor,
+ addcmul_gate: ttnn.Tensor,
+ compute_kernel_config=None,
+ parallel_config: DiTParallelConfig | None = None,
+ dtype=None,
+ ) -> ttnn.Tensor:
+ """Fused to_out projection + addcmul: output = residual + (matmul(x, W) + bias) * gate."""
+ to_out = self.to_out
+ # to_out inlines the AG-matmul rather than calling ColParallelLinear.forward, so it has to
+ # honour the activation cast itself. The addcmul residual/gate stay bf16 — the kernel ties
+ # their tile size to the weight's, not the activation's — and the output is the residual
+ # stream, so it must be pinned back to bf16 rather than inheriting the bf8 activation.
+ x = maybe_cast_activation(x, to_out.activation_dtype)
+ if to_out.pin_output_bf16:
+ dtype = resolve_output_dtype(dtype, x)
+
+ if to_out.fsdp_mesh_axis is not None and to_out.mesh_device.shape[to_out.fsdp_mesh_axis] > 1:
+ unsqueezed_weight = ttnn.unsqueeze_to_4D(to_out.weight.data)
+ weight = self.ccl_manager.all_gather_persistent_buffer(
+ unsqueezed_weight, dim=2, mesh_axis=to_out.fsdp_mesh_axis
+ )
+ weight = ttnn.reshape(weight, (weight.shape[-2], weight.shape[-1]))
+ else:
+ weight = to_out.weight.data
+
+ if parallel_config is not None and parallel_config.tensor_parallel.factor > 1:
+ M, K, N_out = x.padded_shape[-2], weight.padded_shape[-2], weight.padded_shape[-1]
+ full_grid = self.mesh_device.compute_with_storage_grid_size()
+
+ # Known shapes route the addcmul to_out to the strided AG-matmul (out = a + scalar*matmul*b)
+ fabric_cfg = get_fabric_agmm_config(M, K, N_out, 1, full_grid)
+ if fabric_cfg is not None:
+ tp_axis = parallel_config.tensor_parallel.mesh_axis
+ dram = ttnn.MemoryConfig(ttnn.TensorMemoryLayout.INTERLEAVED, ttnn.BufferType.DRAM)
+ fabric_matmul_config = ttnn.MinimalMatmulConfig(
+ M_block_size=fabric_cfg.M_block_size,
+ K_block_size=fabric_cfg.K_block_size,
+ N_block_size=fabric_cfg.N_block_size,
+ subblock_h=fabric_cfg.subblock_h,
+ subblock_w=fabric_cfg.subblock_w,
+ compute_with_storage_grid_size=fabric_cfg.mm_core_grid,
+ )
+ outputs = ttnn.experimental.strided_all_gather_minimal_matmul_async(
+ x,
+ weight,
+ persistent_output_buffer=self.ccl_manager.get_ag_ping_pong_buffer(
+ x.shape, 3, tp_axis, dtype=x.get_dtype()
+ ),
+ dim=3,
+ multi_device_global_semaphore=self.ccl_manager.get_strided_ag_mm_semaphore(
+ tp_axis, fabric_cfg.num_workers_per_link
+ ),
+ strided_all_gather_core_grid_offset=fabric_cfg.ag_core_grid_offset,
+ num_links=self.ccl_manager.num_links,
+ memory_config_ag=dram,
+ topology=self.ccl_manager.topology,
+ cluster_axis=tp_axis,
+ bias=to_out.bias.data if to_out.bias is not None else None,
+ config=fabric_matmul_config,
+ memory_config_mm=dram,
+ compute_kernel_config=compute_kernel_config or to_out.compute_config,
+ num_workers_per_link=fabric_cfg.num_workers_per_link,
+ num_buffers_per_channel=fabric_cfg.num_buffers_per_channel,
+ read_local_slice_from_input=True,
+ fused_ternary_input_a=addcmul_residual,
+ fused_ternary_input_b=addcmul_gate,
+ fused_ternary_scalar=1.0,
+ chunks=1,
+ )
+ # Op returns [all_gather_output, matmul_chunk_0]; take the single matmul chunk.
+ return outputs[1]
+
+ core_grid = ttnn.CoreCoord(full_grid.x, full_grid.y - 1)
+ matmul_config = get_matmul_config(M, K, N_out, core_grid)
+
+ ag_persistent_buffer = self.ccl_manager.get_ag_ping_pong_buffer(
+ x.shape, 3, parallel_config.tensor_parallel.mesh_axis, dtype=x.get_dtype()
+ )
+ ag_global_semaphores = self.ccl_manager.get_ag_ping_pong_semaphore(
+ parallel_config.tensor_parallel.mesh_axis
+ )
+ output = ttnn.experimental.all_gather_minimal_matmul_async(
+ input_tensor=x,
+ weight_tensor=weight,
+ bias_tensor=to_out.bias.data if to_out.bias is not None else None,
+ config=matmul_config,
+ compute_kernel_config=compute_kernel_config or to_out.compute_config,
+ persistent_output_buffer=ag_persistent_buffer,
+ multi_device_global_semaphore=ag_global_semaphores,
+ num_links=self.ccl_manager.num_links,
+ topology=self.ccl_manager.topology,
+ cluster_axis=parallel_config.tensor_parallel.mesh_axis,
+ barrier_semaphore=None,
+ force_transpose=True,
+ num_workers_per_link=full_grid.x // self.ccl_manager.num_links,
+ num_buffers_per_channel=48 if not is_blackhole() else 24,
+ scalar=1.0,
+ addcmul_input_tensor1=addcmul_residual,
+ addcmul_input_tensor2=addcmul_gate,
+ dtype=dtype,
+ )[0]
+ else:
+ M, K, N_out = x.padded_shape[-2], x.padded_shape[-1], weight.padded_shape[-1]
+ core_grid = self.mesh_device.compute_with_storage_grid_size()
+ matmul_config = get_matmul_config(M, K, N_out, core_grid)
+
+ output = ttnn.experimental.dit_minimal_matmul_addcmul_fused(
+ x,
+ weight,
+ 1.0,
+ addcmul_residual,
+ addcmul_gate,
+ bias_tensor=to_out.bias.data if to_out.bias is not None else None,
+ config=matmul_config,
+ compute_kernel_config=compute_kernel_config or to_out.compute_config,
+ dtype=dtype,
+ )
+ return output
+
+ def _gate_is_live(self) -> bool:
+ """True when the gate projection will actually run (and so will gather its input).
+
+ A fused gate has no projection of its own — it falls out of the QKV/Q matmul — so it neither
+ runs nor gathers, and ``to_gate_logits`` is never built.
+ """
+ return self.apply_gated_attention and not self.fuse_gate and self.to_gate_logits.weight._data is not None
+
+ def _compute_gate(
+ self, spatial_1BND: ttnn.Tensor, qkv_parallel_config: DiTParallelConfig | None
+ ) -> ttnn.Tensor | None:
+ """Per-head gate 2 * sigmoid(to_gate_logits(x)); returns (B, H_local, N, 1) or None."""
+ if not self._gate_is_live():
+ return None
+
+ gate_logits = self.to_gate_logits(spatial_1BND, parallel_config=qkv_parallel_config)
+ return self._gate_from_logits(gate_logits)
+
+ def _unpad_gate_logits(self, gate_logits: ttnn.Tensor) -> ttnn.Tensor:
+ """Drop the tile padding from the fused gate chunk, leaving n_local_heads real columns."""
+ if self.gate_padded_per_device == self.gate_width_per_device:
+ return gate_logits
+ shape = gate_logits.shape
+ return ttnn.slice(
+ gate_logits,
+ [0, 0, 0, 0],
+ [shape[0], shape[1], shape[2], self.gate_width_per_device],
+ )
+
+ def _gate_from_logits(self, gate_logits: ttnn.Tensor) -> ttnn.Tensor:
+ """2 * sigmoid(logits) as (B, H_local, N, 1); shared by the fused and unfused paths."""
+ gate = ttnn.multiply(ttnn.sigmoid(gate_logits), 2.0)
+ return ttnn.permute(gate, (1, 3, 2, 0))
+
+ def forward(
+ self,
+ spatial_1BND: ttnn.Tensor,
+ N: int,
+ prompt_1BLP: ttnn.Tensor | None = None,
+ rope_cos: ttnn.Tensor | None = None,
+ rope_sin: ttnn.Tensor | None = None,
+ trans_mat: ttnn.Tensor | None = None,
+ addcmul_residual: ttnn.Tensor | None = None,
+ addcmul_gate: ttnn.Tensor | None = None,
+ k_rope_cos: ttnn.Tensor | None = None,
+ k_rope_sin: ttnn.Tensor | None = None,
+ attn_mask: ttnn.Tensor | None = None,
+ skip_qk: bool = False,
+ kv_replicated: bool = False,
+ kv_logical_n: int | None = None,
+ ) -> ttnn.Tensor:
+ """Same interface as WanAttention.forward(); pass k_rope_cos/sin for separate K RoPE
+ in A2V/V2A cross-attention."""
+ if rope_cos is not None:
+ assert rope_sin is not None
+ assert trans_mat is not None, "INTERLEAVED RoPE requires trans_mat (load-time Q/K permute assumes it)"
+
+ use_nonfused_agmm = (self.ccl_manager.topology == ttnn.Topology.Linear) and (
+ self.parallel_config.tensor_parallel.factor > 1
+ )
+ # With the gate on, the gate and Q/QKV projections would each fuse a gather of the same
+ # activation; hoisting one explicit gather feeds both and halves the fabric traffic here.
+ dedup_gate_gather = (
+ LTX_DEDUP_GATE_GATHER
+ and not use_nonfused_agmm
+ and self._gate_is_live()
+ and self.parallel_config.tensor_parallel.factor > 1
+ )
+ if use_nonfused_agmm or dedup_gate_gather:
+ # Cast BEFORE the gather, not inside the linears downstream of it: this path hoists the
+ # gather out so Q/QKV (and the gate) can share it, so the fabric payload is this tensor.
+ # Casting it at the linear would happen on the already-gathered result and shrink nothing.
+ qkv_linear = self.to_qkv if self.is_self else self.to_q
+ spatial_1BND = maybe_cast_activation(spatial_1BND, qkv_linear.activation_dtype)
+ spatial_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_1BND, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+
+ qkv_parallel_config = None if (use_nonfused_agmm or dedup_gate_gather) else self.parallel_config
+
+ # When fused, the gate falls out of the QKV/Q matmul below; otherwise it's its own projection.
+ gate_bhne = None if self.fuse_gate else self._compute_gate(spatial_1BND, qkv_parallel_config)
+
+ if self.is_self:
+ if self.fuse_gate:
+ q_1BNF, k_1BNF, v_1BNF, gate_logits = self.to_qkv(
+ spatial_1BND,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=qkv_parallel_config,
+ )
+ gate_bhne = self._gate_from_logits(self._unpad_gate_logits(gate_logits))
+ else:
+ q_1BNF, k_1BNF, v_1BNF = self.to_qkv(
+ spatial_1BND,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=qkv_parallel_config,
+ )
+ else:
+ kv_input = prompt_1BLP if prompt_1BLP is not None else spatial_1BND
+ # Cross K/V: gather TP-sharded context for to_kv (replicated text prompt is already full).
+ kv_parallel_config = None
+ if prompt_1BLP is not None and self.parallel_config.tensor_parallel.factor > 1:
+ local_k = kv_input.shape[-1]
+ kv_is_tp_sharded = local_k * self.parallel_config.tensor_parallel.factor == self.kv_input_dim
+ if kv_is_tp_sharded:
+ if use_nonfused_agmm:
+ kv_input = self.ccl_manager.all_gather_persistent_buffer(
+ kv_input, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ else:
+ kv_parallel_config = self.parallel_config
+ if self.fuse_gate:
+ q_1BNF, gate_logits = self.to_q(
+ spatial_1BND,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=qkv_parallel_config,
+ )
+ gate_bhne = self._gate_from_logits(self._unpad_gate_logits(gate_logits))
+ else:
+ q_1BNF = self.to_q(
+ spatial_1BND,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=qkv_parallel_config,
+ )
+ k_1BNF, v_1BNF = self.to_kv(
+ kv_input,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=kv_parallel_config,
+ )
+
+ # RMSNorm on Q/K fused with the head split (emits BHNE via num_heads_per_device).
+ q_BHNE = self.norm_q(q_1BNF, num_heads_per_device=self.n_local_heads)
+ k_BHNE = self.norm_k(k_1BNF, num_heads_per_device=self.n_local_heads)
+
+ def create_heads(inp):
+ out, _, _ = ttnn.experimental.nlp_create_qkv_heads(
+ inp, num_heads=self.n_local_heads, num_kv_heads=0, transpose_k_heads=False
+ )
+ return out
+
+ # V still goes through the explicit reshape (no norm to fuse with).
+ v_BHNE = create_heads(v_1BNF)
+
+ # Cross-attn K/V must be full-seq for SDPA; gather across SP only when genuinely sharded.
+ is_cross = prompt_1BLP is not None
+ sp_factor = self.parallel_config.sequence_parallel.factor
+ _k_cos_pe = k_rope_cos if k_rope_cos is not None else rope_cos
+ # V2A cross: K/V stay SP-sharded (caller passes the sharded K-rope and kv_logical_n) so the
+ # ring SDPA fuses the gather instead of an explicit K/V all-gather + local SDPA.
+ use_ring_cross = is_cross and sp_factor > 1 and not kv_replicated and kv_logical_n is not None
+ if is_cross and sp_factor > 1 and not use_ring_cross:
+ sp_axis = self.parallel_config.sequence_parallel.mesh_axis
+ if kv_replicated:
+ need_gather = False
+ elif _k_cos_pe is not None:
+ need_gather = k_BHNE.shape[2] < _k_cos_pe.shape[2]
+ else:
+ # Unknown sharded context with no rope reference: gather conservatively.
+ need_gather = True
+ if need_gather:
+ k_BHNE = self.ccl_manager.all_gather_persistent_buffer(k_BHNE, dim=2, mesh_axis=sp_axis)
+ v_BHNE = self.ccl_manager.all_gather_persistent_buffer(v_BHNE, dim=2, mesh_axis=sp_axis)
+
+ if rope_cos is not None:
+ _k_cos = _k_cos_pe
+ _k_sin = k_rope_sin if k_rope_sin is not None else rope_sin
+ q_BHNE = ttnn.experimental.rotary_embedding_llama(
+ q_BHNE, rope_cos, rope_sin, trans_mat, compute_kernel_config=self.rope_compute_kernel_config
+ )
+ k_BHNE = ttnn.experimental.rotary_embedding_llama(
+ k_BHNE, _k_cos, _k_sin, trans_mat, compute_kernel_config=self.rope_compute_kernel_config
+ )
+
+ # SDPA input quant, applied after RoPE so the rotation still runs at full precision. On the
+ # ring paths K/V are the fabric payload (SDPA fuses their SP gather), so this shrinks a
+ # collective as well as the QK^T/PV matmuls; dummy_joint is a real SDPA input and must carry
+ # the same dtype. Kept separate from the linear activation cast: SDPA inputs have the widest
+ # dynamic range in the block and are the likeliest place for bf8 to break accuracy.
+ sdpa_input_dtype = getattr(self, "_sdpa_input_dtype", None)
+ dummy_joint = self.dummy_joint_input
+ if sdpa_input_dtype is not None:
+ q_BHNE = maybe_cast_activation(q_BHNE, sdpa_input_dtype)
+ k_BHNE = maybe_cast_activation(k_BHNE, sdpa_input_dtype)
+ v_BHNE = maybe_cast_activation(v_BHNE, sdpa_input_dtype)
+ dummy_joint = maybe_cast_activation(dummy_joint, sdpa_input_dtype)
+
+ if skip_qk:
+ # STG perturbation: skip Q/K attention, use V passthrough.
+ spatial_BHNE = v_BHNE
+ elif prompt_1BLP is None:
+ if sp_factor > 1 and attn_mask is None:
+ spatial_BHNE, _prompt_BHLE, _lse = ttnn.transformer.ring_joint_scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ dummy_joint,
+ dummy_joint,
+ dummy_joint,
+ # The gather buffer must be allocated at the gathered tensor's dtype: the fabric
+ # writes raw tiles into it, so a dtype mismatch is silent corruption, not a cast.
+ persistent_output_buffer_k=self.ccl_manager.get_ag_ping_pong_buffer(
+ k_BHNE.shape, 2, self.parallel_config.sequence_parallel.mesh_axis, dtype=k_BHNE.get_dtype()
+ ),
+ persistent_output_buffer_v=self.ccl_manager.get_ag_ping_pong_buffer(
+ v_BHNE.shape, 2, self.parallel_config.sequence_parallel.mesh_axis, dtype=v_BHNE.get_dtype()
+ ),
+ joint_strategy="rear",
+ logical_n=N,
+ program_config=self._ring_pc_by_n.get(N, self.ring_sdpa_program_config),
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ dim=2,
+ multi_device_global_semaphore=self.ccl_manager.get_ag_ping_pong_semaphore(
+ self.parallel_config.sequence_parallel.mesh_axis
+ ),
+ num_links=self.ccl_manager.num_links,
+ cluster_axis=self.parallel_config.sequence_parallel.mesh_axis,
+ mesh_device=self.mesh_device,
+ topology=self.ccl_manager.topology,
+ subdevice_id=self.ccl_manager.ccl_sub_device_id,
+ ccl_core_grid_offset=(self.sdpa_worker_grid[0], 0),
+ use_column_major_ccl=True,
+ )
+ elif sp_factor > 1:
+ # Masked audio self-attn: gather K/V, keep Q sharded; gather+local SDPA beats ring-joint here.
+ sp_axis = self.parallel_config.sequence_parallel.mesh_axis
+ k_full = self.ccl_manager.all_gather_persistent_buffer(k_BHNE, dim=2, mesh_axis=sp_axis)
+ v_full = self.ccl_manager.all_gather_persistent_buffer(v_BHNE, dim=2, mesh_axis=sp_axis)
+ spatial_BHNE = ttnn.transformer.scaled_dot_product_attention(
+ q_BHNE,
+ k_full,
+ v_full,
+ attn_mask=attn_mask,
+ is_causal=False,
+ program_config=self.sdpa_program_config,
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ )
+ else:
+ spatial_BHNE = ttnn.transformer.scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ attn_mask=attn_mask,
+ is_causal=False,
+ program_config=self.sdpa_program_config,
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ )
+ elif use_ring_cross:
+ # Short audio Q attends non-causally to the SP-sharded video K/V; is_cross fuses the
+ # K/V gather into the ring SDPA. Output is the per-device Q shard (same as local SDPA).
+ sp_mesh_axis = self.parallel_config.sequence_parallel.mesh_axis
+ spatial_BHNE, _prompt_BHLE, _lse = ttnn.transformer.ring_joint_scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ dummy_joint,
+ dummy_joint,
+ dummy_joint,
+ persistent_output_buffer_k=self.ccl_manager.get_ag_ping_pong_buffer(
+ k_BHNE.shape, 2, sp_mesh_axis, dtype=k_BHNE.get_dtype()
+ ),
+ persistent_output_buffer_v=self.ccl_manager.get_ag_ping_pong_buffer(
+ v_BHNE.shape, 2, sp_mesh_axis, dtype=v_BHNE.get_dtype()
+ ),
+ joint_strategy="rear",
+ logical_n=kv_logical_n,
+ is_cross=True,
+ program_config=self.cross_ring_sdpa_program_config,
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ dim=2,
+ multi_device_global_semaphore=self.ccl_manager.get_ag_ping_pong_semaphore(sp_mesh_axis),
+ num_links=self.ccl_manager.num_links,
+ cluster_axis=sp_mesh_axis,
+ mesh_device=self.mesh_device,
+ topology=self.ccl_manager.topology,
+ subdevice_id=self.ccl_manager.ccl_sub_device_id,
+ ccl_core_grid_offset=(self.sdpa_worker_grid[0], 0),
+ use_column_major_ccl=True,
+ )
+ else:
+ # Cross-attention: K/V full-seq, Q SP-sharded so local SDPA returns the local shard.
+ spatial_BHNE = ttnn.transformer.scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ attn_mask=attn_mask,
+ is_causal=False,
+ program_config=self._sdpa_pc_by_shape.get((q_BHNE.shape[2], k_BHNE.shape[2]), self.sdpa_program_config),
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ )
+
+ # Apply per-head gate in BHNE space.
+ if gate_bhne is not None:
+ spatial_BHNE = ttnn.multiply(spatial_BHNE, gate_bhne)
+
+ spatial_1BND = ttnn.transformer.concatenate_heads(spatial_BHNE)
+ spatial_1BND = ttnn.unsqueeze(spatial_1BND, 0)
+
+ # Ring fuses the TP all-gather into the to_out matmul; only Linear needs explicit AG.
+ addcmul_fused = addcmul_residual is not None and addcmul_gate is not None
+ to_out_explicit_ag = self.parallel_config.tensor_parallel.factor > 1 and use_nonfused_agmm
+ if to_out_explicit_ag:
+ # Same ordering rule as the QKV gather above: shrink the tensor before it crosses.
+ spatial_1BND = maybe_cast_activation(spatial_1BND, self.to_out.activation_dtype)
+ spatial_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_1BND, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+
+ if addcmul_fused:
+ spatial_1BND = self._to_out_fused_addcmul(
+ spatial_1BND,
+ addcmul_residual,
+ addcmul_gate,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=None if use_nonfused_agmm else self.parallel_config,
+ )
+ else:
+ spatial_1BND = self.to_out(
+ spatial_1BND,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=None if to_out_explicit_ag else self.parallel_config,
+ )
+
+ return spatial_1BND
diff --git a/code/models/tt_dit/models/transformers/ltx/quant_config.py b/code/models/tt_dit/models/transformers/ltx/quant_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..4b28a04c6bc95f36dff9e4b62f3b9e869004d2cb
--- /dev/null
+++ b/code/models/tt_dit/models/transformers/ltx/quant_config.py
@@ -0,0 +1,143 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Quantization and compute-precision policy for the LTX DiT linears.
+
+A single :class:`LtxQuantProfile` holds the whole policy — weight dtypes, the ``to_out`` carve-out,
+activation/SDPA casts, and matmul/SDPA fidelities — and exposes it as construction kwargs. The
+transformer modules stay precision-agnostic: they spread ``profile.linear_kwargs(role)`` onto each
+linear and read ``profile.mm_compute_config`` / ``profile.sdpa_self_config`` for the op configs, with
+no dtype or fidelity literals of their own.
+
+Weight quantization happens at Parameter construction, not as a state_dict pass: there is no Torch
+bfloat8, so ``ttnn.from_torch(dtype=...)`` is the only quantizer and the cache enforces the declared
+``Parameter.dtype``. The profile therefore sets each linear's ``dtype`` at build time, so a cache miss
+loads weights direct-to-quant and the cache write holds the quantized tensorbins.
+"""
+
+from __future__ import annotations
+
+import os
+from dataclasses import dataclass
+
+import ttnn
+
+# When set, activations are narrowed to bf8 wherever they cross the fabric — both the linear activation
+# casts and the ring-SDPA Q/K/V inputs — so this is the one knob that changes what the collectives move
+# (the linear payloads and the SP-gathered K/V; SDPA itself stays HiFi2, only its inputs narrow). More
+# aggressive than the weight quant it rides on: weights are a fixed, well-conditioned distribution,
+# activations are not. On by default because the shipped 1080p tier is measured and VBench-gated with it
+# on; set to 0 to A/B against the weight-only preset. Keeping the SDPA input cast on this same knob means
+# a run can't narrow the linear activations while silently leaving SDPA at bf16 (or vice versa).
+LTX_QUANT_ACTIVATIONS = os.environ.get("LTX_QUANT_ACTIVATIONS", "1") in ("1", "true", "True")
+
+
+@dataclass(frozen=True)
+class LtxQuantProfile:
+ """One quant profile for every DiT linear, with the ``to_out`` carve-out.
+
+ Every quantized projection (qkv/q/kv, ff1/ff2) shares one weight dtype and one compute profile;
+ only ``to_out`` carves out to ``out_weight_dtype`` (bf16), because it feeds the fused-addcmul
+ epilogue whose bf16 ternary inputs must match the weight tile format. The activation and SDPA-input
+ casts (and the output pins that go with them) ride ``LTX_QUANT_ACTIVATIONS`` together, so a
+ weight-only run keeps activations bf16.
+ """
+
+ weight_dtype: ttnn.DataType # qkv/q/kv, ff1/ff2, and any other quantized projection
+ out_weight_dtype: ttnn.DataType # to_out carve-out
+ activation_dtype: ttnn.DataType # linear activation cast (gated by LTX_QUANT_ACTIVATIONS)
+ mm_fidelity: ttnn.MathFidelity
+ mm_fp32_dest_acc: bool
+ sdpa_input_dtype: ttnn.DataType # self-attn ring-SDPA input cast (gated by LTX_QUANT_ACTIVATIONS)
+ sdpa_fidelity: ttnn.MathFidelity
+ sdpa_fp32_dest_acc: bool
+
+ def linear_kwargs(self, role: str) -> dict:
+ """Construction kwargs for a DiT linear of ``role`` ("out" carves out to bf16, else quantized).
+
+ A ``gate`` keeps its bf16 weight but still consumes the shared bf8 activation, so it is pinned
+ like the projections but gets no weight-dtype or activation-cast change.
+ """
+ pin = LTX_QUANT_ACTIVATIONS
+ if role == "gate":
+ return {"pin_output_bf16": pin}
+ return {
+ "dtype": self.out_weight_dtype if role == "out" else self.weight_dtype,
+ "activation_dtype": self.activation_dtype if LTX_QUANT_ACTIVATIONS else None,
+ "pin_output_bf16": pin,
+ }
+
+ def ffn_kwargs(self) -> dict:
+ """Construction kwargs for the FFN (ParallelFeedForward), where ff1 and ff2 are both quantized."""
+ lk = self.linear_kwargs("ff")
+ return {
+ "ff1_dtype": lk["dtype"],
+ "ff2_dtype": lk["dtype"],
+ "activation_dtype": lk["activation_dtype"],
+ "pin_output_bf16": lk["pin_output_bf16"],
+ }
+
+ def mm_compute_config(self, arch):
+ """Compute-kernel config for the DiT-linear matmuls (attention QKV/out and the FFN).
+
+ ``packer_l1_acc`` stays on regardless of tier: it accumulates matmul partials in L1 at fp32,
+ which is near-free and strictly helps. It is orthogonal to ``fp32_dest_acc_en`` — the pricier
+ fp32 *destination* accumulation that the LoFi/bf8 tier deliberately drops (``mm_fp32_dest_acc``).
+ """
+ return ttnn.init_device_compute_kernel_config(
+ arch,
+ math_fidelity=self.mm_fidelity,
+ math_approx_mode=False,
+ fp32_dest_acc_en=self.mm_fp32_dest_acc,
+ packer_l1_acc=True,
+ )
+
+ def sdpa_self_config(self, arch) -> tuple:
+ """``(compute_config, input_dtype)`` for the self-attn ring SDPA; input cast rides the gate.
+
+ Cross-attention keeps its default bf16/HiFi2 SDPA, so callers apply this to self-attn only.
+ """
+ compute_config = ttnn.init_device_compute_kernel_config(
+ arch,
+ math_fidelity=self.sdpa_fidelity,
+ math_approx_mode=False,
+ fp32_dest_acc_en=self.sdpa_fp32_dest_acc,
+ )
+ return compute_config, (self.sdpa_input_dtype if LTX_QUANT_ACTIVATIONS else None)
+
+ @staticmethod
+ def all_bf8_lofi() -> LtxQuantProfile:
+ """Weights bf8, LoFi compute, bf8 activations. SDPA math stays HiFi2 with bf8 inputs.
+
+ SDPA stays fully unquantized in math (FastVideo kept attention higher precision); only its
+ inputs narrow to bf8 under ``LTX_QUANT_ACTIVATIONS``. ``ff2`` uses the RowParallel RS-fused
+ addcmul, which Wan runs at bf8 with no issue, so it is quantized; only ``to_out`` carves out.
+ """
+ return LtxQuantProfile(
+ weight_dtype=ttnn.bfloat8_b,
+ out_weight_dtype=ttnn.bfloat16,
+ activation_dtype=ttnn.bfloat8_b,
+ mm_fidelity=ttnn.MathFidelity.LoFi,
+ mm_fp32_dest_acc=False,
+ sdpa_input_dtype=ttnn.bfloat8_b,
+ sdpa_fidelity=ttnn.MathFidelity.HiFi2,
+ sdpa_fp32_dest_acc=False,
+ )
+
+ @staticmethod
+ def all_bf4_lofi() -> LtxQuantProfile:
+ """Like ``all_bf8_lofi`` but 4-bit weights and 4-bit linear activations. Opt-in probe tier, not
+ shipped: bf4 *activations* visibly destroy quality (bf4 weights alone hold up), so this is for
+ A/B measurement only. Carve-outs and the ring-SDPA bf8 input match all_bf8_lofi.
+ """
+ return LtxQuantProfile(
+ weight_dtype=ttnn.bfloat4_b,
+ out_weight_dtype=ttnn.bfloat16,
+ activation_dtype=ttnn.bfloat4_b,
+ mm_fidelity=ttnn.MathFidelity.LoFi,
+ mm_fp32_dest_acc=False,
+ sdpa_input_dtype=ttnn.bfloat8_b,
+ sdpa_fidelity=ttnn.MathFidelity.HiFi2,
+ sdpa_fp32_dest_acc=False,
+ )
diff --git a/code/models/tt_dit/models/transformers/ltx/rope_ltx.py b/code/models/tt_dit/models/transformers/ltx/rope_ltx.py
new file mode 100644
index 0000000000000000000000000000000000000000..d210c0f9aba95cac91dcdc5ea86453e2ee0164a7
--- /dev/null
+++ b/code/models/tt_dit/models/transformers/ltx/rope_ltx.py
@@ -0,0 +1,393 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+LTX-2 Rotary Position Embeddings for tt_dit.
+
+Computes cos/sin on CPU using LTX-2's fractional position encoding with custom
+frequency grids, then converts to ttnn tensors for use with
+ttnn.experimental.rotary_embedding_llama.
+
+Reference: LTX-2/packages/ltx-core/src/ltx_core/model/transformer/rope.py
+"""
+
+from __future__ import annotations
+
+import functools
+import math
+from enum import Enum
+
+import numpy as np
+import torch
+
+import ttnn
+
+from ....parallel.config import DiTParallelConfig
+from ....utils.patchifiers import (
+ AudioLatentShape,
+ VideoLatentShape,
+ audio_get_patch_grid_bounds,
+ get_pixel_coords,
+ video_get_patch_grid_bounds,
+)
+from ....utils.tensor import bf16_tensor, bf16_tensor_2dshard
+
+
+class LTXRopeType(Enum):
+ INTERLEAVED = "interleaved"
+ SPLIT = "split"
+
+
+@functools.lru_cache(maxsize=5)
+def generate_freq_grid(
+ positional_embedding_theta: float,
+ positional_embedding_max_pos_count: int,
+ inner_dim: int,
+) -> torch.Tensor:
+ """Power-law frequency grid for LTX-2 RoPE: theta^linspace(...) * pi/2.
+
+ Kept fp32, not HF's fp64: upgrading the grid to fp64 measurably hurt TT-vs-HF
+ audio correlation because the downstream residual stream is bf16. Keep fp32
+ unless the whole RoPE + residual path moves to fp32.
+ """
+ theta = positional_embedding_theta
+ n_elem = 2 * positional_embedding_max_pos_count
+
+ pow_indices = np.power(
+ theta,
+ np.linspace(
+ np.log(1.0) / np.log(theta),
+ np.log(theta) / np.log(theta),
+ inner_dim // n_elem,
+ dtype=np.float64,
+ ),
+ )
+ return torch.tensor(pow_indices * math.pi / 2, dtype=torch.float32)
+
+
+def get_fractional_positions(indices_grid: torch.Tensor, max_pos: list[int]) -> torch.Tensor:
+ """Normalize position indices to [0, 1] by dividing by max_pos per dimension.
+
+ Args:
+ indices_grid: (B, n_dims, N) position indices after averaging start/end
+ max_pos: max position per dimension
+ """
+ n_pos_dims = indices_grid.shape[1]
+ assert n_pos_dims == len(
+ max_pos
+ ), f"Number of position dimensions ({n_pos_dims}) must match max_pos length ({len(max_pos)})"
+ return torch.stack([indices_grid[:, i] / max_pos[i] for i in range(n_pos_dims)], dim=-1)
+
+
+def generate_freqs(
+ indices: torch.Tensor,
+ indices_grid: torch.Tensor,
+ max_pos: list[int],
+ use_middle_indices_grid: bool,
+) -> torch.Tensor:
+ """
+ Compute raw frequencies from position indices and frequency grid.
+
+ freqs = indices * (fractional_positions * 2 - 1)
+ This maps fractional positions from [0,1] to [-1,1] before multiplying by freq indices.
+ """
+ if use_middle_indices_grid:
+ assert len(indices_grid.shape) == 4
+ assert indices_grid.shape[-1] == 2
+ indices_grid_start, indices_grid_end = indices_grid[..., 0], indices_grid[..., 1]
+ indices_grid = (indices_grid_start + indices_grid_end) / 2.0
+ elif len(indices_grid.shape) == 4:
+ indices_grid = indices_grid[..., 0]
+ else:
+ # 3D (B, N, n_dims) → transpose to (B, n_dims, N) for get_fractional_positions
+ indices_grid = indices_grid.transpose(1, 2)
+
+ fractional_positions = get_fractional_positions(indices_grid, max_pos)
+ indices = indices.to(device=fractional_positions.device)
+ freqs = (indices * (fractional_positions.unsqueeze(-1) * 2 - 1)).transpose(-1, -2).flatten(2)
+ return freqs
+
+
+def reshape_interleaved_to_bhnd(t: torch.Tensor, num_heads: int) -> torch.Tensor:
+ """Reshape interleaved (B, N, dim) to (B, num_heads, N, head_dim) for rotary_embedding_llama."""
+ B, N, dim = t.shape
+ head_dim = dim // num_heads
+ return t.reshape(B, N, num_heads, head_dim).permute(0, 2, 1, 3).contiguous()
+
+
+def interleaved_freqs_cis(freqs: torch.Tensor, pad_size: int) -> tuple[torch.Tensor, torch.Tensor]:
+ """
+ Compute cos/sin with interleaved repeat for rotary embedding.
+
+ Each frequency value is duplicated: [f0, f0, f1, f1, ...] to match
+ the interleaved (x0, x1) pair structure expected by rotary_embedding_llama.
+ """
+ cos_freq = freqs.cos().repeat_interleave(2, dim=-1)
+ sin_freq = freqs.sin().repeat_interleave(2, dim=-1)
+ if pad_size != 0:
+ cos_padding = torch.ones_like(cos_freq[:, :, :pad_size])
+ sin_padding = torch.zeros_like(cos_freq[:, :, :pad_size])
+ cos_freq = torch.cat([cos_padding, cos_freq], dim=-1)
+ sin_freq = torch.cat([sin_padding, sin_freq], dim=-1)
+ return cos_freq, sin_freq
+
+
+def split_freqs_cis(freqs: torch.Tensor, pad_size: int, num_attention_heads: int) -> tuple[torch.Tensor, torch.Tensor]:
+ """
+ Compute cos/sin with split layout for rotary_embedding_llama.
+
+ The llama kernel expects (B, H, N, head_dim) and applies rotation to pairs (x[i], x[i+D/2]).
+ We repeat the unique frequencies: [f0,f1,...,f_{d/2}, f0,f1,...,f_{d/2}] so both halves
+ are rotated correctly.
+
+ Output shape: (B, num_heads, T, head_dim) where head_dim = 2 * D_half.
+ """
+ cos_freq = freqs.cos()
+ sin_freq = freqs.sin()
+
+ if pad_size != 0:
+ cos_padding = torch.ones_like(cos_freq[:, :, :pad_size])
+ sin_padding = torch.zeros_like(sin_freq[:, :, :pad_size])
+ cos_freq = torch.concatenate([cos_padding, cos_freq], axis=-1)
+ sin_freq = torch.concatenate([sin_padding, sin_freq], axis=-1)
+
+ b, t = cos_freq.shape[0], cos_freq.shape[1]
+ # Reshape to per-head: (B, T, H, D_half) -> (B, H, T, D_half)
+ cos_freq = cos_freq.reshape(b, t, num_attention_heads, -1).swapaxes(1, 2)
+ sin_freq = sin_freq.reshape(b, t, num_attention_heads, -1).swapaxes(1, 2)
+ return cos_freq, sin_freq
+
+
+def precompute_freqs_cis(
+ indices_grid: torch.Tensor,
+ dim: int,
+ out_dtype: torch.dtype = torch.float32,
+ theta: float = 10000.0,
+ max_pos: list[int] | None = None,
+ use_middle_indices_grid: bool = False,
+ num_attention_heads: int = 32,
+ rope_type: LTXRopeType = LTXRopeType.INTERLEAVED,
+) -> tuple[torch.Tensor, torch.Tensor]:
+ """Precompute (cos, sin) for LTX-2 RoPE.
+
+ Output shape depends on rope_type:
+ - INTERLEAVED: (B, seq_len, dim), each freq duplicated for the (x0, x1) pairs.
+ - SPLIT: (B, num_heads, seq_len, dim // (2 * num_heads)).
+ """
+ if max_pos is None:
+ max_pos = [20, 2048, 2048]
+
+ # n_pos_dims is the number of position dimensions (e.g. 3 for temporal + H + W)
+ # For 4D grids (B, n_dims, N, 2), n_pos_dims is shape[1]; for 3D (B, N, n_dims), it's shape[-1]
+ if use_middle_indices_grid and len(indices_grid.shape) == 4:
+ n_pos_dims = indices_grid.shape[1] # (B, n_dims, N, 2)
+ else:
+ n_pos_dims = indices_grid.shape[-1]
+ indices = generate_freq_grid(theta, n_pos_dims, dim)
+ freqs = generate_freqs(indices, indices_grid, max_pos, use_middle_indices_grid)
+
+ if rope_type == LTXRopeType.SPLIT:
+ expected_freqs = dim // 2
+ current_freqs = freqs.shape[-1]
+ pad_size = expected_freqs - current_freqs
+ cos_freq, sin_freq = split_freqs_cis(freqs, pad_size, num_attention_heads)
+ else:
+ n_elem = 2 * n_pos_dims
+ cos_freq, sin_freq = interleaved_freqs_cis(freqs, dim % n_elem)
+
+ return cos_freq.to(out_dtype), sin_freq.to(out_dtype)
+
+
+# =============================================================================
+# Device-side RoPE builders (INTERLEAVED cos/sin, sharded onto the DiT mesh) for
+# ttnn.experimental.rotary_embedding_llama. All positions stay fp32 — bf16 introduced
+# catastrophic phase error in high-frequency RoPE channels (randomizing the top half of
+# head_dim), so only the final ttnn tensors are cast to bf16.
+# =============================================================================
+
+
+def pad_video_rope_sp(
+ cos_freq: torch.Tensor, sin_freq: torch.Tensor, sp_factor: int
+) -> tuple[torch.Tensor, torch.Tensor]:
+ """Right-pad video RoPE cos/sin on dim=2 to the SP boundary (``ttnn.TILE_SIZE * sp_factor``).
+
+ Padded slots use cos=1, sin=0 (identity rotation); SDPA still masks them via ``logical_n``.
+ Same convention as the audio RoPE padding in ``prepare_audio_rope`` / ``prepare_av_cross_pe``.
+ """
+ video_N_real = cos_freq.shape[2]
+ divisor = ttnn.TILE_SIZE * sp_factor
+ video_N = ((video_N_real + divisor - 1) // divisor) * divisor
+ if video_N == video_N_real:
+ return cos_freq, sin_freq
+ pad = video_N - video_N_real
+ H = cos_freq.shape[1]
+ d_half = cos_freq.shape[-1]
+ cos_pad = torch.ones(1, H, pad, d_half, dtype=cos_freq.dtype)
+ sin_pad = torch.zeros(1, H, pad, d_half, dtype=sin_freq.dtype)
+ cos_freq = torch.cat([cos_freq, cos_pad], dim=2)
+ sin_freq = torch.cat([sin_freq, sin_pad], dim=2)
+ return cos_freq, sin_freq
+
+
+def prepare_video_rope(
+ latent_frames: int,
+ latent_height: int,
+ latent_width: int,
+ *,
+ inner_dim: int,
+ num_attention_heads: int,
+ theta: float,
+ max_pos: list[int],
+ mesh_device: ttnn.MeshDevice,
+ parallel_config: DiTParallelConfig,
+ fps: float = 24.0,
+) -> tuple[ttnn.Tensor, ttnn.Tensor]:
+ """Compute video RoPE in INTERLEAVED layout, SP×TP sharded onto the mesh."""
+ v_shape = VideoLatentShape(batch=1, channels=128, frames=latent_frames, height=latent_height, width=latent_width)
+ v_coords = video_get_patch_grid_bounds(v_shape)
+ v_positions = get_pixel_coords(v_coords, scale_factors=(8, 32, 32), causal_fix=True).float()
+ v_positions[:, 0, ...] = v_positions[:, 0, ...] / fps
+
+ cos_freq, sin_freq = precompute_freqs_cis(
+ v_positions,
+ dim=inner_dim,
+ out_dtype=torch.float32,
+ theta=theta,
+ max_pos=max_pos,
+ use_middle_indices_grid=True,
+ num_attention_heads=num_attention_heads,
+ rope_type=LTXRopeType.INTERLEAVED,
+ ) # (1, N, dim)
+
+ cos_freq = reshape_interleaved_to_bhnd(cos_freq, num_attention_heads)
+ sin_freq = reshape_interleaved_to_bhnd(sin_freq, num_attention_heads)
+
+ # Pad seq dim to ttnn.TILE_SIZE * sp_factor; padded slots use cos=1, sin=0 (identity).
+ cos_freq, sin_freq = pad_video_rope_sp(cos_freq, sin_freq, parallel_config.sequence_parallel.factor)
+
+ sp_axis = parallel_config.sequence_parallel.mesh_axis
+ tp_axis = parallel_config.tensor_parallel.mesh_axis
+ tt_cos = bf16_tensor_2dshard(cos_freq, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ tt_sin = bf16_tensor_2dshard(sin_freq, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ return tt_cos, tt_sin
+
+
+def prepare_audio_rope(
+ audio_N: int,
+ audio_N_real: int,
+ *,
+ theta: float,
+ mesh_device: ttnn.MeshDevice,
+ parallel_config: DiTParallelConfig,
+) -> tuple[ttnn.Tensor, ttnn.Tensor]:
+ """Compute audio RoPE in INTERLEAVED layout, SP×TP sharded onto the mesh."""
+ a_shape = AudioLatentShape(batch=1, channels=8, frames=audio_N_real, mel_bins=16)
+ a_positions = audio_get_patch_grid_bounds(a_shape).float() # (1, 1, N, 2)
+
+ a_cos, a_sin = precompute_freqs_cis(
+ a_positions,
+ dim=2048,
+ out_dtype=torch.float32,
+ theta=theta,
+ max_pos=[20],
+ use_middle_indices_grid=True,
+ num_attention_heads=32,
+ rope_type=LTXRopeType.INTERLEAVED,
+ ) # (1, N, 2048)
+ a_cos = reshape_interleaved_to_bhnd(a_cos, num_heads=32) # (1, 32, N, 64)
+ a_sin = reshape_interleaved_to_bhnd(a_sin, num_heads=32)
+
+ if audio_N > audio_N_real:
+ head_dim = a_cos.shape[-1]
+ a_cos_padded = torch.ones(1, 32, audio_N, head_dim)
+ a_cos_padded[:, :, :audio_N_real, :] = a_cos
+ a_sin_padded = torch.zeros(1, 32, audio_N, head_dim)
+ a_sin_padded[:, :, :audio_N_real, :] = a_sin
+ a_cos, a_sin = a_cos_padded, a_sin_padded
+
+ sp_axis = parallel_config.sequence_parallel.mesh_axis
+ tp_axis = parallel_config.tensor_parallel.mesh_axis
+ return (
+ bf16_tensor_2dshard(a_cos, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1}),
+ bf16_tensor_2dshard(a_sin, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1}),
+ )
+
+
+def prepare_av_cross_pe(
+ latent_frames: int,
+ latent_height: int,
+ latent_width: int,
+ audio_N: int,
+ audio_N_real: int,
+ *,
+ theta: float,
+ mesh_device: ttnn.MeshDevice,
+ parallel_config: DiTParallelConfig,
+ fps: float = 24.0,
+ cross_pe_max_pos: int = 20,
+) -> tuple[ttnn.Tensor, ttnn.Tensor, ttnn.Tensor, ttnn.Tensor, ttnn.Tensor, ttnn.Tensor]:
+ """Temporal-only cross positional embeddings for A↔V cross-attention.
+
+ Reference: ``MultiModalTransformerArgsPreprocessor.prepare`` builds ``cross_pe`` from the
+ temporal slice ``modality.positions[:, 0:1, :]`` at ``dim=audio_cross_attention_dim`` with
+ ``max_pos=[cross_pe_max_pos]``. Video and audio share the scheme so a video token and an
+ audio token at the same time get the same rotary phase (AV temporal sync).
+
+ Returns 6 device tensors used by inner_step:
+ (v_q_cos, v_q_sin) — video Q in A→V cross-attn (SP×TP sharded). Also reused as the
+ video K rope in V→A (ring SDPA gathers the SP-sharded K).
+ (a_q_cos, a_q_sin) — audio Q in V→A cross-attn (SP×TP sharded).
+ (a_k_cos, a_k_sin) — audio K in A→V cross-attn (TP-only; K side after AllGather).
+ """
+ v_shape = VideoLatentShape(batch=1, channels=128, frames=latent_frames, height=latent_height, width=latent_width)
+ v_coords = video_get_patch_grid_bounds(v_shape)
+ v_positions = get_pixel_coords(v_coords, scale_factors=(8, 32, 32), causal_fix=True).float()
+ v_positions[:, 0, ...] = v_positions[:, 0, ...] / fps # temporal axis → seconds
+ v_temporal = v_positions[:, 0:1, :] # (1, 1, video_N, 2)
+
+ a_shape = AudioLatentShape(batch=1, channels=8, frames=audio_N_real, mel_bins=16)
+ a_positions = audio_get_patch_grid_bounds(a_shape).float() # (1, 1, audio_N_real, 2)
+
+ rope_kwargs = dict(
+ dim=2048, # audio_cross_attention_dim — both sides share this
+ out_dtype=torch.float32,
+ theta=theta,
+ max_pos=[cross_pe_max_pos],
+ use_middle_indices_grid=True,
+ num_attention_heads=32,
+ rope_type=LTXRopeType.INTERLEAVED,
+ )
+
+ v_cos, v_sin = precompute_freqs_cis(v_temporal, **rope_kwargs) # (1, video_N, 2048)
+ a_cos, a_sin = precompute_freqs_cis(a_positions, **rope_kwargs) # (1, audio_N_real, 2048)
+ v_cos = reshape_interleaved_to_bhnd(v_cos, num_heads=32) # (1, 32, video_N, 64)
+ v_sin = reshape_interleaved_to_bhnd(v_sin, num_heads=32)
+ a_cos = reshape_interleaved_to_bhnd(a_cos, num_heads=32)
+ a_sin = reshape_interleaved_to_bhnd(a_sin, num_heads=32)
+
+ v_cos, v_sin = pad_video_rope_sp(v_cos, v_sin, parallel_config.sequence_parallel.factor)
+
+ if audio_N > audio_N_real:
+ head_dim = a_cos.shape[-1]
+ a_cos_padded = torch.ones(1, 32, audio_N, head_dim)
+ a_cos_padded[:, :, :audio_N_real, :] = a_cos
+ a_sin_padded = torch.zeros(1, 32, audio_N, head_dim)
+ a_sin_padded[:, :, :audio_N_real, :] = a_sin
+ a_cos, a_sin = a_cos_padded, a_sin_padded
+
+ sp_axis = parallel_config.sequence_parallel.mesh_axis
+ tp_axis = parallel_config.tensor_parallel.mesh_axis
+
+ # Q-side: SP×TP sharded (matches the Q tensor layout post-attention QKV split).
+ v_q_cos = bf16_tensor_2dshard(v_cos, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ v_q_sin = bf16_tensor_2dshard(v_sin, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ a_q_cos = bf16_tensor_2dshard(a_cos, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ a_q_sin = bf16_tensor_2dshard(a_sin, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+
+ # K-side: TP-only on heads (sequence is replicated after AllGather on K). Only the audio K
+ # rope is needed (A→V gathers audio K); video K in V→A reuses the SP-sharded v_q rope.
+ a_k_cos = bf16_tensor(a_cos, device=mesh_device, mesh_axis=tp_axis, shard_dim=1)
+ a_k_sin = bf16_tensor(a_sin, device=mesh_device, mesh_axis=tp_axis, shard_dim=1)
+
+ return (v_q_cos, v_q_sin, a_q_cos, a_q_sin, a_k_cos, a_k_sin)
diff --git a/code/models/tt_dit/models/transformers/ltx/transformer_ltx.py b/code/models/tt_dit/models/transformers/ltx/transformer_ltx.py
new file mode 100644
index 0000000000000000000000000000000000000000..895ee587e2c2db11d87c49c06474fed322d54cae
--- /dev/null
+++ b/code/models/tt_dit/models/transformers/ltx/transformer_ltx.py
@@ -0,0 +1,1249 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import os
+
+import torch
+from loguru import logger
+from safetensors import safe_open
+from safetensors.torch import load_file
+
+import ttnn
+
+from ....layers.embeddings import LTXAdaLayerNormSingle
+from ....layers.feedforward import ParallelFeedForward
+from ....layers.linear import ColParallelLinear, Linear
+from ....layers.module import Module, ModuleList, Parameter
+from ....layers.normalization import DistributedLayerNorm, DistributedRMSNorm
+from ....parallel.config import DiTParallelConfig
+from ....parallel.manager import CCLManager
+from ....utils import cache as cache_module
+from ....utils.fuse_loras import LoraSpec, fuse_loras_into
+from ....utils.substate import pop_substate, rename_substate
+from ....utils.tensor import bf16_tensor
+from ....utils.tracing import traced_function
+from .attention_ltx import LTXAttention
+from .quant_config import LtxQuantProfile
+
+
+def _tile_preserving_chunk0(x: ttnn.Tensor, n: int) -> list[ttnn.Tensor]:
+ """Split ``x`` into ``n`` size-1 slices along dim 0 WITHOUT leaving TILE layout.
+
+ ``ttnn.chunk`` is a host fallback that untilizes to ROW_MAJOR (and, for the
+ ``(coeff,B,1,D)`` AdaLN modulation, the ``(1,B,1,D)`` slices never re-tile), forcing
+ every downstream ``addcmul``/matmul epilogue to re-tilize the shift/scale/gate vectors
+ (the dominant BF16->BF16 tilize cost per block). Slicing dim 0 -- a non-tile outer dim --
+ on the already-TILE ``shifted`` tensor keeps every slice in TILE, so consumers take the
+ fused LLK path instead of the composite (multiply+add w/ per-input tilize) fallback.
+ Output is bit-identical to ``ttnn.chunk`` (pure layout, no value change).
+
+ ``x`` is already TILE here (``TILE scale_shift_table.data`` + temb; ttnn add promotes to
+ TILE), so no explicit re-tilize is needed -- ``ttnn.slice`` is a trace-safe device op that
+ preserves the input layout, whereas ``ttnn.chunk``'s host untilize is what forced RM."""
+ shape = list(x.shape)
+ out = []
+ for i in range(n):
+ starts = [0] * len(shape)
+ ends = list(shape)
+ starts[0] = i
+ ends[0] = i + 1
+ out.append(ttnn.slice(x, starts, ends))
+ return out
+
+
+def build_audio_masks(
+ audio_N: int, audio_N_real: int, *, mesh_device: ttnn.MeshDevice, sp_axis: int
+) -> tuple[ttnn.Tensor | None, ttnn.Tensor | None, ttnn.Tensor | None]:
+ """SDPA attn mask + padding masks for SP-sharded vs gathered audio tokens.
+
+ Returns ``(attn_mask, pad_mask_sp, pad_mask_full)`` — ``pad_mask_sp`` is sharded on the
+ sequence dim for multiply with local audio activations; ``pad_mask_full`` is replicated
+ for multiply after the all_gather on A→V keys. All ``None`` when no padding is needed.
+ """
+ if audio_N <= audio_N_real:
+ return None, None, None
+
+ # Column mask only: real/padded queries are barred from attending TO padded keys.
+ # Do NOT mask padded-query rows to -inf — that makes all attention scores in those
+ # rows -inf → softmax NaN → NaN propagates via padded-token outputs (which we then
+ # multiply by 0; IEEE 0*NaN = NaN, not 0). The pad mask already zeros the padded-query
+ # outputs after attention, so column-only masking is sufficient and numerically safer
+ # at high σ where activations have largest magnitude.
+ mask = torch.zeros(1, 1, audio_N, audio_N)
+ mask[:, :, :, audio_N_real:] = float("-inf")
+ tt_attn_mask = bf16_tensor(mask.to(torch.bfloat16), device=mesh_device, mesh_axis=sp_axis, shard_dim=2)
+
+ pad_mask = torch.ones(1, 1, audio_N, 1, dtype=torch.bfloat16)
+ pad_mask[:, :, audio_N_real:, :] = 0.0
+ tt_pad_mask_sp = bf16_tensor(pad_mask, device=mesh_device, mesh_axis=sp_axis, shard_dim=2)
+ tt_pad_mask_full = bf16_tensor(pad_mask, device=mesh_device)
+ return tt_attn_mask, tt_pad_mask_sp, tt_pad_mask_full
+
+
+def build_video_pad_mask(
+ video_N: int, video_N_real: int, *, mesh_device: ttnn.MeshDevice, sp_axis: int
+) -> ttnn.Tensor | None:
+ """SP-sharded video padding mask ``(1, 1, video_N, 1)``; ``None`` when no padding is needed.
+
+ Multiply the local (sharded) video activations by this to zero padded slots before they
+ propagate downstream (self-attn residual / cross-attn K / FF). No SDPA attn_mask is needed
+ (unlike audio) — video self-attention uses ring SDPA, which masks padded keys via its
+ ``logical_n=video_N_real`` arg.
+ """
+ if video_N <= video_N_real:
+ return None
+ pad_mask = torch.ones(1, 1, video_N, 1, dtype=torch.bfloat16)
+ pad_mask[:, :, video_N_real:, :] = 0.0
+ return bf16_tensor(pad_mask, device=mesh_device, mesh_axis=sp_axis, shard_dim=2)
+
+
+class LTXTransformerBlock(Module):
+ def __init__(
+ self,
+ *,
+ video_dim: int = 4096,
+ audio_dim: int = 2048,
+ video_ffn_dim: int = 16384,
+ audio_ffn_dim: int = 8192,
+ video_num_heads: int = 32,
+ audio_num_heads: int = 32,
+ video_cross_attention_dim: int = 4096,
+ audio_cross_attention_dim: int = 2048,
+ eps: float = 1e-6,
+ mesh_device: ttnn.MeshDevice,
+ ccl_manager: CCLManager | None = None,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool = False,
+ has_audio: bool = False,
+ apply_gated_attention: bool = False,
+ cross_attention_adaln: bool = True,
+ quant_config: LtxQuantProfile | None = None,
+ lora_enabled: bool = False,
+ ) -> None:
+ super().__init__()
+
+ self.video_dim = video_dim
+ self.audio_dim = audio_dim
+ self.has_audio = has_audio
+ self.cross_attention_adaln = cross_attention_adaln
+ self.mesh_device = mesh_device
+ self.ccl_manager = ccl_manager
+ self.parallel_config = parallel_config
+
+ rms_norm_kwargs = {
+ "norm_eps": eps,
+ "norm_elementwise_affine": False,
+ "bias": False,
+ "mesh_axis": parallel_config.tensor_parallel.mesh_axis,
+ "mesh_device": mesh_device,
+ "ccl_manager": ccl_manager,
+ }
+
+ attn_kwargs = {
+ "eps": eps,
+ "mesh_device": mesh_device,
+ "ccl_manager": ccl_manager,
+ "parallel_config": parallel_config,
+ "is_fsdp": is_fsdp,
+ "apply_gated_attention": apply_gated_attention,
+ "quant_config": quant_config,
+ "lora_enabled": lora_enabled,
+ }
+
+ # FFN precision: the profile supplies the ff dtypes + casts; no quant_config leaves the
+ # ParallelFeedForward ctor defaults, reproducing bf16. lora_enabled always threads through.
+ ffn_kwargs = {"lora_enabled": lora_enabled}
+ if quant_config is not None:
+ ffn_kwargs.update(quant_config.ffn_kwargs())
+
+ # FSDP fractures FFN weights across the SP axis (on top of the TP fracture);
+ # without it the FFN is only TP-sharded and replicated across every SP device.
+ fsdp_mesh_axis = parallel_config.sequence_parallel.mesh_axis if is_fsdp else None
+
+ self.norm1 = DistributedRMSNorm(embedding_dim=video_dim, **rms_norm_kwargs)
+ self.attn1 = LTXAttention(dim=video_dim, num_heads=video_num_heads, is_self=True, **attn_kwargs)
+ self.norm2 = DistributedRMSNorm(embedding_dim=video_dim, **rms_norm_kwargs)
+ self.attn2 = LTXAttention(
+ dim=video_dim,
+ num_heads=video_num_heads,
+ is_self=False,
+ context_dim=video_cross_attention_dim,
+ **attn_kwargs,
+ )
+ self.norm3 = DistributedRMSNorm(embedding_dim=video_dim, **rms_norm_kwargs)
+ self.ffn = ParallelFeedForward(
+ video_dim,
+ inner_dim=video_ffn_dim,
+ activation_fn="gelu_tanh",
+ bias=True,
+ mesh_device=mesh_device,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ ccl_manager=ccl_manager,
+ fsdp_mesh_axis=fsdp_mesh_axis,
+ **ffn_kwargs,
+ )
+ self.adaln_coeff = 9 if cross_attention_adaln else 6
+ # Outer-param layout (coeff, 1, 1, D): keeps each modulation parameter on the
+ # non-tiled dim 0 so per-block ttnn.chunk(dim=0) is a free tile-aligned slice
+ # (no untilize/slice/tilize). D stays on dim 3 for TP sharding.
+ self.scale_shift_table = Parameter(
+ total_shape=[self.adaln_coeff, 1, 1, video_dim],
+ mesh_axes=[None, None, None, parallel_config.tensor_parallel.mesh_axis],
+ device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ if cross_attention_adaln:
+ self.prompt_scale_shift_table = Parameter(
+ total_shape=[2, 1, 1, video_dim],
+ mesh_axes=[None, None, None, None],
+ device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+
+ if has_audio:
+ self.audio_norm1 = DistributedRMSNorm(embedding_dim=audio_dim, **rms_norm_kwargs)
+ self.audio_attn1 = LTXAttention(dim=audio_dim, num_heads=audio_num_heads, is_self=True, **attn_kwargs)
+ self.audio_norm2 = DistributedRMSNorm(embedding_dim=audio_dim, **rms_norm_kwargs)
+ self.audio_attn2 = LTXAttention(
+ dim=audio_dim,
+ num_heads=audio_num_heads,
+ is_self=False,
+ context_dim=audio_cross_attention_dim,
+ **attn_kwargs,
+ )
+ self.audio_norm3 = DistributedRMSNorm(embedding_dim=audio_dim, **rms_norm_kwargs)
+ self.audio_ff = ParallelFeedForward(
+ audio_dim,
+ inner_dim=audio_ffn_dim,
+ activation_fn="gelu_tanh",
+ bias=True,
+ mesh_device=mesh_device,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ ccl_manager=ccl_manager,
+ fsdp_mesh_axis=fsdp_mesh_axis,
+ **ffn_kwargs,
+ )
+ self.audio_scale_shift_table = Parameter(
+ total_shape=[self.adaln_coeff, 1, 1, audio_dim],
+ mesh_axes=[None, None, None, parallel_config.tensor_parallel.mesh_axis],
+ device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ if cross_attention_adaln:
+ self.audio_prompt_scale_shift_table = Parameter(
+ total_shape=[2, 1, 1, audio_dim],
+ mesh_axes=[None, None, None, None],
+ device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+
+ self.audio_to_video_attn = LTXAttention(
+ dim=audio_dim,
+ num_heads=audio_num_heads,
+ is_self=False,
+ context_dim=audio_dim,
+ query_input_dim=video_dim,
+ output_dim=video_dim,
+ **attn_kwargs,
+ )
+ self.video_to_audio_attn = LTXAttention(
+ dim=audio_dim,
+ num_heads=audio_num_heads,
+ is_self=False,
+ context_dim=video_dim,
+ **attn_kwargs,
+ )
+ self.scale_shift_table_a2v_ca_audio = Parameter(
+ total_shape=[5, 1, 1, audio_dim],
+ mesh_axes=[None, None, None, parallel_config.tensor_parallel.mesh_axis],
+ device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ self.scale_shift_table_a2v_ca_video = Parameter(
+ total_shape=[5, 1, 1, video_dim],
+ mesh_axes=[None, None, None, parallel_config.tensor_parallel.mesh_axis],
+ device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+
+ if quant_config is not None:
+ self.ff_compute_kernel_config = quant_config.mm_compute_config(mesh_device.arch())
+ else:
+ self.ff_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi2,
+ math_approx_mode=False,
+ fp32_dest_acc_en=True,
+ packer_l1_acc=True,
+ )
+
+ def _prepare_torch_state(self, state: dict[str, torch.Tensor]) -> None:
+ rename_substate(state, "ff.net.0.proj", "ffn.ff1")
+ rename_substate(state, "ff.net.2", "ffn.ff2")
+
+ if self.has_audio:
+ rename_substate(state, "audio_ff.net.0.proj", "audio_ff.ff1")
+ rename_substate(state, "audio_ff.net.2", "audio_ff.ff2")
+
+ # Scale slots pre-incremented (+1) and cast to bf16 so each runtime
+ # modulation fuses to one ttnn.addcmul(shift, x, scale_p1).
+ tables_scale_idxs: dict[str, list[int]] = {}
+ tables_scale_idxs["scale_shift_table"] = [1, 4, 7] if self.cross_attention_adaln else [1, 4]
+ if self.cross_attention_adaln:
+ tables_scale_idxs["prompt_scale_shift_table"] = [1]
+ else:
+ pop_substate(state, "prompt_scale_shift_table")
+ if self.has_audio:
+ tables_scale_idxs["audio_scale_shift_table"] = [1, 4, 7] if self.cross_attention_adaln else [1, 4]
+ if self.cross_attention_adaln:
+ tables_scale_idxs["audio_prompt_scale_shift_table"] = [1]
+ else:
+ pop_substate(state, "audio_prompt_scale_shift_table")
+ tables_scale_idxs["scale_shift_table_a2v_ca_audio"] = [0, 2]
+ tables_scale_idxs["scale_shift_table_a2v_ca_video"] = [0, 2]
+ for key, scale_idxs in tables_scale_idxs.items():
+ if key in state:
+ # Reference table is [coeff, D]; reshape to outer-param [coeff, 1, 1, D]
+ # so dim 0 indexes the parameters (matches the (coeff,1,1,D) Parameter).
+ t = state[key].unsqueeze(1).unsqueeze(1).clone()
+ t[scale_idxs, :, :, :] += 1.0
+ state[key] = t.to(dtype=torch.bfloat16)
+
+ def _modulated_ffn(self, ffn, norm, x_1BND, shift_ff, scale_ff_p1, gate_ff):
+ """norm -> AdaLN (shift + x * scale_p1) -> gated FFN residual.
+
+ Ring fuses ff1(AG) + ff2 + RS + addcmul; Linear needs explicit AG + plain ffn().
+ """
+ normed = norm(x_1BND)
+ normed = ttnn.addcmul(shift_ff, normed, scale_ff_p1)
+ if self.ccl_manager.topology == ttnn.Topology.Ring:
+ return ffn.forward_fused_addcmul(
+ normed,
+ x_1BND,
+ gate_ff,
+ scalar=1.0,
+ compute_kernel_config=self.ff_compute_kernel_config,
+ parallel_config=self.parallel_config,
+ )
+ else:
+ if self.parallel_config.tensor_parallel.factor > 1:
+ normed = self.ccl_manager.all_gather_persistent_buffer(
+ normed, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ ff_out = ffn(normed, compute_kernel_config=self.ff_compute_kernel_config)
+ return ttnn.addcmul(x_1BND, ff_out, gate_ff)
+
+ def forward(
+ self,
+ video_1BND: ttnn.Tensor,
+ video_prompt: ttnn.Tensor,
+ video_temb: ttnn.Tensor, # (adaln_coeff, B, 1, video_dim)
+ video_N: int,
+ video_rope_cos: ttnn.Tensor,
+ video_rope_sin: ttnn.Tensor,
+ trans_mat: ttnn.Tensor | None = None,
+ video_prompt_temb: ttnn.Tensor | None = None,
+ # Audio args (only used when has_audio=True)
+ audio_1BND: ttnn.Tensor | None = None,
+ audio_prompt: ttnn.Tensor | None = None,
+ audio_temb: ttnn.Tensor | None = None,
+ av_ca_temb: ttnn.Tensor | None = None,
+ audio_N: int = 0,
+ audio_rope_cos: ttnn.Tensor | None = None,
+ audio_rope_sin: ttnn.Tensor | None = None,
+ av_ca_audio_temb: ttnn.Tensor | None = None,
+ audio_prompt_temb: ttnn.Tensor | None = None,
+ video_cross_pe_cos: ttnn.Tensor | None = None,
+ video_cross_pe_sin: ttnn.Tensor | None = None,
+ audio_cross_pe_cos: ttnn.Tensor | None = None,
+ audio_cross_pe_sin: ttnn.Tensor | None = None,
+ audio_cross_pe_cos_full: ttnn.Tensor | None = None,
+ audio_cross_pe_sin_full: ttnn.Tensor | None = None,
+ skip_cross_attn: bool = False,
+ skip_self_attn: bool = False,
+ audio_attn_mask: ttnn.Tensor | None = None,
+ audio_padding_mask: ttnn.Tensor | None = None,
+ audio_padding_mask_full: ttnn.Tensor | None = None,
+ video_padding_mask: ttnn.Tensor | None = None,
+ ) -> ttnn.Tensor | tuple[ttnn.Tensor, ttnn.Tensor]:
+ # Video modulation; `_p1` chunks carry +1 baked into the scale slot (see _prepare_torch_state).
+ shifted_v = self.scale_shift_table.data + video_temb
+ chunks = _tile_preserving_chunk0(shifted_v, self.adaln_coeff)
+ v_shift_sa, v_scale_sa_p1, v_gate_sa = chunks[0], chunks[1], chunks[2]
+ v_shift_ff, v_scale_ff_p1, v_gate_ff = chunks[3], chunks[4], chunks[5]
+ if self.cross_attention_adaln:
+ v_shift_ca, v_scale_ca_p1, v_gate_ca = chunks[6], chunks[7], chunks[8]
+
+ # Video self-attention
+ video_normed = self.norm1(video_1BND)
+ video_normed = ttnn.addcmul(v_shift_sa, video_normed, v_scale_sa_p1)
+ video_1BND = self.attn1(
+ spatial_1BND=video_normed,
+ N=video_N,
+ rope_cos=video_rope_cos,
+ rope_sin=video_rope_sin,
+ trans_mat=trans_mat,
+ addcmul_residual=video_1BND,
+ addcmul_gate=v_gate_sa,
+ skip_qk=skip_self_attn,
+ )
+
+ # Video text cross-attention
+ if self.cross_attention_adaln:
+ video_ca_input = ttnn.addcmul(v_shift_ca, self.norm2(video_1BND), v_scale_ca_p1)
+ if video_prompt_temb is not None:
+ shifted_prompt_v = self.prompt_scale_shift_table.data + video_prompt_temb
+ v_kv_shift, v_kv_scale_p1 = _tile_preserving_chunk0(shifted_prompt_v, 2)
+ video_prompt_mod = ttnn.addcmul(v_kv_shift, video_prompt, v_kv_scale_p1)
+ else:
+ video_prompt_mod = video_prompt
+ # Fold the gated residual (video_1BND + video_ca_out * v_gate_ca) into to_out's epilogue,
+ # mirroring attn1's fused path — avoids a separate full-sequence addcmul pass.
+ video_1BND = self.attn2(
+ spatial_1BND=video_ca_input,
+ N=video_N,
+ prompt_1BLP=video_prompt_mod,
+ kv_replicated=True,
+ addcmul_residual=video_1BND,
+ addcmul_gate=v_gate_ca,
+ )
+ else:
+ # 6-output mode: cross-attention with fixed norm (no AdaLN shift/scale/gate)
+ video_ca_input = self.norm2(video_1BND)
+ video_ca_out = self.attn2(
+ spatial_1BND=video_ca_input, N=video_N, prompt_1BLP=video_prompt, kv_replicated=True
+ )
+ video_1BND = video_1BND + video_ca_out
+
+ if not self.has_audio:
+ # Video-only feed forward
+ return self._modulated_ffn(self.ffn, self.norm3, video_1BND, v_shift_ff, v_scale_ff_p1, v_gate_ff)
+
+ # Audio path (has_audio=True from here)
+ shifted_a = self.audio_scale_shift_table.data + audio_temb
+ a_chunks = ttnn.chunk(shifted_a, self.adaln_coeff, dim=0)
+ a_shift_sa, a_scale_sa_p1, a_gate_sa = a_chunks[0], a_chunks[1], a_chunks[2]
+ a_shift_ff, a_scale_ff_p1, a_gate_ff = a_chunks[3], a_chunks[4], a_chunks[5]
+ if self.cross_attention_adaln:
+ a_shift_ca, a_scale_ca_p1, a_gate_ca = a_chunks[6], a_chunks[7], a_chunks[8]
+
+ # Audio self-attention
+ audio_normed = self.audio_norm1(audio_1BND)
+ audio_normed = ttnn.addcmul(a_shift_sa, audio_normed, a_scale_sa_p1)
+ audio_1BND = self.audio_attn1(
+ spatial_1BND=audio_normed,
+ N=audio_N,
+ rope_cos=audio_rope_cos,
+ rope_sin=audio_rope_sin,
+ trans_mat=trans_mat,
+ addcmul_residual=audio_1BND,
+ addcmul_gate=a_gate_sa,
+ skip_qk=skip_self_attn,
+ attn_mask=audio_attn_mask,
+ )
+
+ # Audio text cross-attention
+ if self.cross_attention_adaln:
+ audio_ca_input = ttnn.addcmul(a_shift_ca, self.audio_norm2(audio_1BND), a_scale_ca_p1)
+ if audio_prompt_temb is not None:
+ shifted_prompt_a = self.audio_prompt_scale_shift_table.data + audio_prompt_temb
+ a_kv_shift, a_kv_scale_p1 = ttnn.chunk(shifted_prompt_a, 2, dim=0)
+ audio_prompt_mod = ttnn.addcmul(a_kv_shift, audio_prompt, a_kv_scale_p1)
+ else:
+ audio_prompt_mod = audio_prompt
+ audio_1BND = self.audio_attn2(
+ spatial_1BND=audio_ca_input,
+ N=audio_N,
+ prompt_1BLP=audio_prompt_mod,
+ kv_replicated=True,
+ addcmul_residual=audio_1BND,
+ addcmul_gate=a_gate_ca,
+ )
+ else:
+ audio_ca_input = self.audio_norm2(audio_1BND)
+ audio_ca_out = self.audio_attn2(
+ spatial_1BND=audio_ca_input, N=audio_N, prompt_1BLP=audio_prompt, kv_replicated=True
+ )
+ audio_1BND = audio_1BND + audio_ca_out
+
+ # Bidirectional A<->V cross-attention
+ if not skip_cross_attn:
+ # Chunk layout [scale, shift, scale, shift, gate]: scale slots (idx 0, 2) have +1 baked in.
+ shifted_av = self.scale_shift_table_a2v_ca_video.data + av_ca_temb
+ v_ca_scale_p1, v_ca_shift, a_ca_scale_v_p1, a_ca_shift_v, v_ca_gate = ttnn.chunk(shifted_av, 5, dim=0)
+
+ shifted_av_a = self.scale_shift_table_a2v_ca_audio.data + av_ca_audio_temb
+ a_scale_a2v_p1, a_shift_a2v, a_scale_v2a_p1, a_shift_v2a, a_ca_gate = ttnn.chunk(shifted_av_a, 5, dim=0)
+
+ video_normed_xattn = self.norm3(video_1BND)
+ audio_normed_xattn = self.audio_norm3(audio_1BND)
+
+ # A→V: audio provides context for video
+ video_q_a2v = ttnn.addcmul(v_ca_shift, video_normed_xattn, v_ca_scale_p1)
+ audio_kv_a2v = ttnn.addcmul(a_shift_a2v, audio_normed_xattn, a_scale_a2v_p1)
+ if self.parallel_config.sequence_parallel.factor > 1:
+ audio_kv_a2v = self.ccl_manager.all_gather_persistent_buffer(
+ audio_kv_a2v, dim=2, mesh_axis=self.parallel_config.sequence_parallel.mesh_axis
+ )
+ # Zero padded audio tokens before to_kv so they don't contribute to A->V cross-attention.
+ pad_mask_a2v = audio_padding_mask_full if audio_padding_mask_full is not None else audio_padding_mask
+ if pad_mask_a2v is not None:
+ audio_kv_a2v = ttnn.multiply(audio_kv_a2v, pad_mask_a2v)
+ a2v_output = self.audio_to_video_attn(
+ spatial_1BND=video_q_a2v,
+ N=video_N,
+ prompt_1BLP=audio_kv_a2v,
+ rope_cos=video_cross_pe_cos,
+ rope_sin=video_cross_pe_sin,
+ k_rope_cos=audio_cross_pe_cos_full,
+ k_rope_sin=audio_cross_pe_sin_full,
+ trans_mat=trans_mat,
+ addcmul_residual=video_1BND,
+ addcmul_gate=v_ca_gate,
+ )
+ video_1BND = a2v_output
+
+ # V→A: video provides context for audio
+ audio_q_v2a = ttnn.addcmul(a_shift_v2a, audio_normed_xattn, a_scale_v2a_p1)
+ video_kv_v2a = ttnn.addcmul(a_ca_shift_v, video_normed_xattn, a_ca_scale_v_p1)
+ # Zero padded video tokens (on the SP-local shard) after the affine, before to_kv.
+ if video_padding_mask is not None:
+ video_kv_v2a = ttnn.multiply(video_kv_v2a, video_padding_mask)
+ v2a_output = self.video_to_audio_attn(
+ spatial_1BND=audio_q_v2a,
+ N=audio_N,
+ prompt_1BLP=video_kv_v2a,
+ rope_cos=audio_cross_pe_cos,
+ rope_sin=audio_cross_pe_sin,
+ # Ring cross keeps video K/V SP-sharded: pass the sharded K-rope and kv_logical_n so
+ # the ring SDPA gathers internally instead of a separate K/V all-gather.
+ k_rope_cos=video_cross_pe_cos,
+ k_rope_sin=video_cross_pe_sin,
+ kv_logical_n=video_N,
+ trans_mat=trans_mat,
+ addcmul_residual=audio_1BND,
+ addcmul_gate=a_ca_gate,
+ )
+ audio_1BND = v2a_output
+
+ # Video feed forward
+ video_1BND = self._modulated_ffn(self.ffn, self.norm3, video_1BND, v_shift_ff, v_scale_ff_p1, v_gate_ff)
+
+ # Audio feed forward
+ audio_1BND = self._modulated_ffn(
+ self.audio_ff, self.audio_norm3, audio_1BND, a_shift_ff, a_scale_ff_p1, a_gate_ff
+ )
+
+ return video_1BND, audio_1BND
+
+
+class LTXTransformerModel(Module):
+ def __init__(
+ self,
+ *,
+ # Video config
+ num_attention_heads: int = 32,
+ attention_head_dim: int = 128,
+ in_channels: int = 128,
+ out_channels: int = 128,
+ num_layers: int = 48,
+ cross_attention_dim: int = 4096,
+ # Audio config (only used when has_audio=True)
+ audio_num_attention_heads: int = 32,
+ audio_attention_head_dim: int = 64,
+ audio_in_channels: int = 128,
+ audio_out_channels: int = 128,
+ audio_cross_attention_dim: int = 2048,
+ # Common
+ norm_eps: float = 1e-6,
+ ffn_mult: int = 4,
+ mesh_device: ttnn.MeshDevice,
+ ccl_manager: CCLManager | None = None,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool = False,
+ has_audio: bool = False,
+ apply_gated_attention: bool = False,
+ cross_attention_adaln: bool = True,
+ lora_enabled: bool = False,
+ image_conditioning: bool = False,
+ quant_config: LtxQuantProfile | None = None,
+ ) -> None:
+ super().__init__()
+
+ self.inner_dim = num_attention_heads * attention_head_dim
+ self.in_channels = in_channels
+ self.out_channels = out_channels
+ self.num_layers = num_layers
+ self.has_audio = has_audio
+ self.cross_attention_adaln = cross_attention_adaln
+ self.lora_enabled = lora_enabled
+ # I2V: video AdaLN modulation is per-token (denoise_mask * sigma) instead of batch-scalar.
+ # Audio / prompt / A<->V cross AdaLN stay batch-scalar regardless.
+ self.image_conditioning = image_conditioning
+ self.mesh_device = mesh_device
+ self.ccl_manager = ccl_manager
+ self.parallel_config = parallel_config
+
+ if has_audio:
+ self.audio_inner_dim = audio_num_attention_heads * audio_attention_head_dim
+
+ self.patchify_proj = ColParallelLinear(
+ in_channels,
+ self.inner_dim,
+ bias=True,
+ mesh_device=mesh_device,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ ccl_manager=ccl_manager,
+ )
+ adaln_coeff = 9 if cross_attention_adaln else 6
+ self.adaln_single = LTXAdaLayerNormSingle(
+ embedding_dim=self.inner_dim,
+ embedding_coefficient=adaln_coeff,
+ mesh_device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ if cross_attention_adaln:
+ self.prompt_adaln_single = LTXAdaLayerNormSingle(
+ embedding_dim=self.inner_dim,
+ embedding_coefficient=2,
+ mesh_device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ self.norm_out = DistributedLayerNorm(
+ self.inner_dim,
+ norm_eps=norm_eps,
+ norm_elementwise_affine=False,
+ bias=False,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ )
+ self.scale_shift_table = Parameter(
+ total_shape=[1, 1, 2, self.inner_dim],
+ mesh_axes=[None, None, None, parallel_config.tensor_parallel.mesh_axis],
+ device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ self.proj_out = Linear(self.inner_dim, out_channels, bias=True, mesh_device=mesh_device)
+
+ if has_audio:
+ self.audio_patchify_proj = ColParallelLinear(
+ audio_in_channels,
+ self.audio_inner_dim,
+ bias=True,
+ mesh_device=mesh_device,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ ccl_manager=ccl_manager,
+ )
+ self.audio_adaln_single = LTXAdaLayerNormSingle(
+ embedding_dim=self.audio_inner_dim,
+ embedding_coefficient=adaln_coeff,
+ mesh_device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ if cross_attention_adaln:
+ self.audio_prompt_adaln_single = LTXAdaLayerNormSingle(
+ embedding_dim=self.audio_inner_dim,
+ embedding_coefficient=2,
+ mesh_device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ self.audio_norm_out = DistributedLayerNorm(
+ self.audio_inner_dim,
+ norm_eps=norm_eps,
+ norm_elementwise_affine=False,
+ bias=False,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ )
+ self.audio_scale_shift_table = Parameter(
+ total_shape=[1, 1, 2, self.audio_inner_dim],
+ mesh_axes=[None, None, None, parallel_config.tensor_parallel.mesh_axis],
+ device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ self.audio_proj_out = Linear(self.audio_inner_dim, audio_out_channels, bias=True, mesh_device=mesh_device)
+
+ # Cross-attention timestep conditioning
+ self.av_ca_video_scale_shift_adaln_single = LTXAdaLayerNormSingle(
+ embedding_dim=self.inner_dim,
+ embedding_coefficient=4,
+ mesh_device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ self.av_ca_audio_scale_shift_adaln_single = LTXAdaLayerNormSingle(
+ embedding_dim=self.audio_inner_dim,
+ embedding_coefficient=4,
+ mesh_device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ self.av_ca_a2v_gate_adaln_single = LTXAdaLayerNormSingle(
+ embedding_dim=self.inner_dim,
+ embedding_coefficient=1,
+ mesh_device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+ self.av_ca_v2a_gate_adaln_single = LTXAdaLayerNormSingle(
+ embedding_dim=self.audio_inner_dim,
+ embedding_coefficient=1,
+ mesh_device=mesh_device,
+ dtype=ttnn.bfloat16,
+ )
+
+ video_ffn_dim = self.inner_dim * ffn_mult
+ audio_ffn_dim = self.audio_inner_dim * ffn_mult if has_audio else 0
+ self.transformer_blocks = ModuleList()
+ for _ in range(num_layers):
+ self.transformer_blocks.append(
+ LTXTransformerBlock(
+ video_dim=self.inner_dim,
+ audio_dim=self.audio_inner_dim if has_audio else 0,
+ video_ffn_dim=video_ffn_dim,
+ audio_ffn_dim=audio_ffn_dim,
+ video_num_heads=num_attention_heads,
+ audio_num_heads=audio_num_attention_heads if has_audio else 0,
+ video_cross_attention_dim=cross_attention_dim,
+ audio_cross_attention_dim=audio_cross_attention_dim if has_audio else 0,
+ eps=norm_eps,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=has_audio,
+ apply_gated_attention=apply_gated_attention,
+ cross_attention_adaln=cross_attention_adaln,
+ quant_config=quant_config,
+ lora_enabled=lora_enabled,
+ )
+ )
+
+ # Output-proj config matches WanTransformer's hifi4 config (packer_l1_acc=True).
+ self.hifi4_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi4,
+ math_approx_mode=False,
+ fp32_dest_acc_en=True,
+ packer_l1_acc=True,
+ )
+
+ def _prepare_torch_state(self, state: dict[str, torch.Tensor]) -> None:
+ # Output-proj table is [shift, scale]; bake +1 into the scale slot and cast to bf16.
+ tables = ["scale_shift_table"]
+ if self.has_audio:
+ tables.append("audio_scale_shift_table")
+ for key in tables:
+ if key in state:
+ t = state[key].unsqueeze(0).unsqueeze(0).clone()
+ t[:, :, 1, :] += 1.0
+ state[key] = t.to(dtype=torch.bfloat16)
+
+ # Remove keys not implemented in TTNN
+ pop_substate(state, "video_embeddings_connector")
+ pop_substate(state, "audio_embeddings_connector")
+ # Caption projection is a 19B-distilled-only artifact, not implemented in TTNN.
+ pop_substate(state, "caption_projection")
+ if self.has_audio:
+ pop_substate(state, "audio_caption_projection")
+ # 6-output mode: no prompt_adaln modules
+ if not self.cross_attention_adaln:
+ pop_substate(state, "prompt_adaln_single")
+ if self.has_audio:
+ pop_substate(state, "audio_prompt_adaln_single")
+
+ def weight_cache_subfolder(self) -> str:
+ """Cache subfolder naming this model's weight LAYOUT, not just its parallel config.
+
+ On a Ring topology the attention gate is fused into the QKV/Q projection
+ (``LTXAttention.fuse_gate``): ``to_qkv`` grows by one padded tile per device and the
+ separate gate tensor disappears, so the cached tensorbins have different shapes and a
+ different count than the Linear-topology layout. Both used to land under ``transformer/``
+ under an identical key, and the second topology to run died loading the first one's cache.
+
+ A ``transformer/`` cache written by a Ring run before this split is stale (nothing reads it any
+ more) and can be deleted; Ring now writes ``transformer_fusedgate/``.
+ """
+ fused = any(getattr(block.attn1, "fuse_gate", False) for block in self.transformer_blocks)
+ return "transformer_fusedgate" if fused else "transformer"
+
+ def forward(
+ self,
+ # Video
+ video_1BNI_torch: torch.Tensor,
+ video_prompt_1BLP: ttnn.Tensor,
+ video_rope_cos: ttnn.Tensor,
+ video_rope_sin: ttnn.Tensor,
+ video_N: int,
+ # Shared
+ trans_mat: ttnn.Tensor | None,
+ timestep_torch: torch.Tensor,
+ # I2V per-token video timestep (B, N); required when image_conditioning=True.
+ video_timestep_torch: torch.Tensor | None = None,
+ # Audio (only used when has_audio=True)
+ audio_1BNI_torch: torch.Tensor | None = None,
+ audio_prompt_1BLP: ttnn.Tensor | None = None,
+ audio_rope_cos: ttnn.Tensor | None = None,
+ audio_rope_sin: ttnn.Tensor | None = None,
+ audio_N: int = 0,
+ # Cross-modal positional embeddings (optional)
+ video_cross_pe_cos: ttnn.Tensor | None = None,
+ video_cross_pe_sin: ttnn.Tensor | None = None,
+ audio_cross_pe_cos: ttnn.Tensor | None = None,
+ audio_cross_pe_sin: ttnn.Tensor | None = None,
+ audio_cross_pe_cos_full: ttnn.Tensor | None = None,
+ audio_cross_pe_sin_full: ttnn.Tensor | None = None,
+ skip_cross_attn: bool = False,
+ skip_self_attn_blocks: list[int] | None = None,
+ audio_attn_mask: ttnn.Tensor | None = None,
+ audio_padding_mask: ttnn.Tensor | None = None,
+ audio_padding_mask_full: ttnn.Tensor | None = None,
+ video_padding_mask: ttnn.Tensor | None = None,
+ ) -> ttnn.Tensor | tuple[ttnn.Tensor, ttnn.Tensor]:
+ """Host entry: upload torch latents/timestep, then run the device-only inner_step."""
+ sp_axis = self.parallel_config.sequence_parallel.mesh_axis
+ video_1BNI = bf16_tensor(video_1BNI_torch, device=self.mesh_device, mesh_axis=sp_axis, shard_dim=-2)
+ B_size = video_1BNI_torch.shape[1]
+ # Scalar timestep (1,1,B,1) drives audio / prompt / A<->V cross modulation.
+ timestep = bf16_tensor(timestep_torch.reshape(1, 1, B_size, 1) * 1000.0, device=self.mesh_device)
+ # Per-token video timestep (1,1,B*N,1), SP-sharded on dim 2 to match video_1BNI (shard_dim=-2).
+ video_timestep = None
+ if self.image_conditioning:
+ assert video_timestep_torch is not None, "video_timestep_torch required when image_conditioning=True"
+ video_timestep = bf16_tensor(
+ video_timestep_torch.reshape(1, 1, -1, 1) * 1000.0,
+ device=self.mesh_device,
+ mesh_axis=sp_axis,
+ shard_dim=-2,
+ )
+ audio_1BNI = (
+ bf16_tensor(audio_1BNI_torch, device=self.mesh_device, mesh_axis=sp_axis, shard_dim=-2)
+ if self.has_audio
+ else None
+ )
+ return self.inner_step(
+ video_1BNI=video_1BNI,
+ timestep=timestep,
+ video_timestep=video_timestep,
+ audio_1BNI=audio_1BNI,
+ video_prompt_1BLP=video_prompt_1BLP,
+ video_rope_cos=video_rope_cos,
+ video_rope_sin=video_rope_sin,
+ video_N=video_N,
+ trans_mat=trans_mat,
+ audio_prompt_1BLP=audio_prompt_1BLP,
+ audio_rope_cos=audio_rope_cos,
+ audio_rope_sin=audio_rope_sin,
+ audio_N=audio_N,
+ video_cross_pe_cos=video_cross_pe_cos,
+ video_cross_pe_sin=video_cross_pe_sin,
+ audio_cross_pe_cos=audio_cross_pe_cos,
+ audio_cross_pe_sin=audio_cross_pe_sin,
+ audio_cross_pe_cos_full=audio_cross_pe_cos_full,
+ audio_cross_pe_sin_full=audio_cross_pe_sin_full,
+ skip_cross_attn=skip_cross_attn,
+ skip_self_attn_blocks=skip_self_attn_blocks,
+ audio_attn_mask=audio_attn_mask,
+ audio_padding_mask=audio_padding_mask,
+ audio_padding_mask_full=audio_padding_mask_full,
+ video_padding_mask=video_padding_mask,
+ )
+
+ @traced_function(device=lambda self: self.mesh_device, clone_prep_inputs=False, prep_run=False)
+ def inner_step(
+ self,
+ *,
+ video_1BNI: ttnn.Tensor,
+ timestep: ttnn.Tensor,
+ video_timestep: ttnn.Tensor | None = None,
+ video_ts_pair: ttnn.Tensor | None = None,
+ video_pin_mask: ttnn.Tensor | None = None,
+ audio_1BNI: ttnn.Tensor | None = None,
+ video_prompt_1BLP: ttnn.Tensor = None,
+ video_rope_cos: ttnn.Tensor = None,
+ video_rope_sin: ttnn.Tensor = None,
+ video_N: int = 0,
+ trans_mat: ttnn.Tensor | None = None,
+ audio_prompt_1BLP: ttnn.Tensor | None = None,
+ audio_rope_cos: ttnn.Tensor | None = None,
+ audio_rope_sin: ttnn.Tensor | None = None,
+ audio_N: int = 0,
+ video_cross_pe_cos: ttnn.Tensor | None = None,
+ video_cross_pe_sin: ttnn.Tensor | None = None,
+ audio_cross_pe_cos: ttnn.Tensor | None = None,
+ audio_cross_pe_sin: ttnn.Tensor | None = None,
+ audio_cross_pe_cos_full: ttnn.Tensor | None = None,
+ audio_cross_pe_sin_full: ttnn.Tensor | None = None,
+ skip_cross_attn: bool = False,
+ skip_self_attn_blocks: list[int] | None = None,
+ audio_attn_mask: ttnn.Tensor | None = None,
+ audio_padding_mask: ttnn.Tensor | None = None,
+ audio_padding_mask_full: ttnn.Tensor | None = None,
+ video_padding_mask: ttnn.Tensor | None = None,
+ gather_output: bool = True,
+ ) -> ttnn.Tensor | tuple[ttnn.Tensor, ttnn.Tensor]:
+ """Device-only, trace-capturable denoising step. All tensor args are ttnn (no torch).
+ ``video_1BNI``/``audio_1BNI``/``timestep`` are the per-step inputs; the rest are
+ constant across a denoise stage.
+
+ ``gather_output`` SP-gathers the velocity outputs to full sequence length (default,
+ for the host-Euler path). Pass ``False`` to keep them SP-sharded so an on-device
+ solver can step the latent without leaving the device (the traced WAN pattern)."""
+ # Video modulation (6 or 9 params depending on cross_attention_adaln).
+ adaln_coeff = 9 if self.cross_attention_adaln else 6
+ # I2V compact path: per-token timestep has 2 values (pinned frame-0 vs. sigma), passed as a
+ # (1,1,2,1) pair + {0,1} pin mask. Blend per token to avoid the dense (1,1,N,coeff*D) modulation.
+ compact_i2v = self.image_conditioning and video_ts_pair is not None and video_pin_mask is not None
+ if compact_i2v:
+ N = video_pin_mask.shape[2]
+ mod_pair, emb_pair = self.adaln_single(video_ts_pair) # (1,1,2,coeff*D), (1,1,2,D)
+ mod_pair = ttnn.reshape(mod_pair, (1, 2, adaln_coeff, self.inner_dim))
+ if self.parallel_config.tensor_parallel.factor > 1:
+ mod_pair = ttnn.mesh_partition(
+ mod_pair, dim=3, cluster_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ mod_pair = ttnn.permute(mod_pair, (2, 0, 1, 3)) # (coeff,1,2,Dloc)
+ mod_pin, mod_base = ttnn.chunk(mod_pair, 2, dim=2) # each (coeff,1,1,Dloc)
+ ttnn.deallocate(mod_pair)
+ # video_mod = base + (pin - base) * mask, materialized per token via broadcast-safe ops.
+ mod_delta = ttnn.sub(mod_pin, mod_base)
+ mod_delta = ttnn.repeat(mod_delta, ttnn.Shape([1, 1, N, 1])) # (coeff,1,N,Dloc)
+ mod_delta = ttnn.mul(mod_delta, video_pin_mask) # mask broadcasts over coeff/Dloc
+ video_mod_CB1D = ttnn.add(mod_base, mod_delta) # base broadcasts over N
+ ttnn.deallocate(mod_delta)
+ ttnn.deallocate(mod_pin)
+ ttnn.deallocate(mod_base)
+ # Embedded timestep (for norm_out), same per-token blend, kept full-D.
+ emb_pin, emb_base = ttnn.chunk(emb_pair, 2, dim=2) # each (1,1,1,D)
+ ttnn.deallocate(emb_pair)
+ emb_delta = ttnn.sub(emb_pin, emb_base)
+ emb_delta = ttnn.repeat(emb_delta, ttnn.Shape([1, 1, N, 1])) # (1,1,N,D)
+ emb_delta = ttnn.mul(emb_delta, video_pin_mask)
+ video_emb_ts = ttnn.add(emb_base, emb_delta)
+ ttnn.deallocate(emb_delta)
+ ttnn.deallocate(emb_pin)
+ ttnn.deallocate(emb_base)
+ B = 1
+ else:
+ # I2V (dense): feed the per-token timestep so each token gets its own AdaLN modulation.
+ video_ts = video_timestep if self.image_conditioning else timestep
+ video_modulation, video_emb_ts = self.adaln_single(video_ts)
+ # dim 2 is B (scalar) or B*N (per-token, B=1 -> N_local on the SP shard).
+ X = video_modulation.shape[2]
+ video_mod_CB1D = ttnn.reshape(video_modulation, (1, X, adaln_coeff, self.inner_dim))
+ if self.parallel_config.tensor_parallel.factor > 1:
+ video_mod_CB1D = ttnn.mesh_partition(
+ video_mod_CB1D, dim=3, cluster_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ # Move the coeff axis to dim 0 once per step so each block's chunk(dim=0) is free.
+ # Scalar: (1,B,coeff,D) -> (coeff,B,1,D). Per-token: (1,N,coeff,D) -> (coeff,1,N,D).
+ if self.image_conditioning:
+ video_mod_CB1D = ttnn.permute(video_mod_CB1D, (2, 0, 1, 3))
+ else:
+ video_mod_CB1D = ttnn.permute(video_mod_CB1D, (2, 1, 0, 3))
+ B = 1 if self.image_conditioning else X
+
+ # Video prompt modulation (2 params, only for 9-output mode)
+ video_prompt_2B1D = None
+ if self.cross_attention_adaln:
+ video_prompt_mod, _ = self.prompt_adaln_single(timestep)
+ video_prompt_2B1D = ttnn.reshape(video_prompt_mod, (1, B, 2, self.inner_dim))
+ video_prompt_2B1D = ttnn.permute(video_prompt_2B1D, (2, 1, 0, 3))
+
+ # Audio modulation (only when has_audio)
+ audio_mod_CB1D = None
+ audio_prompt_2B1D = None
+ av_ca_video_temb = None
+ av_ca_audio_temb = None
+ audio_emb_ts = None
+
+ if self.has_audio:
+ audio_modulation, audio_emb_ts = self.audio_adaln_single(timestep)
+ audio_mod_CB1D = ttnn.reshape(audio_modulation, (1, B, adaln_coeff, self.audio_inner_dim))
+ if self.parallel_config.tensor_parallel.factor > 1:
+ audio_mod_CB1D = ttnn.mesh_partition(
+ audio_mod_CB1D, dim=3, cluster_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ audio_mod_CB1D = ttnn.permute(audio_mod_CB1D, (2, 1, 0, 3))
+
+ if self.cross_attention_adaln:
+ audio_prompt_mod, _ = self.audio_prompt_adaln_single(timestep)
+ audio_prompt_2B1D = ttnn.reshape(audio_prompt_mod, (1, B, 2, self.audio_inner_dim))
+ audio_prompt_2B1D = ttnn.permute(audio_prompt_2B1D, (2, 1, 0, 3))
+
+ # Cross-attention timestep conditioning; concat on the param axis (dim 0).
+ av_ca_video_ss, _ = self.av_ca_video_scale_shift_adaln_single(timestep)
+ av_ca_video_ss = ttnn.permute(ttnn.reshape(av_ca_video_ss, (1, B, 4, self.inner_dim)), (2, 1, 0, 3))
+ av_ca_a2v_gate, _ = self.av_ca_a2v_gate_adaln_single(timestep)
+ av_ca_a2v_gate = ttnn.permute(ttnn.reshape(av_ca_a2v_gate, (1, B, 1, self.inner_dim)), (2, 1, 0, 3))
+ av_ca_video_temb = ttnn.concat([av_ca_video_ss, av_ca_a2v_gate], dim=0)
+
+ av_ca_audio_ss, _ = self.av_ca_audio_scale_shift_adaln_single(timestep)
+ av_ca_audio_ss = ttnn.permute(ttnn.reshape(av_ca_audio_ss, (1, B, 4, self.audio_inner_dim)), (2, 1, 0, 3))
+ av_ca_v2a_gate, _ = self.av_ca_v2a_gate_adaln_single(timestep)
+ av_ca_v2a_gate = ttnn.permute(ttnn.reshape(av_ca_v2a_gate, (1, B, 1, self.audio_inner_dim)), (2, 1, 0, 3))
+ av_ca_audio_temb = ttnn.concat([av_ca_audio_ss, av_ca_v2a_gate], dim=0)
+
+ if self.parallel_config.tensor_parallel.factor > 1:
+ av_ca_video_temb = ttnn.mesh_partition(
+ av_ca_video_temb, dim=3, cluster_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ av_ca_audio_temb = ttnn.mesh_partition(
+ av_ca_audio_temb, dim=3, cluster_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+
+ # Patchify
+ video_1BND = self.patchify_proj(video_1BNI)
+ audio_1BND = self.audio_patchify_proj(audio_1BNI) if self.has_audio else None
+
+ skip_self_attn_set = frozenset(skip_self_attn_blocks) if skip_self_attn_blocks else frozenset()
+
+ # Transformer blocks
+ for block_idx, block in enumerate(self.transformer_blocks):
+ result = block(
+ video_1BND=video_1BND,
+ video_prompt=video_prompt_1BLP,
+ video_temb=video_mod_CB1D,
+ video_N=video_N,
+ video_rope_cos=video_rope_cos,
+ video_rope_sin=video_rope_sin,
+ trans_mat=trans_mat,
+ video_prompt_temb=video_prompt_2B1D,
+ audio_1BND=audio_1BND,
+ audio_prompt=audio_prompt_1BLP,
+ audio_temb=audio_mod_CB1D,
+ av_ca_temb=av_ca_video_temb,
+ audio_N=audio_N,
+ audio_rope_cos=audio_rope_cos,
+ audio_rope_sin=audio_rope_sin,
+ av_ca_audio_temb=av_ca_audio_temb,
+ audio_prompt_temb=audio_prompt_2B1D,
+ video_cross_pe_cos=video_cross_pe_cos,
+ video_cross_pe_sin=video_cross_pe_sin,
+ audio_cross_pe_cos=audio_cross_pe_cos,
+ audio_cross_pe_sin=audio_cross_pe_sin,
+ audio_cross_pe_cos_full=audio_cross_pe_cos_full,
+ audio_cross_pe_sin_full=audio_cross_pe_sin_full,
+ skip_cross_attn=skip_cross_attn,
+ skip_self_attn=block_idx in skip_self_attn_set,
+ audio_attn_mask=audio_attn_mask,
+ audio_padding_mask=audio_padding_mask,
+ audio_padding_mask_full=audio_padding_mask_full,
+ video_padding_mask=video_padding_mask,
+ )
+ if self.has_audio:
+ video_1BND, audio_1BND = result
+ else:
+ video_1BND = result
+
+ v_inner_local = video_emb_ts.shape[-1]
+ if self.image_conditioning:
+ # Per-token (video_emb_ts is (1,1,N,D)): split the (shift, scale) table, broadcast-add per token.
+ v_emb_1B1D = video_emb_ts
+ if self.parallel_config.tensor_parallel.factor > 1:
+ v_emb_1B1D = ttnn.mesh_partition(
+ v_emb_1B1D, dim=3, cluster_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ sst_shift, sst_scale_p1 = ttnn.chunk(self.scale_shift_table.data, 2, dim=2) # each (1,1,1,Dloc)
+ v_shift_out = ttnn.add(sst_shift, v_emb_1B1D) # (1,1,N,Dloc)
+ v_scale_out_p1 = ttnn.add(sst_scale_p1, v_emb_1B1D)
+ else:
+ v_emb_1B1D = ttnn.reshape(video_emb_ts, (1, B, 1, v_inner_local))
+ if self.parallel_config.tensor_parallel.factor > 1:
+ v_emb_1B1D = ttnn.mesh_partition(
+ v_emb_1B1D, dim=3, cluster_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ shifted_v = self.scale_shift_table.data + v_emb_1B1D
+ v_shift_out, v_scale_out_p1 = ttnn.chunk(shifted_v, 2, dim=2)
+ if self.image_conditioning:
+ # Per-token: fused norm_out gamma/beta can't carry per-token scale/shift, so plain
+ # layernorm then manual shift + normed * scale_p1, in fp32 to match the T2V branch.
+ video_1BND = self.norm_out(video_1BND, dtype=ttnn.float32)
+ v_shift_out = ttnn.typecast(v_shift_out, ttnn.float32)
+ v_scale_out_p1 = ttnn.typecast(v_scale_out_p1, ttnn.float32)
+ video_1BND = ttnn.addcmul(v_shift_out, video_1BND, v_scale_out_p1)
+ else:
+ # Fuse the AdaLN (1 + scale) * normed + shift modulation into norm_out (WAN pattern).
+ video_1BND = self.norm_out(
+ video_1BND, dynamic_weight=v_scale_out_p1, dynamic_bias=v_shift_out, dtype=ttnn.float32
+ )
+ if self.parallel_config.tensor_parallel.factor > 1:
+ video_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ video_1BND, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ video_out = self.proj_out(
+ video_1BND, compute_kernel_config=self.hifi4_compute_kernel_config, dtype=ttnn.float32
+ )
+
+ # Output projection — audio
+ if self.has_audio:
+ a_inner_local = audio_emb_ts.shape[-1]
+ a_emb_1B1D = ttnn.reshape(audio_emb_ts, (1, B, 1, a_inner_local))
+ if self.parallel_config.tensor_parallel.factor > 1:
+ a_emb_1B1D = ttnn.mesh_partition(
+ a_emb_1B1D, dim=3, cluster_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ shifted_a = self.audio_scale_shift_table.data + a_emb_1B1D
+ a_shift_out, a_scale_out_p1 = ttnn.chunk(shifted_a, 2, dim=2)
+ # Fuse the AdaLN (1 + scale) * normed + shift modulation into audio_norm_out (WAN pattern).
+ audio_1BND = self.audio_norm_out(
+ audio_1BND, dynamic_weight=a_scale_out_p1, dynamic_bias=a_shift_out, dtype=ttnn.float32
+ )
+ if self.parallel_config.tensor_parallel.factor > 1:
+ audio_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ audio_1BND, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+ audio_out = self.audio_proj_out(
+ audio_1BND, compute_kernel_config=self.hifi4_compute_kernel_config, dtype=ttnn.float32
+ )
+
+ # SP gather (skipped when an on-device solver steps the still-sharded latent)
+ if gather_output and self.parallel_config.sequence_parallel.factor > 1:
+ video_out = self.ccl_manager.all_gather_persistent_buffer(
+ video_out, dim=2, mesh_axis=self.parallel_config.sequence_parallel.mesh_axis
+ )
+ if self.has_audio:
+ audio_out = self.ccl_manager.all_gather_persistent_buffer(
+ audio_out, dim=2, mesh_axis=self.parallel_config.sequence_parallel.mesh_axis
+ )
+
+ if self.has_audio:
+ return video_out, audio_out
+ return video_out
+
+ @staticmethod
+ def device_to_host(
+ tt_tensor: ttnn.Tensor,
+ *,
+ ccl_manager: CCLManager | None = None,
+ parallel_config: DiTParallelConfig | None = None,
+ sp_already_gathered: bool = False,
+ tp_already_gathered: bool = False,
+ ) -> torch.Tensor:
+ """Assemble SP/TP shards on host (do not read device 0 alone on multi-device meshes).
+
+ ``inner_step`` already AllGathers velocities on dim=2 before return; pass
+ ``sp_already_gathered=True`` from the denoise loop to avoid concatenating the
+ full sequence twice (audible doubling / ghost vocals).
+ """
+ if ccl_manager is not None and parallel_config is not None:
+ mesh_dims: list[int | None] = [None, None]
+ if not tp_already_gathered and parallel_config.tensor_parallel.factor > 1:
+ mesh_dims[parallel_config.tensor_parallel.mesh_axis] = 3
+ if not sp_already_gathered and parallel_config.sequence_parallel.factor > 1:
+ mesh_dims[parallel_config.sequence_parallel.mesh_axis] = 2
+ return ccl_manager.device_to_host(tt_tensor, mesh_dims).float().clone()
+ return ttnn.to_torch(ttnn.get_device_tensors(tt_tensor)[0]).float()
+
+
+class LTXTransformerCheckpoint:
+ """An LTX transformer checkpoint: parses variant config, builds transformers, and loads weights.
+
+ Mirrors ``WanCheckpoint`` but for the single-file LTX safetensors layout. The pipeline-level
+ flags that vary per instance (``has_audio`` / ``image_conditioning``) are passed into ``build``
+ explicitly — the checkpoint never reads pipeline state. LoRA specs vary per variant, so they
+ are threaded through ``state_dict`` / ``cache_name`` / ``load`` rather than stored.
+ """
+
+ def __init__(self, checkpoint_path: str, *, inner_dim: int) -> None:
+ self._checkpoint_path = checkpoint_path
+ # Transformer + connector detection (key scan + one tensor-shape read). No tensor loads.
+ with safe_open(checkpoint_path, framework="pt") as f:
+ keys = list(f.keys())
+ adaln_key = "model.diffusion_model.adaln_single.linear.weight"
+ if adaln_key in keys:
+ self.cross_attention_adaln = f.get_tensor(adaln_key).shape[0] > 6 * inner_dim
+ else:
+ self.cross_attention_adaln = True
+ self.has_gate = any("to_gate_logits" in k for k in keys)
+ logger.info(f"Detected: has_gate={self.has_gate}, cross_attention_adaln={self.cross_attention_adaln}")
+
+ def state_dict(self, lora_specs: list[LoraSpec]) -> dict[str, torch.Tensor]:
+ """Load + LoRA-fuse the transformer state dict from safetensors. Only
+ invoked on cache miss by ``cache_module.load_model``."""
+ logger.info(f"Transformer cache miss — loading safetensors: {self._checkpoint_path}")
+ raw = load_file(self._checkpoint_path)
+ prefix = "model.diffusion_model."
+ sd = {k[len(prefix) :]: v for k, v in raw.items() if k.startswith(prefix)}
+ if lora_specs:
+ sd = fuse_loras_into(sd, lora_specs)
+ return sd
+
+ def cache_name(self, lora_specs: list[LoraSpec], quant_tag: str | None = None) -> str:
+ """Cache key for ``cache_module.load_model``. LoRA-tagged so fused and base weights don't
+ alias in ``TT_DIT_CACHE_DIR``; quant-tagged because cached tensorbins carry their dtype, so
+ a bf8 preset run and the bf16 baseline must live in separate dirs."""
+ base = os.path.basename(self._checkpoint_path).removesuffix(".safetensors")
+ if lora_specs:
+ tag = "+".join(f"{os.path.basename(s.path).removesuffix('.safetensors')}@{s.strength}" for s in lora_specs)
+ base = f"{base}.lora-{tag}"
+ if quant_tag:
+ base = f"{base}.q-{quant_tag}"
+ return base
+
+ def build(
+ self,
+ *,
+ num_attention_heads: int,
+ attention_head_dim: int,
+ in_channels: int,
+ out_channels: int,
+ num_layers: int,
+ cross_attention_dim: int,
+ mesh_device: ttnn.MeshDevice,
+ ccl_manager: CCLManager,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool,
+ has_audio: bool,
+ image_conditioning: bool,
+ quant_config: LtxQuantProfile | None = None,
+ lora_enabled: bool = False,
+ ) -> LTXTransformerModel:
+ """Construct an ``LTXTransformerModel`` for this checkpoint (weights NOT loaded).
+
+ Loading is deferred so the caller can manage the lifecycle (deallocate / reload).
+ ``quant_config`` bakes the preset dtypes into construction, so a cache miss loads
+ weights direct-to-quant and the cache write holds the quantized tensorbins.
+ """
+ return LTXTransformerModel(
+ num_attention_heads=num_attention_heads,
+ attention_head_dim=attention_head_dim,
+ in_channels=in_channels,
+ out_channels=out_channels,
+ num_layers=num_layers,
+ cross_attention_dim=cross_attention_dim,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=has_audio,
+ apply_gated_attention=self.has_gate,
+ cross_attention_adaln=self.cross_attention_adaln,
+ image_conditioning=image_conditioning,
+ quant_config=quant_config,
+ lora_enabled=lora_enabled,
+ )
+
+ def load(
+ self,
+ model: LTXTransformerModel,
+ *,
+ parallel_config: DiTParallelConfig,
+ mesh_shape: tuple[int, ...],
+ is_fsdp: bool,
+ lora_specs: list[LoraSpec],
+ quant_tag: str | None = None,
+ ) -> None:
+ """Load (or reload) weights for a previously-built transformer."""
+ cache_module.load_model(
+ model,
+ model_name=self.cache_name(lora_specs, quant_tag),
+ subfolder=model.weight_cache_subfolder(),
+ parallel_config=parallel_config,
+ mesh_shape=mesh_shape,
+ mesh_device=model.mesh_device,
+ is_fsdp=is_fsdp,
+ get_torch_state_dict=lambda: self.state_dict(lora_specs),
+ )
diff --git a/code/models/tt_dit/models/transformers/minimax_h3/attention_minimax_h3.py b/code/models/tt_dit/models/transformers/minimax_h3/attention_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..645a83e2438ae2aabe9f20b340f216ab9b71c3d2
--- /dev/null
+++ b/code/models/tt_dit/models/transformers/minimax_h3/attention_minimax_h3.py
@@ -0,0 +1,609 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import math
+
+import torch
+
+import ttnn
+from models.common.utility_functions import is_blackhole
+
+from ....layers.linear import ColParallelLinear
+from ....layers.module import Module
+from ....layers.normalization import DistributedRMSNorm
+from ....parallel.config import DiTParallelConfig
+from ....parallel.manager import CCLManager
+from ....utils.mochi import get_rot_transformation_mat
+from ....utils.substate import pop_substate, rename_substate
+from ....utils.tensor import bf16_tensor
+from .agmm_config import agmm_block_size
+
+
+def rope_channel_permutation(head_dim: int, rotary_dim: int) -> torch.Tensor:
+ """Reorder a head's channels from MiniMax-H3's half-split RoPE layout to the interleaved one.
+
+ The fused RoPE inside `dit_fused_distributed_rmsnorm` rotates by multiplying every 32-column tile
+ by one 32x32 matrix, which pairs *adjacent* channels: `out[2i] = -in[2i+1]`, `out[2i+1] = in[2i]`.
+ MiniMax-H3's reference instead pairs `i` with `i + rotary_dim/2`. The two are the same operation
+ under this permutation of the rotary channels -- `out[2i] = in[i]`, `out[2i+1] = in[i + rot/2]` --
+ with the `head_dim - rotary_dim` pass-through channels left where they are.
+
+ Applied identically to the Q and K projection output channels, the QK-norm affine weight and the
+ cos/sin tables. Attention sees Q and K only through `q . k`, which any *shared* permutation of the
+ channel axis leaves unchanged, and V and `to_out` are untouched -- so the relayout is numerically
+ neutral, not an approximation. Same trick as `transformer_ideogram4.rope_halfsplit_to_interleaved_perm`,
+ extended to a rotary_dim narrower than head_dim.
+ """
+ half = rotary_dim // 2
+ rotary = torch.stack([torch.arange(half), torch.arange(half) + half], dim=1).flatten()
+ return torch.cat([rotary, torch.arange(rotary_dim, head_dim)])
+
+
+def prepare_rope_tables(cos: torch.Tensor, sin: torch.Tensor, head_dim: int) -> tuple[torch.Tensor, torch.Tensor]:
+ """Turn the reference's `[.., rotary_dim]` cos/sin into what the fused RoPE consumes.
+
+ Permutes the rotary channels into the interleaved layout (see `rope_channel_permutation`) and pads
+ out to `head_dim` with cos=1 / sin=0. Those pad channels are the ones MiniMax-H3 passes through
+ unrotated: because the fused rotate only ever mixes channels *within* a 32-column tile, and the
+ pass-through channels occupy whole tiles of their own, `sin=0` there makes the rotate an exact
+ identity. That is what lets a partial-head-dim RoPE run on an op that has no notion of one.
+ """
+ rotary_dim = cos.shape[-1]
+ perm = rope_channel_permutation(rotary_dim, rotary_dim) # permute the rotary block only
+ cos, sin = cos[..., perm], sin[..., perm]
+ pad = head_dim - rotary_dim
+ if pad:
+ ones = torch.ones(*cos.shape[:-1], pad, dtype=cos.dtype)
+ zeros = torch.zeros(*sin.shape[:-1], pad, dtype=sin.dtype)
+ cos, sin = torch.cat([cos, ones], dim=-1), torch.cat([sin, zeros], dim=-1)
+ return cos, sin
+
+
+class MiniMaxH3Attention(Module):
+ """Full self-attention over one packed sequence. MiniMax-H3 has no cross-attention.
+
+ Two things differ from `WanAttention` and drive the shape of this module:
+
+ * The attention inner dim (`num_heads * head_dim` = 7168) is *larger* than the residual stream
+ (`hidden_size` = 5376), so `to_q/k/v` widen 5376 -> 7168 and `to_out` narrows 7168 -> 5376.
+ Nothing here may assume `inner_dim == hidden_size`. Every projection is bias-free.
+ * The query/key norms are RMSNorms over `head_dim` (128), not over the TP-sharded residual
+ stream. They use `DistributedRMSNorm` in `per_head_norm` mode, which reduces over each head's
+ head_dim *locally* -- no all-gather, since a head's channels all live on one device -- and in
+ the same op splits the heads and applies RoPE. So one fused op replaces the norm, the head
+ split and the whole rotary sequence. See `rope_channel_permutation` for how MiniMax-H3's
+ partial, half-split rotary is made to fit an op that implements a full-width interleaved one.
+ """
+
+ # Per-device sequence length -> measured-best ring SDPA (q_chunk_size, k_chunk_size).
+ # See `_sdpa_program_config` for how these were obtained and why the optimum moves with length.
+ # 4768 / 9216 / 13632 are 768P at 5s / 10s / 15s, packed and padded, divided by SP=8.
+ measured_sdpa_chunk_sizes = {
+ 4768: (320, 384),
+ 9216: (256, 512),
+ 13632: (256, 512),
+ }
+
+ def __init__(
+ self,
+ *,
+ hidden_size: int,
+ num_heads: int,
+ head_dim: int,
+ rotary_dim: int | None = None,
+ qk_norm_eps: float = 1e-5,
+ mesh_device: ttnn.MeshDevice,
+ ccl_manager: CCLManager,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool = False,
+ is_sequence_parallel: bool = True,
+ ) -> None:
+ super().__init__()
+
+ # is_sequence_parallel=False means the sequence is *replicated* on the SP axis rather than
+ # fractured across it, so attention runs locally with plain SDPA and no ring all-gather. The
+ # token refiner uses that: its text stream is short and every SP device holds all of it.
+ self.is_sequence_parallel = is_sequence_parallel
+
+ self.hidden_size = hidden_size
+ self.num_heads = num_heads
+ self.head_dim = head_dim
+ self.inner_dim = num_heads * head_dim
+ # Channels [rotary_dim, head_dim) pass through the rotary embedding unrotated. None means
+ # the whole head rotates, which is what the token refiner (no RoPE at all) leaves unused.
+ self.rotary_dim = head_dim if rotary_dim is None else rotary_dim
+ self.qk_norm_eps = qk_norm_eps
+
+ self.mesh_device = mesh_device
+ self.ccl_manager = ccl_manager
+ self.parallel_config = parallel_config
+
+ tp_factor = parallel_config.tensor_parallel.factor
+ assert num_heads % tp_factor == 0, f"{num_heads} heads must divide across TP={tp_factor}"
+ self.n_local_heads = num_heads // tp_factor
+ self.tp_mesh_axis = parallel_config.tensor_parallel.mesh_axis
+ self.sp_mesh_axis = parallel_config.sequence_parallel.mesh_axis
+ # Fractured sequence means attention has to gather K/V around the ring.
+ self.use_ring = is_sequence_parallel and parallel_config.sequence_parallel.factor > 1
+
+ fsdp_mesh_axis = self.sp_mesh_axis if is_fsdp else None
+
+ # Fused QKV: one matmul, output split into three. The state dict is rearranged in
+ # `_prepare_torch_state` so that column-parallel fracturing hands each device the same
+ # 14 heads of q, k and v.
+ self.to_qkv = ColParallelLinear(
+ hidden_size,
+ 3 * self.inner_dim,
+ chunks=3,
+ bias=False,
+ mesh_device=mesh_device,
+ mesh_axis=self.tp_mesh_axis,
+ fsdp_mesh_axis=fsdp_mesh_axis,
+ ccl_manager=ccl_manager,
+ )
+ self.to_out = ColParallelLinear(
+ self.inner_dim,
+ hidden_size,
+ bias=False,
+ mesh_device=mesh_device,
+ mesh_axis=self.tp_mesh_axis,
+ fsdp_mesh_axis=fsdp_mesh_axis,
+ ccl_manager=ccl_manager,
+ )
+
+ # QK-norm + head split + RoPE in one fused op. embedding_dim is the *inner* dim so the
+ # per-device weight slice covers n_local_heads * head_dim; `per_head_norm=True` at the call
+ # site makes the reduction per head and device-local.
+ qk_norm_kwargs = dict(
+ embedding_dim=self.inner_dim,
+ norm_eps=qk_norm_eps,
+ norm_elementwise_affine=True,
+ mesh_axis=self.tp_mesh_axis,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ )
+ self.norm_q = DistributedRMSNorm(**qk_norm_kwargs)
+ self.norm_k = DistributedRMSNorm(**qk_norm_kwargs)
+ self.rope_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+
+ # all_gather_minimal_matmul_async folds the TP all-gather into the matmul. Ring only: on a
+ # line topology WanAttention measured the unfused path faster, so match that condition.
+ self.use_fused_agmm = ccl_manager.topology == ttnn.Topology.Ring and tp_factor > 1
+
+ # Ring SDPA reuses the joint-attention entry point with empty joint inputs, as WanAttention does.
+ self.dummy_joint_input = bf16_tensor(torch.zeros((1, self.n_local_heads, 0, head_dim)), device=mesh_device)
+
+ full_grid = mesh_device.compute_with_storage_grid_size()
+ self.full_grid = full_grid
+ self.sdpa_worker_grid = (full_grid.x - 1, full_grid.y) # reserve last column for CCL
+ self._sdpa_program_configs: dict[tuple[int, bool], ttnn.SDPAProgramConfig] = {}
+
+ # The exp ring op walks head-SEGMENTS (a head's Q chunks split over segs_per_head rows) as
+ # serial passes, ceil(n_local_heads * segs / rows) passes per row. Segmentation is what
+ # balances 14 local heads over 10 rows: segs=1 gives 2 passes of 10-tile chunks with 6 rows
+ # idle on the second pass, while segs=2 gives 3 passes of 5-tile chunks on every core --
+ # 15 Q tile-rows per core instead of 20 on the bottleneck cores.
+ self.exp_ring_max_passes = 3 # kMaxPasses in exp_ring_joint_sdpa_program_factory.cpp
+ self.exp_ring_num_passes = math.ceil(self.n_local_heads / full_grid.y)
+ self.exp_ring_max_k_chunk = 512 # largest k worth trying; `_exp_sdpa_l1_bytes` picks down from here
+ self.use_exp_ring_sdpa = (
+ self.use_ring
+ and is_blackhole()
+ and tp_factor == 4
+ and parallel_config.sequence_parallel.factor == 32
+ and self.exp_ring_num_passes <= self.exp_ring_max_passes
+ )
+ self._exp_sdpa_program_configs: dict[int, ttnn.SDPAProgramConfig | None] = {}
+
+ self.sdpa_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi2,
+ math_approx_mode=False,
+ fp32_dest_acc_en=False,
+ )
+ self.mm_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi2,
+ math_approx_mode=True,
+ fp32_dest_acc_en=True,
+ packer_l1_acc=True,
+ )
+
+ # ------------------------------------------------------------------ weights
+
+ def _prepare_torch_state(self, state: dict[str, torch.Tensor]) -> None:
+ rename_substate(state, "to_out.0", "to_out")
+
+ def _interleave_heads(tensors: list[torch.Tensor]) -> torch.Tensor:
+ """Reorder [out, in] weights so TP column-fracturing gives each device matching heads.
+
+ Out dim is `num_heads * head_dim`. Reshaping it to [n_dev, n_local_heads, head_dim] and
+ concatenating the tensors on the heads axis puts device `d`'s q, k and v heads
+ contiguously inside shard `d`, which is also the order `chunks=3` splits them back out in.
+ Device `d` therefore owns canonical heads `[d * n_local, (d + 1) * n_local)`, so simply
+ all-gathering the attention output on TP rebuilds the canonical 7168-channel order that
+ `to_out` expects.
+ """
+ n_dev = self.parallel_config.tensor_parallel.factor
+ tensors = [t.T for t in tensors] # -> [in, out]
+ tensors = [t.reshape(t.shape[0], n_dev, self.n_local_heads, self.head_dim) for t in tensors]
+ merged = torch.cat(tensors, dim=2)
+ merged = merged.reshape(merged.shape[0], len(tensors) * self.inner_dim)
+ return merged.T
+
+ q_state = pop_substate(state, "to_q")
+ k_state = pop_substate(state, "to_k")
+ v_state = pop_substate(state, "to_v")
+
+ # Relayout Q and K into the interleaved rotary channel order the fused RoPE consumes. Shared
+ # between Q and K and absent from V, so Q.K is unchanged (see `rope_channel_permutation`).
+ perm = rope_channel_permutation(self.head_dim, self.rotary_dim)
+
+ def _permute_rotary(weight: torch.Tensor) -> torch.Tensor:
+ # [out, in] with out == num_heads * head_dim; permute within each head.
+ return weight.reshape(self.num_heads, self.head_dim, -1)[:, perm].reshape(weight.shape)
+
+ state["to_qkv.weight"] = _interleave_heads(
+ [_permute_rotary(q_state["weight"]), _permute_rotary(k_state["weight"]), v_state["weight"]]
+ )
+
+ # The reference's QK-norm affine is one head_dim vector shared by every head. The fused op
+ # wants it spanning the whole inner dim, so permute it the same way and repeat per head.
+ for name in ("norm_q", "norm_k"):
+ sub = pop_substate(state, name)
+ if "weight" in sub:
+ state[f"{name}.weight"] = sub["weight"][perm].repeat(self.num_heads)
+
+ # ------------------------------------------------------------------ helpers
+
+ def _sdpa_program_config(self, seq_local: int, *, ring: bool) -> ttnn.SDPAProgramConfig:
+ """Ring SDPA chunk sizes for a given per-device sequence length.
+
+ Measured points come from the sweep in
+ `tests/nightly/blackhole/sdpa/test_ring_joint_sdpa.py::test_ring_joint_attention_create_perf_table`
+ (model configs `minimax_h3_{5s,10s,15s}_768p`) on 4x8 Blackhole Galaxy, TP=4 / SP=8. Anything
+ else falls back to the generic rule below.
+
+ The optimum depends on the *local* sequence length, which is why this is keyed on it rather
+ than on the mesh shape the way `WanAttention.sdpa_chunk_size_map` is. At long sequences there
+ is enough work that (256, 512) wins -- a larger k halves the ring's K-loop iterations. At 5s
+ there is too little work to fill 110 cores that way: 14 heads x ceil(4768/256) = 266 work items
+ over 110 cores rounds up to 3 per core and wastes ~19% of the slots, whereas q=320 gives 210
+ items, 2 per core and ~4.5% waste. That is worth more than the larger k.
+
+ L1 (1.57 MB) bounds the (q, k) product, and the bound is what makes k=384 interesting: k=1024
+ never fits, (320, 512) reaches 1.65 MB and does not either, but (320, 384) does -- combining
+ the good q with a k larger than 256. Neither chunk size has to be a power of two, only a
+ multiple of TILE, and restricting the search to {256, 512} misses this point entirely.
+
+ Padding is not the thing to tune. 4768 is 32 x 149 with 149 prime, so no sensible chunk size
+ divides it, yet at q=320 the Q padding is only 0.67%. Core slot efficiency dominates, and the
+ optimum in it is sharp rather than a plateau: at 5s q=288 measured 10.43 ms against q=320's
+ 7.81 ms. Slot efficiency is a good candidate generator but not a predictor -- q=416 at 10s has
+ the best slot efficiency of any k=256 point there (97.6%) and measured the worst (28.39 ms).
+ """
+ key = (seq_local, ring)
+ if key not in self._sdpa_program_configs:
+ tile = ttnn.TILE_SIZE
+ measured = self.measured_sdpa_chunk_sizes.get(seq_local)
+ if measured is not None:
+ q_chunk, k_chunk = measured
+ else:
+ q_chunk = max(tile, min(256, (seq_local // tile) * tile))
+ k_chunk = max(tile, min(512, (seq_local // tile) * tile))
+ grid = (
+ ttnn.CoreCoord(*self.sdpa_worker_grid) if ring else ttnn.CoreCoord(self.full_grid.x, self.full_grid.y)
+ )
+ self._sdpa_program_configs[key] = ttnn.SDPAProgramConfig(
+ compute_with_storage_grid_size=grid,
+ q_chunk_size=q_chunk,
+ k_chunk_size=k_chunk,
+ exp_approx_mode=False, # NOTE: False is more correct
+ )
+ return self._sdpa_program_configs[key]
+
+ # One accumulator entry and one Q chunk per pass, in tiles of `_EXP_L1_TILE_BYTES`. Mirrors the
+ # CB table in exp_ring_joint_sdpa_program_factory.cpp; reproduces its measured 1,302,528 B at
+ # (224, 512) exactly. Nothing in the op validates this, so an oversized shape would only surface
+ # as a CB allocation failure at program build.
+ _EXP_L1_TILE_BYTES = 2048 # bf16 and Float16_b tiles are both 2 KiB
+ # CB space measured IN THE PIPELINE, not bare L1: the op's CBs must end below the lowest live
+ # L1 buffer (global semaphores etc. occupy the top of L1), which a 15s run measured at
+ # 1,504,000 with the CB region starting at 191,360. The factory checks the live value at build;
+ # this constant only has to be a safe lower bound so the k search picks a buildable shape.
+ _EXP_USABLE_L1_BYTES = 1_312_640
+ # DEST tiles from `get_dest_reg_count`: 1024 * 16 / (32 * 32), halved because dst_full_sync_en is
+ # off, not halved again because `sdpa_compute_kernel_config` has fp32_dest_acc_en off.
+ _EXP_DST_TILES = 8
+ # `determine_largest_subblock_size` in sdpa_subblock_utils.hpp, in its search order.
+ _EXP_SUBBLOCKS = (
+ (2, 4), (4, 2), (1, 8), (8, 1), (1, 7), (7, 1), (2, 3), (3, 2), (1, 6), (6, 1),
+ (1, 5), (5, 1), (2, 2), (1, 4), (4, 1), (1, 3), (3, 1), (1, 2), (2, 1), (1, 1),
+ ) # fmt: skip
+
+ def _exp_streaming_compute_enabled(self, sq_t: int, sk_t: int) -> bool:
+ """Whether the op picks its streaming compute path for a chunk shape.
+
+ Mirrors `use_streaming_compute` in exp_ring_joint_sdpa_program_factory.cpp. The exp compute
+ kernel `static_assert`s on it, so a shape the factory judges ineligible does not fall back --
+ it fails to build the kernel. The binding term in practice is `sk_t % (dst / h) == 0`: it
+ rejects k=320 at q=320 (h=1, so sk_t must be a multiple of 8, and 10 is not).
+ """
+ dst = self._EXP_DST_TILES
+ for h, w in self._EXP_SUBBLOCKS:
+ if h * w <= dst and sq_t % h == 0 and sk_t % w == 0:
+ return h <= 2 and sk_t % (dst // h) == 0 and sq_t // h > 1
+ return False
+
+ def _exp_sdpa_l1_bytes(self, sq_t: int, sk_t: int, p: int, resident_q: bool = True) -> int:
+ """L1 the exp op's circular buffers need for a (q, k, passes) shape, in tiles of head_dim.
+
+ `p` is the candidate's pass count (head-segment scheduling makes it per-candidate:
+ ceil(n_local_heads * segs / rows)). `resident_q=False` models the op's streamed-Q fallback:
+ when the resident total does not fit, the factory sizes c_0 to a single chunk and the reader
+ re-reads each pass's Q every ring iteration. The op selects the mode itself from this same
+ arithmetic; the model only needs it to know which (q, k) shapes are buildable.
+ """
+ dh_t = self.head_dim // ttnn.TILE_SIZE
+ tiles = (
+ (p if resident_q else 1) * sq_t * dh_t # c_0 Q: one chunk per pass, or one when streamed
+ + 4 * sk_t * dh_t # c_1/c_14 K and c_2/c_15 V, double buffered
+ + 7 # c_3 mask, scalars, reciprocal scratch
+ + 2 * p * sq_t # c_6 / c_11 state FIFO running max and sum
+ + p * sq_t * dh_t # c_7 state FIFO partial output
+ + sq_t # c_17 stats out (c_10 is dead on this path and not allocated)
+ + 16 # c_16 streaming output ping-pong
+ + sq_t * sk_t # c_24 qk intermediate
+ + 2 * sq_t * dh_t # c_25/c_26 output scratch halves
+ + 4 * sq_t # c_27-c_30 max/sum scratch halves
+ + sq_t # c_31 exp max diff
+ )
+ return tiles * self._EXP_L1_TILE_BYTES
+
+ def _exp_sdpa_program_config(self, seq_local: int) -> ttnn.SDPAProgramConfig | None:
+ """Exp ring SDPA config for a per-device sequence length, or None if it cannot use the op.
+
+ The op gives Q chunk `x` to core column `x`, so a head's chunks must fill its row exactly:
+ `ceil(seq_local / q_chunk)` has to equal the SDPA column count. That pins q_chunk to the
+ window `[seq_local / cols, seq_local / (cols - 1))` for a given width, and only a TILE
+ multiple will do, so a width is usable only if one lands there. `measured_sdpa_chunk_sizes`
+ therefore does not apply on this path -- though at the H3 10s shape the window happens to
+ give q=224, the value WanAttention measured anyway.
+
+ Widest usable grid wins, so try `full_grid.x - 1` columns first and step down. A narrower
+ grid frees no L1 whatsoever -- every CB is sized from (q_chunk, k_chunk, passes), never from
+ the column count -- so it is never worth taking while a wider one fits. It is only worth
+ taking when no wider width admits a q_chunk at all, and there the comparison is not against
+ a full-grid exp config but against no exp config at all: at 5s, 11 columns admit nothing
+ (96 -> 13 chunks, 128 -> 10) while 10 columns take q=128 with L1 to spare.
+
+ k_chunk is the one genuinely free variable, and the only one that buys L1 headroom, so take
+ the largest that both fits L1 and keeps the op on its streaming compute path -- the kernel
+ static_asserts on the latter, so an ineligible k fails the build rather than falling back.
+ A shape fits if either Q mode does: resident Q (all passes' chunks stay in L1, read once) or
+ the op's streamed-Q fallback (one chunk resident, re-read per pass per ring iteration).
+ That gives 512 at q=224 resident, and 384 at q=320 streamed -- where the k=256 that resident
+ Q would force measured far slower (small k doubles the per-chunk flash overhead; see
+ exp_more_heads_per_row.md §9).
+ """
+ if not self.use_exp_ring_sdpa:
+ return None
+ if seq_local not in self._exp_sdpa_program_configs:
+ self._exp_sdpa_program_configs[seq_local] = self._build_exp_sdpa_program_config(seq_local)
+ return self._exp_sdpa_program_configs[seq_local]
+
+ def _build_exp_sdpa_program_config(self, seq_local: int) -> ttnn.SDPAProgramConfig | None:
+ """Search (cols, segs_per_head, q_chunk, k_chunk) and take the lightest bottleneck load.
+
+ The per-core matmul work per ring iteration is passes * q_chunk Q rows against the full
+ K/V stream, so `passes * q_chunk` is the primary score: at 14 heads on 10 rows, segs=1
+ gives 2 passes x 320 = 640 rows while segs=2 gives 3 passes x 160 = 480. Larger k_chunk
+ is the tie-break (fewer per-chunk overheads), then wider grids.
+ """
+ tile = ttnn.TILE_SIZE
+ rows = self.full_grid.y
+ best = None
+ for cols in range(self.full_grid.x - 1, 1, -1):
+ for segs in (1, 2, 3):
+ chunks = cols * segs
+ q_chunk = math.ceil(math.ceil(seq_local / chunks) / tile) * tile
+ if math.ceil(seq_local / q_chunk) != chunks:
+ continue # this (cols, segs) admits no tile-multiple q_chunk
+ passes = math.ceil(self.n_local_heads * segs / rows)
+ if passes > self.exp_ring_max_passes:
+ continue
+ for k_chunk in range(self.exp_ring_max_k_chunk, 0, -tile):
+ sq_t, sk_t = q_chunk // tile, k_chunk // tile
+ fits = (
+ self._exp_sdpa_l1_bytes(sq_t, sk_t, passes) <= self._EXP_USABLE_L1_BYTES
+ or self._exp_sdpa_l1_bytes(sq_t, sk_t, passes, resident_q=False) <= self._EXP_USABLE_L1_BYTES
+ )
+ if fits and self._exp_streaming_compute_enabled(sq_t, sk_t):
+ score = (passes * q_chunk, -k_chunk, -cols)
+ if best is None or score < best[0]:
+ best = (score, cols, q_chunk, k_chunk)
+ break
+ if best is None:
+ return None
+ _, cols, q_chunk, k_chunk = best
+ return ttnn.SDPAProgramConfig(
+ compute_with_storage_grid_size=ttnn.CoreCoord(cols + 1, self.full_grid.y),
+ q_chunk_size=q_chunk,
+ k_chunk_size=k_chunk,
+ exp_approx_mode=False, # NOTE: False is more correct
+ )
+
+ # ------------------------------------------------------------------ forward
+
+ def forward(
+ self,
+ spatial_1BND: ttnn.Tensor,
+ N: int | None = None,
+ rope_cos: ttnn.Tensor | None = None,
+ rope_sin: ttnn.Tensor | None = None,
+ addcmul_residual: ttnn.Tensor | None = None,
+ addcmul_gate: ttnn.Tensor | None = None,
+ ) -> ttnn.Tensor:
+ """
+ spatial_1BND: fractured hidden_size on TP; fractured N on SP when `is_sequence_parallel`,
+ otherwise replicated on SP.
+ rope_cos/rope_sin: [1, 1, N_local, rotary_dim], fractured N on SP, replicated on TP. Both
+ None skips the rotary embedding entirely, as the token refiner requires.
+ N: logical (unfractured) sequence length. Only needed for ring attention.
+ addcmul_residual/addcmul_gate: when both are given, the gated residual
+ `addcmul_residual + to_out(...) * addcmul_gate` is folded into the to_out matmul's
+ epilogue instead of running as separate ops. Both must be TP-fractured like the output.
+
+ Returns the attention output with the same distribution as the input.
+ """
+ assert (addcmul_residual is None) == (addcmul_gate is None), "addcmul residual/gate come as a pair"
+ assert (rope_cos is None) == (rope_sin is None), "rope_cos and rope_sin must be given together"
+ # The fused RoPE consumes head_dim-wide tables, not the reference's rotary_dim-wide ones: the
+ # pass-through channels must be present as cos=1 / sin=0. Passing the raw reference tables
+ # here is silently wrong rather than a shape error, so check it. See `prepare_rope_tables`.
+ if rope_cos is not None and rope_cos.shape[-1] != self.head_dim:
+ msg = (
+ f"rope tables must be head_dim ({self.head_dim}) wide, got {rope_cos.shape[-1]}; "
+ "build them with prepare_rope_tables()"
+ )
+ raise ValueError(msg)
+
+ tp_factor = self.parallel_config.tensor_parallel.factor
+ assert not (self.use_ring and N is None), "ring attention needs the logical sequence length N"
+
+ # Passing parallel_config puts ColParallelLinear on all_gather_minimal_matmul_async: the TP
+ # all-gather of the K-fractured input folds into the matmul that consumes it, instead of
+ # running as a separate op. Only on ring topologies -- on a line the unfused path is faster,
+ # which is the same condition WanAttention uses.
+ matmul_parallel_config = self.parallel_config if self.use_fused_agmm else None
+ if not self.use_fused_agmm and tp_factor > 1:
+ spatial_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_1BND, dim=3, mesh_axis=self.tp_mesh_axis
+ )
+
+ q_1BNF, k_1BNF, v_1BNF = self.to_qkv(
+ spatial_1BND,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=matmul_parallel_config,
+ default_block_size=agmm_block_size(self.hidden_size, 3 * self.inner_dim // tp_factor),
+ )
+
+ def create_heads(inp: ttnn.Tensor) -> ttnn.Tensor:
+ out, _, _ = ttnn.experimental.nlp_create_qkv_heads(
+ inp,
+ num_heads=self.n_local_heads,
+ num_kv_heads=0,
+ transpose_k_heads=False,
+ )
+ return out
+
+ # One fused op per stream: per-head RMSNorm over head_dim, head split, and RoPE. It emits
+ # head-split [B, n_local_heads, N, head_dim] directly, so Q and K need no create_heads.
+ norm_kwargs = dict(
+ num_heads_per_device=self.n_local_heads,
+ per_head_norm=True,
+ rope_cos=rope_cos,
+ rope_sin=rope_sin,
+ trans_mat=self.rope_trans_mat if rope_cos is not None else None,
+ )
+ q_BHNE = self.norm_q(q_1BNF, **norm_kwargs)
+ k_BHNE = self.norm_k(k_1BNF, **norm_kwargs)
+ v_BHNE = create_heads(v_1BNF)
+
+ # Sequence is fractured across SP, so attention must gather K/V around the ring.
+ # The packed sequence is one attention document and logical_n masks the pad tail, so no mask.
+ exp_program_config = self._exp_sdpa_program_config(q_BHNE.shape[2])
+ if exp_program_config is not None:
+ spatial_BHNE, _prompt, _lse = ttnn.transformer.exp_ring_joint_scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ self.dummy_joint_input,
+ self.dummy_joint_input,
+ self.dummy_joint_input,
+ persistent_output_buffer_k=self.ccl_manager.get_ag_ping_pong_buffer(
+ k_BHNE.shape, 2, self.sp_mesh_axis, dtype=k_BHNE.dtype
+ ),
+ persistent_output_buffer_v=self.ccl_manager.get_ag_ping_pong_buffer(
+ v_BHNE.shape, 2, self.sp_mesh_axis, dtype=v_BHNE.dtype
+ ),
+ joint_strategy="rear",
+ logical_n=N,
+ program_config=exp_program_config,
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ dim=2,
+ multi_device_global_semaphore=self.ccl_manager.get_exp_ring_ping_pong_semaphore(self.sp_mesh_axis),
+ num_links=self.ccl_manager.num_links,
+ cluster_axis=self.sp_mesh_axis,
+ mesh_device=self.mesh_device,
+ topology=self.ccl_manager.topology,
+ subdevice_id=self.ccl_manager.ccl_sub_device_id,
+ num_workers_per_link=5,
+ num_buffers_per_channel=32,
+ )
+ elif self.use_ring:
+ spatial_BHNE, _prompt, _lse = ttnn.transformer.ring_joint_scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ self.dummy_joint_input,
+ self.dummy_joint_input,
+ self.dummy_joint_input,
+ persistent_output_buffer_k=self.ccl_manager.get_ag_ping_pong_buffer(
+ k_BHNE.shape, 2, self.sp_mesh_axis, dtype=k_BHNE.dtype
+ ),
+ persistent_output_buffer_v=self.ccl_manager.get_ag_ping_pong_buffer(
+ v_BHNE.shape, 2, self.sp_mesh_axis, dtype=v_BHNE.dtype
+ ),
+ joint_strategy="rear",
+ logical_n=N,
+ program_config=self._sdpa_program_config(q_BHNE.shape[2], ring=True),
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ dim=2,
+ multi_device_global_semaphore=self.ccl_manager.get_ag_ping_pong_semaphore(self.sp_mesh_axis),
+ num_links=self.ccl_manager.num_links,
+ cluster_axis=self.sp_mesh_axis,
+ mesh_device=self.mesh_device,
+ topology=self.ccl_manager.topology,
+ subdevice_id=self.ccl_manager.ccl_sub_device_id,
+ ccl_core_grid_offset=(self.sdpa_worker_grid[0], 0),
+ use_column_major_ccl=True,
+ )
+ else:
+ spatial_BHNE = ttnn.transformer.scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ is_causal=False,
+ program_config=self._sdpa_program_config(q_BHNE.shape[2], ring=False),
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ )
+
+ spatial_1BND = ttnn.transformer.concatenate_heads(spatial_BHNE)
+ spatial_1BND = ttnn.unsqueeze(spatial_1BND, 0)
+
+ # Each device holds canonical heads [d * n_local, (d+1) * n_local), so gathering on TP
+ # rebuilds the full inner_dim in canonical order for to_out -- fused into the matmul when
+ # use_fused_agmm.
+ if not self.use_fused_agmm and tp_factor > 1:
+ spatial_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_1BND, dim=3, mesh_axis=self.tp_mesh_axis
+ )
+
+ # The gated residual rides along in the matmul epilogue on the fused path; on the unfused
+ # path the op has no addcmul, so apply it afterwards.
+ fuse_gate = addcmul_residual is not None and self.use_fused_agmm
+ out = self.to_out(
+ spatial_1BND,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=matmul_parallel_config,
+ default_block_size=agmm_block_size(self.inner_dim, self.hidden_size // tp_factor),
+ addcmul_a=addcmul_residual if fuse_gate else None,
+ addcmul_b=addcmul_gate if fuse_gate else None,
+ )
+ if addcmul_residual is not None and not fuse_gate:
+ out = ttnn.addcmul(addcmul_residual, out, addcmul_gate)
+ return out
diff --git a/code/models/tt_dit/models/transformers/minimax_h3/mmrs_config.py b/code/models/tt_dit/models/transformers/minimax_h3/mmrs_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e18f1c4bfd06ef577982c43bdb84b8db23efc5f
--- /dev/null
+++ b/code/models/tt_dit/models/transformers/minimax_h3/mmrs_config.py
@@ -0,0 +1,105 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Fused matmul + reduce-scatter (+ addcmul) blockings for the MiniMax-H3 feed-forward ff2.
+
+`ff2` is row-parallel, so its reduce-scatter and the gated residual after it collapse into one
+`minimal_matmul_strided_reduce_scatter_async`. That fusion is only a win with a *swept* blocking:
+`get_fused_mmrs_config` falls back to `default_fused_mmrs_config`, which puts the matmul on an 8x7 =
+56-core grid with `M_block=2` and subblock 1x1 -- half the machine at the least efficient subblock,
+where the unfused path runs on 110 cores at subblock (2, 2). Measured, that fallback makes the fusion a
+**45% regression** on the stage (1.75 -> 2.55 ms).
+
+Unlike `agmm_config` these must be keyed on the full `(M, K, N)`, because that is what
+`get_fused_mmrs_config` looks up -- there is no `default_block_size` hook to key more loosely. M is the
+per-device packed sequence length, so it varies with the video duration; `has_mmrs_config` gates the
+fused path off entirely for shapes it cannot serve, and `register_mmrs_config` registers the blocking
+for whichever M the fused path is about to run at.
+
+`compute_with_storage_grid_size` is the *matmul* grid, and the reduce-scatter workers occupy the rows
+between it and the full device grid:
+
+ rs_zone_capacity = (device_grid.y - mm_grid.y) * device_grid.x
+ num_workers_per_link = rs_zone_capacity // (2 * num_links) - 1
+
+so the matmul grid and the reduce-scatter's bandwidth trade against each other directly, and that
+trade is the dominant axis. Swept on 4x8 Blackhole Galaxy at 2 links with
+`models/tt_dit/utils/sweep_mm_block_sizes.py` (`mmrs` use case), first across the three candidate
+grids at M=4768:
+
+ 12x7 84 mm cores, 8 RS workers/link M=6 K=4 N=16 sb(2,2) 1.373 ms
+ 12x8 96 mm cores, 5 RS workers/link M=4 K=8 N=14 sb(2,2) 1.313 ms <- best, -25.0%
+ 12x9 108 mm cores, 2 RS workers/link M=6 K=2 N=8 sb(2,2) 1.487 ms
+
+The optimum is interior: 12x9 starves the reduce-scatter and 12x7 starves the matmul. Ms without a
+swept entry are NOT registered here: they fall through to the v2.3 rule engine inside
+`get_fused_mmrs_config` (`utils/mmrs_rules.py`), which picks the 12x8 blocking per M. Measured
+across the 41 swept H3 Ms, the rules average +1.9% over each M's swept optimum (worst +5.3%),
+versus +6.6% (worst +26.7%, at small M) for the earlier policy of reusing the M=3424 blocking
+at every duration.
+"""
+
+from __future__ import annotations
+
+import ttnn
+
+from ....utils.matmul import FusedMMRSConfig, register_fused_mmrs_configs
+
+# ff2 per-device K = 14336 / tp 4 = 3584; N = 5376 is the full hidden size (the reduce-scatter
+# fractures it back to 1344 per device on the way out).
+_K = 3584
+_N = 5376
+
+# Per-M entries for the Ms that have been swept at their own shape; any other M is left
+# unregistered so `get_fused_mmrs_config` resolves it with the v2.3 rules instead. Earlier
+# revisions of this file keyed everything off M=4768 ("5s@768P"), then reused the nearest
+# swept blocking for every duration; both traded up to 27% on Ms far from the swept one.
+#
+# M=3424 (the current run's per-device ff2 M) swept 2026-08-25 under the windowed L1 handoff,
+# across all three candidate grids -- 12x8 still wins: 755.6 us vs 804.2 at 12x7 and 856.8 at
+# 12x9. Mt_per_core = 107/12 -> 9, so M_block=8 leaves 2 blocks and the window rotates.
+_SWEPT_BLOCKINGS = {
+ 3424: FusedMMRSConfig(ttnn.CoreCoord(12, 8), 8, 2, 8, 2, 2, None, 1),
+}
+
+_DEVICE_GRID = ttnn.CoreCoord(12, 10)
+
+_TILE = 32
+# The swept blocking's K divisibility holds for the fixed K: K_block 2 divides K/32 = 112. The
+# rule engine guarantees the same for its own picks (K_block is chosen among divisors of K/32).
+
+
+def has_mmrs_config(m: int, k: int, n: int) -> bool:
+ """Whether the fused MM+RS+addcmul path should be taken for this shape. Pure query, no side effects.
+
+ Gate the fused path on this, and call `register_mmrs_config` before running the fused op. A shape
+ this rejects would resolve neither a table entry nor a rule pick and land on
+ `default_fused_mmrs_config`, whose 56-core matmul grid at subblock 1x1 makes the fused op far
+ *slower* than not fusing -- measured as a 45% regression on this stage.
+
+ Rather than a whitelist of the shipped durations, any tile-aligned M is accepted, so an
+ arbitrary duration gets the fused path: a swept M resolves to its table entry, and every other
+ M falls through to the v2.3 rules inside `get_fused_mmrs_config` (blind-validated within 5% of
+ the swept optimum on 14/14 fresh H3 Ms). `M_block` does *not* have to divide the M tile count --
+ a partial trailing block along M is fine, unlike along K, where the ring delivers the gathered
+ input in fixed chunks; requiring divisibility here would silently disable the fused path for
+ most durations.
+ """
+ return k == _K and n == _N and m % _TILE == 0
+
+
+def register_mmrs_config(m: int, k: int, n: int) -> None:
+ """Register the swept blocking for this shape into the global fused-MMRS table, if one exists.
+
+ Call at the site that is about to take the fused path (idempotent, cheap), for a shape
+ `has_mmrs_config` accepts; registration is explicit so that merely *querying* a shape never
+ mutates the global table. An M without a swept entry is deliberately NOT registered:
+ `get_fused_mmrs_config` then resolves it with the v2.3 rule engine, which beats reusing a
+ neighboring swept blocking (see the module docstring for the measured comparison).
+ """
+ if not has_mmrs_config(m, k, n):
+ msg = f"No fused MMRS blocking for (M, K, N) = ({m}, {k}, {n}); gate on has_mmrs_config first"
+ raise ValueError(msg)
+ if m in _SWEPT_BLOCKINGS:
+ register_fused_mmrs_configs({_DEVICE_GRID: {(m, _K, _N): _SWEPT_BLOCKINGS[m]}})
diff --git a/code/models/tt_dit/models/transformers/minimax_h3/transformer_block_minimax_h3.py b/code/models/tt_dit/models/transformers/minimax_h3/transformer_block_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..1dbb1fb0d4fcb6bcec5456bc0d2c136dac263bcb
--- /dev/null
+++ b/code/models/tt_dit/models/transformers/minimax_h3/transformer_block_minimax_h3.py
@@ -0,0 +1,339 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import torch
+
+import ttnn
+
+from ....layers.feedforward import ParallelFeedForward
+from ....layers.linear import ColParallelLinear
+from ....layers.module import Module
+from ....layers.normalization import DistributedRMSNorm
+from ....parallel.config import DiTParallelConfig
+from ....parallel.manager import CCLManager
+from ....utils.substate import rename_substate
+from .agmm_config import agmm_block_size
+from .attention_minimax_h3 import MiniMaxH3Attention
+from .mmrs_config import has_mmrs_config, register_mmrs_config
+
+# Number of modalities the AdaLN table is indexed by: video (0), text (1), audio (2).
+# Mirrors `MINIMAX_H3_MODALITY_NUM` in the reference; padding rows (-1) are clamped to 0.
+MODALITY_NUM = 3
+
+# Modulation parameters per block, in the reference's chunk order.
+NUM_MODULATION_PARAMS = 6
+(
+ _SHIFT_MSA,
+ _SCALE_MSA,
+ _GATE_MSA,
+ _SHIFT_MLP,
+ _SCALE_MLP,
+ _GATE_MLP,
+) = range(NUM_MODULATION_PARAMS)
+
+
+class MiniMaxH3TransformerBlock(Module):
+ """One MiniMax-H3 block: pre-norm self-attention and SwiGLU feed-forward, each modulated by
+ AdaLN parameters selected *per row* of the packed sequence.
+
+ The per-row modulation is what sets this apart from Wan. A single projection turns the shared
+ timestep embedding into a `(num_timesteps * MODALITY_NUM, hidden_size)` table for each of the six
+ modulation parameters, and every row of the packed sequence indexes that table by
+ `timestep_indices * MODALITY_NUM + token_tags`. One forward therefore serves rows at different
+ noise levels and of different modalities. Wan, by contrast, broadcasts one modulation over the
+ whole sequence, so it needs no gather at all.
+
+ The projection is shared by `norm1` and `norm2` and by all three modalities, so -- unlike
+ `AdaLayerNormZero` -- it cannot be folded into either norm and lives as a block-level module,
+ named `adaln_proj` after the checkpoint.
+ """
+
+ def __init__(
+ self,
+ *,
+ hidden_size: int,
+ num_heads: int,
+ head_dim: int,
+ rotary_dim: int | None = None,
+ ffn_dim: int,
+ time_embed_dim: int,
+ norm_eps: float = 1e-5,
+ qk_norm_eps: float = 1e-5,
+ mesh_device: ttnn.MeshDevice,
+ ccl_manager: CCLManager,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool = False,
+ precomputed_adaln: bool = False,
+ ) -> None:
+ super().__init__()
+
+ self.hidden_size = hidden_size
+ self.ffn_dim = ffn_dim
+ self.time_embed_dim = time_embed_dim
+
+ self.mesh_device = mesh_device
+ self.ccl_manager = ccl_manager
+ self.parallel_config = parallel_config
+
+ self.tp_mesh_axis = parallel_config.tensor_parallel.mesh_axis
+ self.tp_factor = parallel_config.tensor_parallel.factor
+ assert hidden_size % self.tp_factor == 0
+ self.hidden_local = hidden_size // self.tp_factor
+
+ fsdp_mesh_axis = parallel_config.sequence_parallel.mesh_axis if is_fsdp else None
+
+ self.norm1 = DistributedRMSNorm(
+ embedding_dim=hidden_size,
+ norm_eps=norm_eps,
+ norm_elementwise_affine=True,
+ mesh_axis=self.tp_mesh_axis,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ )
+ self.attn = MiniMaxH3Attention(
+ hidden_size=hidden_size,
+ num_heads=num_heads,
+ head_dim=head_dim,
+ rotary_dim=rotary_dim,
+ qk_norm_eps=qk_norm_eps,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ )
+ self.norm2 = DistributedRMSNorm(
+ embedding_dim=hidden_size,
+ norm_eps=norm_eps,
+ norm_elementwise_affine=True,
+ mesh_axis=self.tp_mesh_axis,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ )
+ # The reference's SwiGLU is `up, gate = proj(x).chunk(2); up * silu(gate)`, which is the
+ # `gate_is_first=False` layout `ColParallelLinear` already assumes for activation_fn="swiglu".
+ self.ff = ParallelFeedForward(
+ hidden_size,
+ inner_dim=ffn_dim,
+ activation_fn="swiglu",
+ bias=False,
+ mesh_device=mesh_device,
+ mesh_axis=self.tp_mesh_axis,
+ fsdp_mesh_axis=fsdp_mesh_axis,
+ ccl_manager=ccl_manager,
+ )
+ # With precomputed modulation the projection never exists on device: the caller passes the
+ # six tables into `forward` instead, and this block's `adaln_proj.*` checkpoint keys are
+ # dropped in `_prepare_torch_state`. See `adaln_cache_minimax_h3`.
+ self.precomputed_adaln = precomputed_adaln
+ self.adaln_proj = (
+ None
+ if precomputed_adaln
+ else ColParallelLinear(
+ time_embed_dim,
+ NUM_MODULATION_PARAMS * hidden_size * MODALITY_NUM,
+ bias=True,
+ mesh_device=mesh_device,
+ mesh_axis=self.tp_mesh_axis,
+ fsdp_mesh_axis=fsdp_mesh_axis,
+ ccl_manager=ccl_manager,
+ )
+ )
+
+ self.mm_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi2,
+ math_approx_mode=True,
+ fp32_dest_acc_en=True,
+ packer_l1_acc=True,
+ )
+ self.use_fused_agmm = ccl_manager.topology == ttnn.Topology.Ring and self.tp_factor > 1
+ # ff1 packs gate and up together for the fused SwiGLU, so its per-device N is 2 * ffn_dim / tp.
+ self.ff1_block_size = agmm_block_size(hidden_size, 2 * ffn_dim // self.tp_factor)
+
+ # ------------------------------------------------------------------ weights
+
+ def _prepare_torch_state(self, state: dict[str, torch.Tensor]) -> None:
+ rename_substate(state, "ff.net.0.proj", "ff.ff1")
+ rename_substate(state, "ff.net.2", "ff.ff2")
+
+ # The reference lays the AdaLN projection's output dim out as
+ # [modality][param][hidden], and `hidden` is what TP fractures. Reordering the output dim to
+ # [device][modality][param][hidden_local] makes ColParallelLinear's contiguous column split
+ # hand each device every (modality, param) pair restricted to its own hidden slice.
+ def _reorder_for_tp(t: torch.Tensor) -> torch.Tensor:
+ # t is [out, ...] in torch convention; out == 6 * hidden_size * MODALITY_NUM.
+ trailing = t.shape[1:]
+ t = t.reshape(MODALITY_NUM, NUM_MODULATION_PARAMS, self.tp_factor, self.hidden_local, *trailing)
+ t = t.permute(2, 0, 1, 3, *range(4, t.ndim))
+ return t.reshape(-1, *trailing)
+
+ weight = state.pop("adaln_proj.linear.weight", None)
+ bias = state.pop("adaln_proj.linear.bias", None)
+ if self.precomputed_adaln:
+ # Dropped: these are the 26 GB the precomputed table exists to keep off the
+ # device. The pops above already strip them from the state the loader checks.
+ return
+ if weight is not None:
+ state["adaln_proj.weight"] = _reorder_for_tp(weight)
+ if bias is not None:
+ state["adaln_proj.bias"] = _reorder_for_tp(bias.unsqueeze(-1)).squeeze(-1)
+
+ # ------------------------------------------------------------------ modulation
+
+ def _modulation_tables(self, temb: ttnn.Tensor) -> list[ttnn.Tensor]:
+ """Project `temb` into the six per-(timestep, modality) modulation tables.
+
+ `temb` is [1, 1, num_timesteps, time_embed_dim]. Returns six [num_timesteps * MODALITY_NUM,
+ hidden_local] tables, row `t * MODALITY_NUM + modality`, matching the row order that
+ `adaln_indices` addresses.
+
+ The SiLU runs at `temb`'s own (float32) precision and only its result is cast
+ down to the bfloat16 projection, as the reference is explicit about: every block reads the
+ same `temb`, so rounding applied before the activation biases every block's modulation
+ identically at every sampling step and accumulates over the denoising trajectory.
+ """
+ num_timesteps = temb.shape[2]
+ # silu at temb's precision, then cast to the projection's dtype -- the reference's
+ # `self.linear(silu(temb).to(self.linear.weight.dtype))`. Casting here also keeps the
+ # resulting tables bfloat16, which `ttnn.embedding` requires of its weights.
+ activated = ttnn.silu(temb)
+ if activated.dtype != ttnn.bfloat16:
+ activated = ttnn.typecast(activated, ttnn.bfloat16)
+
+ # [1, 1, num_timesteps, MODALITY_NUM * 6 * hidden_local] per device
+ projected = self.adaln_proj(activated, compute_kernel_config=self.mm_compute_kernel_config)
+
+ # Fold the modality axis into rows: [num_timesteps, mod, param, h] -> [t * mod, param * h].
+ # Done in row-major because the row count changes and num_timesteps is tiny (a handful of
+ # rows), so the layout round-trip is negligible.
+ rows = num_timesteps * MODALITY_NUM
+ projected = ttnn.to_layout(projected, ttnn.ROW_MAJOR_LAYOUT)
+ projected = ttnn.reshape(projected, (1, 1, rows, NUM_MODULATION_PARAMS * self.hidden_local))
+ projected = ttnn.to_layout(projected, ttnn.TILE_LAYOUT)
+
+ tables = []
+ for p in range(NUM_MODULATION_PARAMS):
+ table = ttnn.slice(projected, [0, 0, 0, p * self.hidden_local], [1, 1, rows, (p + 1) * self.hidden_local])
+ # AdaLN applies the scales as (1 + scale). Adding the 1 to the table costs one elementwise
+ # op on `num_timesteps * MODALITY_NUM` rows -- six, typically -- where doing it after the
+ # gather would cost one over the whole packed sequence, per scale, per block.
+ if p in (_SCALE_MSA, _SCALE_MLP):
+ table = ttnn.add(table, 1.0)
+ # ttnn.embedding wants a 2D [num_embeddings, embedding_dim] weight.
+ table = ttnn.to_layout(table, ttnn.ROW_MAJOR_LAYOUT)
+ table = ttnn.reshape(table, (rows, self.hidden_local))
+ tables.append(ttnn.to_layout(table, ttnn.TILE_LAYOUT))
+ return tables
+
+ def _gather_rows(self, table: ttnn.Tensor, adaln_indices: ttnn.Tensor) -> ttnn.Tensor:
+ """Select one table row per row of the local packed sequence -> [1, 1, S_local, hidden_local]."""
+ out = ttnn.embedding(adaln_indices, table, layout=ttnn.TILE_LAYOUT)
+ return ttnn.unsqueeze(out, 0)
+
+ # ------------------------------------------------------------------ forward
+
+ def forward(
+ self,
+ spatial_1BND: ttnn.Tensor,
+ N: int,
+ temb: ttnn.Tensor | None,
+ adaln_indices: ttnn.Tensor,
+ rope_cos: ttnn.Tensor,
+ rope_sin: ttnn.Tensor,
+ modulation_tables: list[ttnn.Tensor] | None = None,
+ ) -> ttnn.Tensor:
+ """
+ spatial_1BND: fractured N on SP, fractured hidden_size on TP
+ temb: [1, 1, num_timesteps, time_embed_dim], replicated, float32
+ adaln_indices: [1, 1, 1, N_local] integer row indices, fractured N on SP
+ rope_cos/rope_sin: [1, 1, N_local, rotary_dim], fractured N on SP, replicated on TP
+ N: logical (unfractured) packed sequence length
+
+ Returns the block output, fractured N on SP and hidden_size on TP.
+ """
+ # Precomputed: the six tables come from the host-built cache, addressed by the same absolute
+ # `adaln_indices`, so nothing downstream changes. Otherwise project `temb` on device.
+ if modulation_tables is not None:
+ if len(modulation_tables) != NUM_MODULATION_PARAMS:
+ raise ValueError(f"expected {NUM_MODULATION_PARAMS} modulation tables, got {len(modulation_tables)}")
+ tables = modulation_tables
+ else:
+ if self.precomputed_adaln:
+ raise ValueError("block was built with precomputed_adaln but forward got no modulation_tables")
+ tables = self._modulation_tables(temb)
+
+ # ttnn.embedding takes [batch, seq] indices; uint32 is the dtype it expects.
+ indices = ttnn.reshape(adaln_indices, (1, adaln_indices.shape[-1]))
+ if indices.dtype != ttnn.uint32:
+ indices = ttnn.typecast(indices, ttnn.uint32)
+
+ def modulation(param: int) -> ttnn.Tensor:
+ return self._gather_rows(tables[param], indices)
+
+ # 1. Modulated self-attention. The (1 + scale) and shift are handed to the norm as a per-token
+ # dynamic weight and bias, so the fused norm op applies the modulation itself rather than the
+ # block following it with a separate multiply and add. The gated residual is one addcmul
+ # (residual + gate * branch) rather than a multiply and an add.
+ residual = spatial_1BND
+ normed = self.norm1(
+ spatial_1BND,
+ dynamic_weight=modulation(_SCALE_MSA),
+ dynamic_bias=modulation(_SHIFT_MSA),
+ )
+ # The gated residual is fused into to_out's matmul epilogue, so `attn` returns
+ # `residual + attn_out * gate` directly rather than the block adding it afterwards.
+ spatial_1BND = self.attn(
+ normed,
+ N=N,
+ rope_cos=rope_cos,
+ rope_sin=rope_sin,
+ addcmul_residual=residual,
+ addcmul_gate=modulation(_GATE_MSA),
+ )
+
+ # 2. Modulated feed-forward.
+ residual = spatial_1BND
+ normed = self.norm2(
+ spatial_1BND,
+ dynamic_weight=modulation(_SCALE_MLP),
+ dynamic_bias=modulation(_SHIFT_MLP),
+ )
+ # ff1 gathers the TP-fractured input inside its matmul (all_gather_minimal_matmul_async) when
+ # parallel_config is passed; ff2 is row-parallel and reduce-scatters back to TP-fractured.
+ if not self.use_fused_agmm and self.tp_factor > 1:
+ normed = self.ccl_manager.all_gather_persistent_buffer(normed, dim=3, mesh_axis=self.tp_mesh_axis)
+ # ff2's reduce-scatter and the gated residual after it fuse into a single
+ # minimal_matmul_strided_reduce_scatter_async, computing residual + ff2(...) * gate in one op.
+ #
+ # Only take it for a shape with a swept blocking. The generic fallback config runs the matmul
+ # on 56 of the device's 120 cores at subblock 1x1, making the fused op a measured 45%
+ # regression on this stage (1.75 -> 2.55 ms) -- and silently, since an unknown shape does not
+ # warn. See mmrs_config for the sweep and the grid/bandwidth tradeoff behind it.
+ #
+ # Ring only: the op asserts `attributes.topology == Ring` ("MinimalMatmulStridedReduceScatter
+ # Async only supports Ring topology"), so a line-cabled mesh has to take the unfused path
+ # below. Gated here rather than left to fail, because the assert fires on the first denoise
+ # step of the first request -- long after warmup reports the model loaded.
+ ff2_shape = (normed.shape[2], self.ffn_dim // self.tp_factor, self.hidden_size)
+ if self.tp_factor > 1 and self.ccl_manager.topology == ttnn.Topology.Ring and has_mmrs_config(*ff2_shape):
+ # M is only known here (it tracks the packed sequence length), so the blocking is
+ # registered at the point of use rather than at construction. Idempotent and cheap.
+ register_mmrs_config(*ff2_shape)
+ return self.ff.forward_fused_addcmul(
+ normed,
+ residual,
+ modulation(_GATE_MLP),
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=self.parallel_config if self.use_fused_agmm else None,
+ default_block_size=self.ff1_block_size,
+ )
+ ff_out = self.ff(
+ normed,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=self.parallel_config if self.use_fused_agmm else None,
+ default_block_size=self.ff1_block_size,
+ )
+ return ttnn.addcmul(residual, ff_out, modulation(_GATE_MLP))
diff --git a/code/models/tt_dit/models/transformers/wan2_2/attention_wan.py b/code/models/tt_dit/models/transformers/wan2_2/attention_wan.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c7f7e1cc1fc35e069f29aefff0c417b748c4b9e
--- /dev/null
+++ b/code/models/tt_dit/models/transformers/wan2_2/attention_wan.py
@@ -0,0 +1,529 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import torch
+
+import ttnn
+from models.common.utility_functions import is_blackhole
+
+from ....layers.linear import ColParallelLinear, LoRAColParallelLinear
+from ....layers.module import Module
+from ....layers.normalization import DistributedRMSNorm
+from ....parallel.config import DiTParallelConfig
+from ....parallel.manager import CCLManager
+from ....utils.matmul import get_matmul_config
+from ....utils.substate import pop_substate, rename_substate
+from ....utils.tensor import bf16_tensor
+
+
+class WanAttention(Module):
+ # Map from (is_blackhole, sp_factor, tp_factor) -> (q_chunk_size, k_chunk_size)
+ sdpa_chunk_size_map = {
+ (False, 2, 4): (256, 256),
+ (False, 8, 4): (256, 256),
+ (True, 2, 2): (128, 512),
+ (True, 8, 4): (288, 512),
+ (True, 32, 4): (224, 512),
+ }
+ default_sdpa_chunk_size = (256, 256)
+
+ def __init__(
+ self,
+ *,
+ dim: int,
+ num_heads: int,
+ qk_norm: bool = True,
+ eps: float = 1e-5,
+ mesh_device: ttnn.MeshDevice,
+ ccl_manager: CCLManager | None = None,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool = False,
+ is_self: bool = True,
+ sdpa_chunk_size_overrides: dict | None = None,
+ lora_enabled: bool = False,
+ ) -> None:
+ super().__init__()
+
+ assert dim % num_heads == 0
+ self.dim = dim
+ self.num_heads = num_heads
+ self.head_dim = dim // num_heads
+ self.qk_norm = qk_norm
+ self.eps = eps
+
+ self.mesh_device = mesh_device
+ self.ccl_manager = ccl_manager
+ self.parallel_config = parallel_config
+ self.is_self = is_self
+
+ self.n_local_heads = self.num_heads // self.parallel_config.tensor_parallel.factor
+
+ fsdp_mesh_axis = self.parallel_config.sequence_parallel.mesh_axis if is_fsdp else None
+
+ rms_kwargs = {
+ "embedding_dim": dim,
+ "norm_eps": eps,
+ "norm_elementwise_affine": True,
+ "bias": False,
+ "mesh_device": mesh_device,
+ "mesh_axis": parallel_config.tensor_parallel.mesh_axis,
+ "ccl_manager": ccl_manager,
+ }
+
+ self.norm_q = DistributedRMSNorm(**rms_kwargs)
+ self.norm_k = DistributedRMSNorm(**rms_kwargs)
+
+ col_parallel_kwargs = {
+ "bias": True,
+ "mesh_device": mesh_device,
+ "mesh_axis": parallel_config.tensor_parallel.mesh_axis,
+ "fsdp_mesh_axis": fsdp_mesh_axis,
+ "ccl_manager": ccl_manager,
+ }
+ ColCls = LoRAColParallelLinear if lora_enabled else ColParallelLinear
+
+ if is_self:
+ # Fused QKV for self-attention: single matmul split into 3 outputs
+ self.to_qkv = ColCls(
+ dim,
+ 3 * dim,
+ chunks=3,
+ **col_parallel_kwargs,
+ )
+ else:
+ # Cross-attention: Q from spatial, K/V from prompt
+ self.to_q = ColCls(dim, dim, **col_parallel_kwargs)
+ # Fused KV: single matmul split into 2 outputs
+ self.to_kv = ColCls(
+ dim,
+ 2 * dim,
+ chunks=2,
+ **col_parallel_kwargs,
+ )
+
+ self.to_out = ColCls(
+ dim,
+ dim,
+ bias=True,
+ mesh_device=mesh_device,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ fsdp_mesh_axis=fsdp_mesh_axis,
+ ccl_manager=ccl_manager,
+ )
+
+ self.dummy_joint_input = bf16_tensor(torch.zeros((1, self.n_local_heads, 0, self.head_dim)), device=mesh_device)
+
+ full_grid = self.mesh_device.compute_with_storage_grid_size()
+ self.sdpa_program_config = ttnn.SDPAProgramConfig(
+ compute_with_storage_grid_size=full_grid,
+ q_chunk_size=256,
+ k_chunk_size=256,
+ exp_approx_mode=False, # NOTE: False is more correct
+ )
+
+ self.sdpa_worker_grid = (full_grid.x - 1, full_grid.y) # Reserve last column for CCL
+ chunk_lookup = {**self.sdpa_chunk_size_map, **(sdpa_chunk_size_overrides or {})}
+ ring_sdpa_chunk_size = chunk_lookup.get(
+ (
+ is_blackhole(),
+ self.parallel_config.sequence_parallel.factor,
+ self.parallel_config.tensor_parallel.factor,
+ ),
+ self.default_sdpa_chunk_size,
+ )
+
+ self.ring_sdpa_program_config = ttnn.SDPAProgramConfig(
+ compute_with_storage_grid_size=self.sdpa_worker_grid,
+ q_chunk_size=ring_sdpa_chunk_size[0],
+ k_chunk_size=ring_sdpa_chunk_size[1],
+ exp_approx_mode=False, # NOTE: False is more correct
+ )
+
+ self.use_exp_ring_sdpa = (
+ self.parallel_config.tensor_parallel.factor == 4 and self.parallel_config.sequence_parallel.factor == 32
+ )
+
+ if self.use_exp_ring_sdpa:
+ self.exp_ring_sdpa_program_config = ttnn.SDPAProgramConfig(
+ compute_with_storage_grid_size=full_grid,
+ q_chunk_size=ring_sdpa_chunk_size[0],
+ k_chunk_size=ring_sdpa_chunk_size[1],
+ exp_approx_mode=False,
+ )
+
+ self.sdpa_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ self.mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi2,
+ math_approx_mode=False,
+ fp32_dest_acc_en=False, # NOTE: Set to True if there's a correctness issue
+ )
+
+ self.rope_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ self.mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi4,
+ math_approx_mode=False,
+ fp32_dest_acc_en=True,
+ packer_l1_acc=True,
+ )
+
+ device_grid = self.mesh_device.compute_with_storage_grid_size()
+ self.core_grid = ttnn.CoreGrid(x=device_grid.x, y=device_grid.y)
+
+ self.mm_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ self.mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi2,
+ math_approx_mode=True,
+ fp32_dest_acc_en=True,
+ packer_l1_acc=True,
+ )
+
+ def _prepare_torch_state(self, state: dict[str, torch.Tensor]) -> None:
+ rename_substate(state, "to_out.0", "to_out")
+
+ def _interleave_heads(tensors: list[torch.Tensor]):
+ """Interleave weight/bias tensors so column-parallel fracturing assigns correct heads per device.
+
+ Each tensor has out-dim = num_heads * head_dim. We interleave them on the heads
+ axis so that after TP column-sharding each device gets the right heads from every tensor.
+
+ Args:
+ tensors: list of tensors in [out, in] PyTorch convention (or [out, 1] for bias).
+ Returns:
+ Merged tensor in [out, in] PyTorch convention.
+ """
+ n_dev = self.parallel_config.tensor_parallel.factor
+ # Transpose to [in, out]
+ tensors = [t.T for t in tensors]
+ # Reshape out dim to [in, n_dev, n_local_heads, head_dim]
+ tensors = [t.reshape(t.shape[0], n_dev, self.n_local_heads, self.head_dim) for t in tensors]
+ # Concatenate on the heads dim so each device shard gets its own heads from every tensor
+ merged = torch.cat(tensors, dim=2) # [in, n_dev, len(tensors)*n_local_heads, head_dim]
+ merged = merged.reshape(merged.shape[0], len(tensors) * self.num_heads * self.head_dim)
+ # Transpose back to [out, in] PyTorch convention
+ return merged.T
+
+ if self.is_self:
+ # Merge separate to_q, to_k, to_v weights into fused to_qkv
+ q_state = pop_substate(state, "to_q")
+ k_state = pop_substate(state, "to_k")
+ v_state = pop_substate(state, "to_v")
+
+ state["to_qkv.weight"] = _interleave_heads([q_state["weight"], k_state["weight"], v_state["weight"]])
+ if "bias" in q_state:
+ bias = _interleave_heads(
+ [q_state["bias"].unsqueeze(-1), k_state["bias"].unsqueeze(-1), v_state["bias"].unsqueeze(-1)]
+ )
+ state["to_qkv.bias"] = bias.squeeze(-1)
+ else:
+ # Merge separate to_k, to_v weights into fused to_kv
+ k_state = pop_substate(state, "to_k")
+ v_state = pop_substate(state, "to_v")
+
+ state["to_kv.weight"] = _interleave_heads([k_state["weight"], v_state["weight"]])
+ if "bias" in k_state:
+ bias = _interleave_heads([k_state["bias"].unsqueeze(-1), v_state["bias"].unsqueeze(-1)])
+ state["to_kv.bias"] = bias.squeeze(-1)
+
+ def _to_out_fused_addcmul(
+ self,
+ x: ttnn.Tensor,
+ addcmul_residual: ttnn.Tensor,
+ addcmul_gate: ttnn.Tensor,
+ compute_kernel_config=None,
+ parallel_config=None,
+ dtype=None,
+ ) -> ttnn.Tensor:
+ """Fused to_out projection + addcmul: output = residual + (matmul(x, W) + bias) * gate."""
+ to_out = self.to_out
+
+ # Reads to_out.weight.data directly, so runtime-mode LoRA (which keeps
+ # the delta in self._runtime_A/B) would silently no-op. Fuse mode is
+ # fine — the delta lives in weight.data.
+ if getattr(to_out, "lora_mode", None) == "runtime" and getattr(to_out, "is_lora_active", False):
+ raise RuntimeError(
+ "runtime LoRA mode is incompatible with WanAttention._to_out_fused_addcmul; "
+ "construct to_out with lora_mode='fuse'"
+ )
+
+ # Handle FSDP weight gathering (mirrors ColParallelLinear.forward)
+ if to_out.fsdp_mesh_axis is not None and to_out.mesh_device.shape[to_out.fsdp_mesh_axis] > 1:
+ unsqueezed_weight = ttnn.unsqueeze_to_4D(to_out.weight.data)
+ weight = self.ccl_manager.all_gather_persistent_buffer(
+ unsqueezed_weight, dim=2, mesh_axis=to_out.fsdp_mesh_axis
+ )
+ weight = ttnn.reshape(weight, (weight.shape[-2], weight.shape[-1]))
+ else:
+ weight = to_out.weight.data
+
+ if parallel_config is not None and parallel_config.tensor_parallel.factor > 1:
+ M, K, N = x.padded_shape[-2], weight.padded_shape[-2], weight.padded_shape[-1]
+ full_grid = self.mesh_device.compute_with_storage_grid_size()
+ core_grid = ttnn.CoreCoord(full_grid.x, full_grid.y - 1)
+ matmul_config = get_matmul_config(M, K, N, core_grid)
+
+ ag_persistent_buffer = self.ccl_manager.get_ag_ping_pong_buffer(
+ x.shape, 3, parallel_config.tensor_parallel.mesh_axis, dtype=x.get_dtype()
+ )
+ ag_global_semaphores = self.ccl_manager.get_ag_ping_pong_semaphore(
+ parallel_config.tensor_parallel.mesh_axis
+ )
+ output = ttnn.experimental.all_gather_minimal_matmul_async(
+ input_tensor=x,
+ weight_tensor=weight,
+ bias_tensor=to_out.bias.data if to_out.bias is not None else None,
+ config=matmul_config,
+ compute_kernel_config=compute_kernel_config or to_out.compute_config,
+ persistent_output_buffer=ag_persistent_buffer,
+ multi_device_global_semaphore=ag_global_semaphores,
+ num_links=self.ccl_manager.num_links,
+ topology=self.ccl_manager.topology,
+ cluster_axis=parallel_config.tensor_parallel.mesh_axis,
+ barrier_semaphore=None,
+ force_transpose=True,
+ num_workers_per_link=full_grid.x // self.ccl_manager.num_links,
+ num_buffers_per_channel=48 if not is_blackhole() else 24,
+ scalar=1.0,
+ addcmul_input_tensor1=addcmul_residual,
+ addcmul_input_tensor2=addcmul_gate,
+ dtype=dtype,
+ )[0]
+ else:
+ M, K, N_out = x.padded_shape[-2], x.padded_shape[-1], weight.padded_shape[-1]
+ core_grid = self.mesh_device.compute_with_storage_grid_size()
+ matmul_config = get_matmul_config(M, K, N_out, core_grid)
+
+ output = ttnn.experimental.dit_minimal_matmul_addcmul_fused(
+ x,
+ weight,
+ 1.0, # scalar
+ addcmul_residual,
+ addcmul_gate,
+ bias_tensor=to_out.bias.data if to_out.bias is not None else None,
+ config=matmul_config,
+ compute_kernel_config=compute_kernel_config or to_out.compute_config,
+ dtype=dtype,
+ )
+ return output
+
+ def forward(
+ self,
+ spatial_1BND: ttnn.Tensor,
+ N: int,
+ prompt_1BLP: ttnn.Tensor | None = None,
+ cross_attn_mask: ttnn.Tensor | None = None,
+ rope_cos: ttnn.Tensor | None = None,
+ rope_sin: ttnn.Tensor | None = None,
+ trans_mat: ttnn.Tensor | None = None,
+ addcmul_residual: ttnn.Tensor | None = None,
+ addcmul_gate: ttnn.Tensor | None = None,
+ ) -> ttnn.Tensor:
+ """
+ spatial_1BND: fractured N on SP, fracturd D on TP
+ prompt_1BLP: replicated on SP, replicated D on TP (optional)
+ cross_attn_mask: (optional, cross-attn only) mask for SDPA, shape [B|1, nqh|1, Sq, Sk].
+ Must be TILE layout, bf16/bfp8/bfp4 dtype, on device in DRAM.
+ rope_cos: fractured on SP, TP
+ rope_sin: fractured on SP, TP
+ trans_mat: replicated
+ addcmul_residual: (optional) residual tensor for fused matmul+addcmul (self-attn only)
+ addcmul_gate: (optional) gate tensor for fused matmul+addcmul (self-attn only)
+
+ If prompt_1BLP is not provided, run self-attention.
+ Otherwise, run cross-attention on prompt.
+
+ When addcmul_residual and addcmul_gate are both provided (self-attention only),
+ the to_out projection and residual addcmul are fused into a single op:
+ output = addcmul_residual + to_out(attn_output) * addcmul_gate
+
+ Outputs:
+ spatial_1BND: fractured N on SP, fractured D on TP
+ """
+
+ if rope_cos is not None:
+ # If ROPE is given, this is self-attention
+ assert rope_sin is not None
+ assert trans_mat is not None
+ assert prompt_1BLP is None
+
+ use_nonfused_agmm = (self.ccl_manager.topology == ttnn.Topology.Linear) and (
+ self.parallel_config.tensor_parallel.factor > 1
+ )
+ if use_nonfused_agmm:
+ spatial_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_1BND, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+
+ if self.is_self:
+ # Fused QKV matmul with split output for self-attention
+ q_1BNF, k_1BNF, v_1BNF = self.to_qkv(
+ spatial_1BND,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=None if use_nonfused_agmm else self.parallel_config,
+ )
+ else:
+ # Cross-attention: Q from spatial, fused KV from prompt
+ assert prompt_1BLP is not None
+ kv_input = prompt_1BLP
+ q_1BNF = self.to_q(
+ spatial_1BND,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=None if use_nonfused_agmm else self.parallel_config,
+ )
+ k_1BNF, v_1BNF = self.to_kv(kv_input, compute_kernel_config=self.mm_compute_kernel_config)
+
+ # Set norm output dtype to the input dtype required for ring self-attn.
+ sdpa_input_dtype = getattr(self, "_sdpa_input_dtype", None)
+ use_ring_sdpa = self.parallel_config.sequence_parallel.factor > 1
+ norm_output_dtype = sdpa_input_dtype if (use_ring_sdpa and prompt_1BLP is None) else None
+
+ # Norm spatial before splitting heads
+ q_BHNE = self.norm_q(
+ q_1BNF,
+ num_heads_per_device=self.n_local_heads,
+ rope_cos=rope_cos,
+ rope_sin=rope_sin,
+ trans_mat=trans_mat,
+ dtype=norm_output_dtype,
+ )
+ k_BHNE = self.norm_k(
+ k_1BNF,
+ num_heads_per_device=self.n_local_heads,
+ rope_cos=rope_cos,
+ rope_sin=rope_sin,
+ trans_mat=trans_mat,
+ dtype=norm_output_dtype,
+ )
+
+ def create_heads(inp):
+ out, _, _ = ttnn.experimental.nlp_create_qkv_heads(
+ inp,
+ num_heads=self.n_local_heads,
+ num_kv_heads=0,
+ transpose_k_heads=False,
+ )
+ return out
+
+ v_BHNE = create_heads(v_1BNF)
+
+ if prompt_1BLP is None:
+ # Self attention
+ if self.parallel_config.sequence_parallel.factor > 1:
+ # Q and K already cast by norm kernel; cast V and dummy joint inputs to match
+ dummy_joint = self.dummy_joint_input
+ if sdpa_input_dtype is not None:
+ if v_BHNE.dtype != sdpa_input_dtype:
+ v_BHNE = ttnn.typecast(v_BHNE, sdpa_input_dtype)
+ if dummy_joint.dtype != sdpa_input_dtype:
+ dummy_joint = ttnn.typecast(dummy_joint, sdpa_input_dtype)
+
+ # HACK: pass null joint inputs to take advantage of ring attention, even though this is self-attention.
+ if self.use_exp_ring_sdpa:
+ spatial_BHNE, prompt_BHLE, _lse = ttnn.transformer.exp_ring_joint_scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ dummy_joint,
+ dummy_joint,
+ dummy_joint,
+ persistent_output_buffer_k=self.ccl_manager.get_ag_ping_pong_buffer(
+ k_BHNE.shape, 2, self.parallel_config.sequence_parallel.mesh_axis, dtype=k_BHNE.dtype
+ ),
+ persistent_output_buffer_v=self.ccl_manager.get_ag_ping_pong_buffer(
+ v_BHNE.shape, 2, self.parallel_config.sequence_parallel.mesh_axis, dtype=v_BHNE.dtype
+ ),
+ joint_strategy="rear",
+ logical_n=N,
+ program_config=self.exp_ring_sdpa_program_config,
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ dim=2,
+ multi_device_global_semaphore=self.ccl_manager.get_exp_ring_ping_pong_semaphore(
+ self.parallel_config.sequence_parallel.mesh_axis
+ ),
+ num_links=self.ccl_manager.num_links,
+ cluster_axis=self.parallel_config.sequence_parallel.mesh_axis,
+ mesh_device=self.mesh_device,
+ topology=self.ccl_manager.topology,
+ subdevice_id=self.ccl_manager.ccl_sub_device_id,
+ num_workers_per_link=5,
+ num_buffers_per_channel=32,
+ )
+ else:
+ spatial_BHNE, prompt_BHLE, _lse = ttnn.transformer.ring_joint_scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ dummy_joint,
+ dummy_joint,
+ dummy_joint,
+ persistent_output_buffer_k=self.ccl_manager.get_ag_ping_pong_buffer(
+ k_BHNE.shape, 2, self.parallel_config.sequence_parallel.mesh_axis, dtype=k_BHNE.dtype
+ ),
+ persistent_output_buffer_v=self.ccl_manager.get_ag_ping_pong_buffer(
+ v_BHNE.shape, 2, self.parallel_config.sequence_parallel.mesh_axis, dtype=v_BHNE.dtype
+ ),
+ joint_strategy="rear",
+ logical_n=N,
+ program_config=self.ring_sdpa_program_config,
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ dim=2,
+ multi_device_global_semaphore=self.ccl_manager.get_ag_ping_pong_semaphore(
+ self.parallel_config.sequence_parallel.mesh_axis
+ ),
+ num_links=self.ccl_manager.num_links,
+ cluster_axis=self.parallel_config.sequence_parallel.mesh_axis,
+ mesh_device=self.mesh_device,
+ topology=self.ccl_manager.topology,
+ subdevice_id=self.ccl_manager.ccl_sub_device_id,
+ ccl_core_grid_offset=(self.sdpa_worker_grid[0], 0),
+ use_column_major_ccl=True,
+ )
+ else:
+ spatial_BHNE = ttnn.transformer.scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ is_causal=False,
+ program_config=self.sdpa_program_config,
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ )
+ else:
+ # Cross attention
+ spatial_BHNE = ttnn.transformer.scaled_dot_product_attention(
+ q_BHNE,
+ k_BHNE,
+ v_BHNE,
+ is_causal=False,
+ attn_mask=cross_attn_mask,
+ program_config=self.sdpa_program_config,
+ compute_kernel_config=self.sdpa_compute_kernel_config,
+ )
+
+ spatial_1BND = ttnn.transformer.concatenate_heads(spatial_BHNE)
+ spatial_1BND = ttnn.unsqueeze(spatial_1BND, 0)
+
+ if use_nonfused_agmm:
+ # Gather spatial on TP axis before projection
+ spatial_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_1BND, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+
+ if addcmul_residual is not None and addcmul_gate is not None:
+ # Fused to_out projection + addcmul (self-attention only)
+ spatial_1BND = self._to_out_fused_addcmul(
+ spatial_1BND,
+ addcmul_residual,
+ addcmul_gate,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=None if use_nonfused_agmm else self.parallel_config,
+ )
+ else:
+ spatial_1BND = self.to_out(
+ spatial_1BND,
+ compute_kernel_config=self.mm_compute_kernel_config,
+ parallel_config=None if use_nonfused_agmm else self.parallel_config,
+ )
+
+ return spatial_1BND
diff --git a/code/models/tt_dit/models/transformers/wan2_2/transformer_wan.py b/code/models/tt_dit/models/transformers/wan2_2/transformer_wan.py
new file mode 100644
index 0000000000000000000000000000000000000000..8668c15ec5966f47a195923ccdc20d66271f8d54
--- /dev/null
+++ b/code/models/tt_dit/models/transformers/wan2_2/transformer_wan.py
@@ -0,0 +1,779 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
+import torch
+from diffusers import WanTransformer3DModel as TorchWanTransformer3DModel
+from diffusers.models.transformers.transformer_wan import WanRotaryPosEmbed as TorchWanRotaryPosEmbed
+from loguru import logger
+
+if not os.environ.get("TT_DIT_DEBUG"):
+ logger.disable(__name__)
+
+
+import ttnn
+
+from ....layers.embeddings import WanPatchEmbed, WanTimeTextImageEmbedding
+from ....layers.feedforward import ParallelFeedForward
+from ....layers.linear import Linear
+from ....layers.module import Module, ModuleList, Parameter
+from ....layers.normalization import DistributedLayerNorm
+from ....parallel.config import DiTParallelConfig
+from ....parallel.manager import CCLManager
+from ....utils import cache
+from ....utils.mochi import get_rot_transformation_mat
+from ....utils.padding import pad_vision_seq_parallel
+from ....utils.substate import pop_substate, rename_substate
+from ....utils.tensor import bf16_tensor, from_torch, unflatten
+from ....utils.tracing import traced_function
+from .attention_wan import WanAttention
+
+
+class WanTransformerBlock(Module):
+ def __init__(
+ self,
+ *,
+ dim: int,
+ ffn_dim: int,
+ num_heads: int,
+ cross_attention_norm: bool = True,
+ eps: float = 1e-6,
+ mesh_device: ttnn.MeshDevice,
+ ccl_manager: CCLManager | None = None,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool = False,
+ sdpa_chunk_size_overrides: dict | None = None,
+ lora_enabled: bool = False,
+ ) -> None:
+ super().__init__()
+
+ self.dim = dim
+ self.ffn_dim = ffn_dim
+ self.num_heads = num_heads
+ self.cross_attention_norm = cross_attention_norm
+ self.eps = eps
+
+ self.mesh_device = mesh_device
+ self.ccl_manager = ccl_manager
+ self.parallel_config = parallel_config
+
+ fsdp_mesh_axis = self.parallel_config.sequence_parallel.mesh_axis if is_fsdp else None
+
+ self.norm1 = DistributedLayerNorm(
+ dim,
+ norm_eps=eps,
+ norm_elementwise_affine=False,
+ bias=False,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ )
+
+ self.attn1 = WanAttention(
+ dim=dim,
+ num_heads=num_heads,
+ eps=eps,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ is_self=True,
+ sdpa_chunk_size_overrides=sdpa_chunk_size_overrides,
+ lora_enabled=lora_enabled,
+ )
+
+ self.attn2 = WanAttention(
+ dim=dim,
+ num_heads=num_heads,
+ eps=eps,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ is_self=False,
+ sdpa_chunk_size_overrides=sdpa_chunk_size_overrides,
+ lora_enabled=lora_enabled,
+ )
+
+ self.norm2 = (
+ DistributedLayerNorm(
+ dim,
+ norm_eps=eps,
+ norm_elementwise_affine=True,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ )
+ if cross_attention_norm
+ else None
+ )
+
+ self.ffn = ParallelFeedForward(
+ dim,
+ inner_dim=ffn_dim,
+ activation_fn="gelu_tanh",
+ bias=True,
+ mesh_device=mesh_device,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ ccl_manager=ccl_manager,
+ fsdp_mesh_axis=fsdp_mesh_axis,
+ lora_enabled=lora_enabled,
+ )
+
+ self.norm3 = DistributedLayerNorm(
+ dim,
+ norm_eps=eps,
+ norm_elementwise_affine=False,
+ bias=False,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ )
+
+ self.scale_shift_table = Parameter(
+ total_shape=[1, 1, 6, dim],
+ mesh_axes=[None, None, None, parallel_config.tensor_parallel.mesh_axis],
+ device=mesh_device,
+ dtype=ttnn.float32,
+ )
+
+ self.ff_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi2,
+ math_approx_mode=False,
+ fp32_dest_acc_en=True,
+ packer_l1_acc=True,
+ )
+
+ device_grid = self.mesh_device.compute_with_storage_grid_size()
+ self.core_grid = ttnn.CoreGrid(x=device_grid.x, y=device_grid.y)
+
+ def _prepare_torch_state(self, state: dict[str, torch.Tensor]) -> None:
+ rename_substate(state, "ffn.net.0.proj", "ffn.ff1")
+ rename_substate(state, "ffn.net.2", "ffn.ff2")
+
+ if "scale_shift_table" in state:
+ state["scale_shift_table"] = state["scale_shift_table"].unsqueeze(0)
+
+ def forward(
+ self,
+ spatial_1BND: ttnn.Tensor,
+ prompt_1BLP: ttnn.Tensor,
+ temb_1BTD: ttnn.Tensor,
+ N: int,
+ rope_cos: ttnn.Tensor,
+ rope_sin: ttnn.Tensor,
+ trans_mat: ttnn.Tensor,
+ cross_attn_mask: ttnn.Tensor | None = None,
+ ) -> ttnn.Tensor:
+ """
+ spatial_1BND: fractured N on SP, fractured D on TP
+ prompt_1BLP: replicated on SP, replicated D on TP
+ temb_1BTD: replicated on SP, fractured D on TP
+ N: logical sequence length of the spatial input
+ rope_cos_BANH: fractured N on SP, A (num_heads) on TP
+ rope_sin_BANH: fractured N on SP, A (num_heads) on TP
+ trans_mat: replicated on SP, replicated D on TP
+
+ Outputs:
+ spatial_1BND: fractured N on SP, fractured D on TP
+ """
+
+ assert temb_1BTD.shape[2] == 6, "wan2.2 14b expects 6 chunks in timestep embedding"
+
+ shifted_temb_1BTD = self.scale_shift_table.data + temb_1BTD
+ shift_msa_1B1D, scale_msa_1B1D, gate_msa_1B1D, c_shift_msa_1B1D, c_scale_msa_1B1D, c_gate_msa_1B1D = ttnn.chunk(
+ shifted_temb_1BTD, 6, dim=2
+ )
+
+ # NOTE: workaround - addcmul (fused and unfused) is less accurate with fp32 gate input
+ gate_msa_1B1D = ttnn.typecast(gate_msa_1B1D, dtype=ttnn.bfloat16)
+ c_gate_msa_1B1D = ttnn.typecast(c_gate_msa_1B1D, dtype=ttnn.bfloat16)
+
+ spatial_normed_1BND = self.norm1(
+ spatial_1BND, dynamic_weight=(1.0 + scale_msa_1B1D), dynamic_bias=shift_msa_1B1D
+ )
+
+ # Self attention on spatial with fused residual addcmul
+ # Fuses: spatial_1BND = spatial_1BND + to_out(attn_output) * gate_msa_1B1D
+ spatial_1BND = self.attn1(
+ spatial_1BND=spatial_normed_1BND,
+ N=N,
+ rope_cos=rope_cos,
+ rope_sin=rope_sin,
+ trans_mat=trans_mat,
+ addcmul_residual=spatial_1BND,
+ addcmul_gate=gate_msa_1B1D,
+ )
+
+ # Cross attention on prompt
+ spatial_normed_1BND = self.norm2(spatial_1BND)
+
+ attn_output_1BND = self.attn2(
+ spatial_1BND=spatial_normed_1BND,
+ N=N,
+ prompt_1BLP=prompt_1BLP,
+ cross_attn_mask=cross_attn_mask,
+ )
+ spatial_1BND = spatial_1BND + attn_output_1BND
+
+ # Feed Forward
+ spatial_normed_1BND = self.norm3(
+ spatial_1BND, dynamic_weight=(1.0 + c_scale_msa_1B1D), dynamic_bias=c_shift_msa_1B1D
+ )
+
+ if self.ccl_manager.topology == ttnn.Topology.Linear:
+ if self.parallel_config.tensor_parallel.factor > 1:
+ spatial_normed_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_normed_1BND, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+
+ spatial_ff_1BND = self.ffn(spatial_normed_1BND, compute_kernel_config=self.ff_compute_kernel_config)
+ # spatial_1BND = spatial_1BND + spatial_ff_1BND * c_gate_msa_1B1D
+ # NOTE: higher precision compute config in addcmul may be needed for correctness
+ spatial_1BND = ttnn.addcmul(spatial_1BND, spatial_ff_1BND, c_gate_msa_1B1D)
+ else:
+ # Fused FFN + addcmul at the RS final write step (Phase 2).
+ # Both 'a' (spatial_1BND) and 'b' (c_gate_msa_1B1D) are already at [D/tp] size
+ # on each TP device — no AllGather or scatter matmul needed.
+ spatial_1BND = self.ffn.forward_fused_addcmul(
+ spatial_normed_1BND,
+ spatial_1BND,
+ c_gate_msa_1B1D,
+ scalar=1.0,
+ compute_kernel_config=self.ff_compute_kernel_config,
+ parallel_config=self.parallel_config,
+ )
+
+ return spatial_1BND
+
+
+class WanTransformer3DModel(Module):
+ def __init__(
+ self,
+ *,
+ patch_size: tuple = (1, 2, 2),
+ num_heads: int = 40,
+ dim: int = 5120,
+ in_channels: int = 16,
+ out_channels: int = 16,
+ text_dim: int = 4096,
+ freq_dim: int = 256,
+ ffn_dim: int = 13824,
+ num_layers: int = 40,
+ cross_attn_norm: bool = True,
+ eps: float = 1e-6,
+ rope_max_seq_len: int = 1024,
+ mesh_device: ttnn.MeshDevice,
+ ccl_manager: CCLManager | None = None,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool = True,
+ model_type: str = "t2v",
+ output_dtype: ttnn.DataType = ttnn.float32,
+ lora_enabled: bool = False,
+ ) -> None:
+ super().__init__()
+
+ self.mesh_device = mesh_device
+ self.ccl_manager = ccl_manager
+ self.parallel_config = parallel_config
+ self.is_fsdp = is_fsdp
+ self.lora_enabled = lora_enabled
+ self.fsdp_mesh_axis = self.parallel_config.sequence_parallel.mesh_axis if is_fsdp else None
+ self.model_type = model_type
+ self.cached_rope_features = {}
+ self.output_dtype = output_dtype
+
+ assert model_type in ["t2v", "i2v"], "model_type must be either t2v or i2v"
+ if model_type == "i2v":
+ in_channels = 36
+ else:
+ assert in_channels == 16, "in_channels must be 16 for t2v"
+
+ self.patch_size = patch_size
+ self.dim = dim
+
+ self.out_channels = out_channels
+
+ # NOTE: Fallback
+ self.rope = TorchWanRotaryPosEmbed(
+ dim // num_heads,
+ patch_size,
+ rope_max_seq_len,
+ )
+
+ self.patch_embedding = WanPatchEmbed(
+ patch_size=patch_size,
+ in_channels=in_channels,
+ embed_dim=dim,
+ mesh_device=mesh_device,
+ tp_mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ )
+
+ self.condition_embedder = WanTimeTextImageEmbedding(
+ dim=dim,
+ time_freq_dim=freq_dim,
+ time_proj_dim=dim * 6,
+ text_embed_dim=text_dim,
+ mesh_device=self.mesh_device,
+ tp_mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ ccl_manager=ccl_manager,
+ )
+
+ self.blocks = ModuleList(
+ WanTransformerBlock(
+ dim=dim,
+ ffn_dim=ffn_dim,
+ num_heads=num_heads,
+ cross_attention_norm=cross_attn_norm,
+ eps=eps,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ lora_enabled=lora_enabled,
+ )
+ for i in range(num_layers)
+ )
+
+ self.norm_out = DistributedLayerNorm(
+ dim,
+ norm_eps=eps,
+ norm_elementwise_affine=False,
+ bias=False,
+ mesh_axis=parallel_config.tensor_parallel.mesh_axis,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ )
+
+ self.proj_out = Linear(
+ dim,
+ patch_size[0] * patch_size[1] * patch_size[2] * self.out_channels,
+ bias=True,
+ mesh_device=mesh_device,
+ )
+
+ self.scale_shift_table = Parameter(
+ total_shape=[1, 2, dim],
+ device=mesh_device,
+ mesh_axes=[None, None, parallel_config.tensor_parallel.mesh_axis],
+ dtype=ttnn.float32,
+ )
+
+ self.hifi4_compute_kernel_config = ttnn.init_device_compute_kernel_config(
+ mesh_device.arch(),
+ math_fidelity=ttnn.MathFidelity.HiFi4,
+ math_approx_mode=False,
+ fp32_dest_acc_en=True,
+ packer_l1_acc=True,
+ )
+
+ device_grid = self.mesh_device.compute_with_storage_grid_size()
+ self.core_grid = ttnn.CoreGrid(x=device_grid.x, y=device_grid.y)
+
+ def save(self, directory: str | Path, /, *, prefix: str = "") -> None:
+ super().save(directory, prefix=prefix)
+
+ directory = Path(directory)
+
+ # Torch fallbacks
+ torch.save(self.rope.state_dict(), directory / f"{prefix}rope.pt")
+
+ def load(self, directory: str | Path, /, *, prefix: str = "") -> None:
+ super().load(directory, prefix=prefix)
+
+ directory = Path(directory)
+
+ # Torch fallbacks
+ self.rope.load_state_dict(torch.load(directory / f"{prefix}rope.pt"))
+
+ def _prepare_torch_state(self, state: dict[str, torch.Tensor]) -> None:
+ # Torch fallbacks
+ self.rope.load_state_dict(pop_substate(state, "rope"))
+
+ def get_rope_features(self, hidden_states):
+ if tuple(hidden_states.shape) not in self.cached_rope_features:
+ rope_features = self.prepare_rope_features(hidden_states)
+ self.cached_rope_features[tuple(hidden_states.shape)] = rope_features
+ return self.cached_rope_features[tuple(hidden_states.shape)]
+
+ def prepare_rope_features(self, hidden_states):
+ """
+ Given video input, compute RoPE features.
+ Return tensors on device.
+ """
+ logger.info(f"Preparing rope features for shape {hidden_states.shape}")
+ rope_cos, rope_sin = self.rope(hidden_states)
+
+ # Convert to TT tensors with proper sharding
+ rope_cos_1HND = rope_cos.permute(0, 2, 1, 3)
+ rope_sin_1HND = rope_sin.permute(0, 2, 1, 3)
+
+ rope_cos_1HND = pad_vision_seq_parallel(
+ rope_cos_1HND, num_devices=self.parallel_config.sequence_parallel.factor
+ )
+ rope_sin_1HND = pad_vision_seq_parallel(
+ rope_sin_1HND, num_devices=self.parallel_config.sequence_parallel.factor
+ )
+
+ trans_mat = get_rot_transformation_mat()
+
+ sp_axis = self.parallel_config.sequence_parallel.mesh_axis
+ tt_rope_cos_1HND = from_torch(
+ rope_cos_1HND,
+ device=self.mesh_device,
+ dtype=ttnn.float32,
+ mesh_axes=[..., sp_axis, None],
+ )
+ tt_rope_sin_1HND = from_torch(
+ rope_sin_1HND,
+ device=self.mesh_device,
+ dtype=ttnn.float32,
+ mesh_axes=[..., sp_axis, None],
+ )
+ tt_trans_mat = bf16_tensor(trans_mat, device=self.mesh_device)
+
+ logger.info(f"TT rope cos shape: {tt_rope_cos_1HND.shape}")
+ logger.info(f"TT rope sin shape: {tt_rope_sin_1HND.shape}")
+ logger.info(f"TT trans mat shape: {tt_trans_mat.shape}")
+
+ return tt_rope_cos_1HND, tt_rope_sin_1HND, tt_trans_mat
+
+ def prepare_text_conditioning(self, encoder_hidden_states):
+ tt_prompt_1BLP = self.condition_embedder.forward_text(encoder_hidden_states)
+
+ logger.info(f"TT prompt shape: {tt_prompt_1BLP.shape}")
+ return tt_prompt_1BLP
+
+ def prepare_timestep_conditioning(self, timestep):
+ tt_temb_11BD, tt_timestep_proj_1BTD = self.condition_embedder.forward_timestep(timestep, timestep_seq_len=None)
+ tt_timestep_proj_1BTD = unflatten(ttnn.squeeze(tt_timestep_proj_1BTD, -2), -1, (6, -1))
+ logger.info(f"TT temb shape: {tt_temb_11BD.shape}")
+ logger.info(f"TT timestep proj shape: {tt_timestep_proj_1BTD.shape}")
+ return tt_temb_11BD, tt_timestep_proj_1BTD
+
+ def prepare_conditioning(self, timestep, encoder_hidden_states):
+ """
+ Given inputs, execute the combined timestep and text embedding.
+ Return tensors on device.
+ """
+ tt_temb_11BD, tt_timestep_proj_1BTD = self.prepare_timestep_conditioning(timestep)
+ tt_prompt_1BLP = self.prepare_text_conditioning(encoder_hidden_states)
+
+ logger.info(f"TT temb shape: {tt_temb_11BD.shape}")
+ logger.info(f"TT timestep proj shape: {tt_timestep_proj_1BTD.shape}")
+ logger.info(f"TT prompt shape: {tt_prompt_1BLP.shape}")
+ return tt_temb_11BD, tt_timestep_proj_1BTD, tt_prompt_1BLP
+
+ def preprocess_spatial_input_host(self, spatial):
+ B, C, F, H, W = spatial.shape
+ logger.info(f"Preprocessing spatial input with shape {spatial.shape}")
+ assert B == 1, "Batch size must be 1"
+ pF, pH, pW = self.patch_size
+ patch_F, patch_H, patch_W = F // pF, H // pH, W // pW
+ N = patch_F * patch_H * patch_W
+
+ # Patchify video input
+ spatial = spatial.reshape(B, C, patch_F, pF, patch_H, pH, patch_W, pW)
+ spatial = spatial.permute(0, 2, 4, 6, 3, 5, 7, 1).reshape(1, B, N, pF * pH * pW * C)
+ logger.info(f"spatial input after patchifying: {spatial.shape}")
+
+ spatial = pad_vision_seq_parallel(spatial, num_devices=self.parallel_config.sequence_parallel.factor)
+ logger.info(f"spatial input after padding: {spatial.shape}")
+
+ return spatial, N
+
+ def preprocess_spatial_input(self, spatial):
+ spatial, N = self.preprocess_spatial_input_host(spatial)
+ spatial = bf16_tensor(
+ spatial, device=self.mesh_device, mesh_axis=self.parallel_config.sequence_parallel.mesh_axis, shard_dim=-2
+ )
+ logger.info(f"TT spatial shape: {spatial.shape}")
+ return spatial, N
+
+ def postprocess_spatial_output_host(self, spatial_1BND, F, H, W, N):
+ """
+ This is the reverse of preprocess_spatial_input
+ Input is of shape: 1 B (patch_F patch_H patch_W) (pF pH pW C)
+ returns shape: B C F H W
+ """
+ assert len(spatial_1BND.shape) == 4
+ assert spatial_1BND.shape[0] == 1
+ B = spatial_1BND.shape[1]
+ pF, pH, pW = self.patch_size
+ patch_F, patch_H, patch_W = F // pF, H // pH, W // pW
+ logger.info(f"Postprocessing spatial output with shape {spatial_1BND.shape}")
+ spatial_BND = spatial_1BND.squeeze(0)
+
+ spatial_BND = spatial_BND[:, :N] # Slice out sequence-parallel padding tokens
+ logger.info(f"Spatial output after slicing: {spatial_BND.shape}")
+
+ spatial_patches = spatial_BND.reshape(B, patch_F, patch_H, patch_W, pF, pH, pW, self.out_channels)
+ logger.info(f"Spatial output after reshaping: {spatial_patches.shape}")
+
+ spatial_BCFHW = spatial_patches.permute(0, 7, 1, 4, 2, 5, 3, 6).reshape(B, self.out_channels, F, H, W)
+ logger.info(f"Spatial output after permuting: {spatial_BCFHW.shape}")
+ return spatial_BCFHW
+
+ def postprocess_spatial_output(self, spatial_1BND, F, H, W, N):
+ """
+ This is the reverse of preprocess_spatial_input
+ Input is of shape: 1 B (patch_F patch_H patch_W) (pF pH pW C)
+ returns shape: B C F H W
+ """
+ assert len(spatial_1BND.shape) == 4
+ assert spatial_1BND.shape[0] == 1
+ # Gather sequence-parallel output
+ spatial_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_1BND, dim=2, mesh_axis=self.parallel_config.sequence_parallel.mesh_axis
+ )
+
+ spatial_1BND = ttnn.to_torch(ttnn.get_device_tensors(spatial_1BND)[0])
+
+ spatial_BCFHW = self.postprocess_spatial_output_host(spatial_1BND, F, H, W, N)
+ return spatial_BCFHW
+
+ def forward(
+ self,
+ spatial: torch.Tensor,
+ prompt: ttnn.Tensor,
+ timestep: torch.Tensor,
+ y: torch.Tensor | None = None,
+ ) -> torch.Tensor:
+ """
+ Inputs are all torch tensors
+ y is an optional argument for image-to-video generation.
+ We assume that preprocessing has already been done for y and y has the same shape as spatial.
+ Output is torch tensor
+ """
+
+ if self.model_type == "i2v":
+ assert y is not None, "y must be provided for image-to-video generation"
+
+ B, C, F, H, W = spatial.shape
+ pF, pH, pW = self.patch_size
+ patch_F, patch_H, patch_W = F // pF, H // pH, W // pW
+ N = patch_F * patch_H * patch_W
+
+ rope_cos_1HND, rope_sin_1HND, trans_mat = self.get_rope_features(spatial)
+
+ temb_11BD, timestep_proj_1BTD, prompt_1BLP = self.prepare_conditioning(timestep, prompt)
+
+ # Concatenate spatial and y along the channel dimension
+ if self.model_type == "i2v":
+ print(spatial.shape, y.shape)
+ spatial = torch.cat([spatial, y], dim=1)
+
+ spatial_1BNI, N = self.preprocess_spatial_input(spatial)
+
+ spatial_1BND = self.patch_embedding(spatial_1BNI)
+
+ for idx, block in enumerate(self.blocks):
+ spatial_1BND = block(
+ spatial_1BND=spatial_1BND,
+ prompt_1BLP=prompt_1BLP,
+ temb_1BTD=timestep_proj_1BTD,
+ N=N,
+ rope_cos=rope_cos_1HND,
+ rope_sin=rope_sin_1HND,
+ trans_mat=trans_mat,
+ )
+
+ scale_shift_1BSD = self.scale_shift_table.data + temb_11BD
+ shift_11BD, scale_11BD = ttnn.chunk(scale_shift_1BSD, 2, -2)
+
+ spatial_norm_1BND = self.norm_out(
+ spatial_1BND, dynamic_weight=(1 + scale_11BD), dynamic_bias=shift_11BD, dtype=ttnn.float32
+ )
+
+ if self.parallel_config.tensor_parallel.factor > 1:
+ spatial_norm_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_norm_1BND, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+
+ proj_out_1BNI = self.proj_out(
+ spatial_norm_1BND, compute_kernel_config=self.hifi4_compute_kernel_config, dtype=ttnn.float32
+ )
+
+ spatial_out = self.postprocess_spatial_output(proj_out_1BNI, F, H, W, N)
+
+ return spatial_out
+
+ def inner_step(
+ self, spatial_1BNI, prompt_1BLP, rope_cos_1HND, rope_sin_1HND, trans_mat, N, timestep, gather_output=True
+ ):
+ """
+ Reduced forward function which assumes outer loop has cached certain inputs that are step independent:
+ - prompt_1BLP
+ - rope_cos_1HND
+ - rope_sin_1HND
+ - trans_mat
+ - N
+
+ Spatial input is a tensor with layout `1 B (patch_F patch_H patch_W) (pF pH pW C)`.
+ Spatial output is an fp32 ttnn.Tensor on device with same layout.
+ """
+ temb_11BD, timestep_proj_1BTD = self.prepare_timestep_conditioning(timestep)
+
+ spatial_1BND = self.patch_embedding(spatial_1BNI)
+
+ for block in self.blocks:
+ spatial_1BND = block(
+ spatial_1BND=spatial_1BND,
+ prompt_1BLP=prompt_1BLP,
+ temb_1BTD=timestep_proj_1BTD,
+ N=N,
+ rope_cos=rope_cos_1HND,
+ rope_sin=rope_sin_1HND,
+ trans_mat=trans_mat,
+ )
+ scale_shift_1BSD = self.scale_shift_table.data + temb_11BD
+ shift_11BD, scale_11BD = ttnn.chunk(scale_shift_1BSD, 2, -2)
+
+ spatial_norm_1BND = self.norm_out(
+ spatial_1BND, dynamic_weight=(1 + scale_11BD), dynamic_bias=shift_11BD, dtype=ttnn.float32
+ )
+
+ if self.parallel_config.tensor_parallel.factor > 1:
+ spatial_norm_1BND = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_norm_1BND, dim=3, mesh_axis=self.parallel_config.tensor_parallel.mesh_axis
+ )
+
+ spatial_1BNI = self.proj_out(
+ spatial_norm_1BND, compute_kernel_config=self.hifi4_compute_kernel_config, dtype=self.output_dtype
+ )
+
+ # Gather fp32 spatial output across sequence parallel devices (remains on device)
+ if gather_output:
+ spatial_1BNI = self.ccl_manager.all_gather_persistent_buffer(
+ spatial_1BNI, dim=2, mesh_axis=self.parallel_config.sequence_parallel.mesh_axis
+ )
+
+ return spatial_1BNI
+
+ # Prep run is False because we warmup the entire pipeline first. Remove if this is not desired.
+ @traced_function(device=lambda self: self.mesh_device, clone_prep_inputs=False, prep_run=False)
+ def combined_step(
+ self,
+ do_classifier_free_guidance: bool,
+ spatial_1BNI: ttnn.Tensor,
+ prompt_1BLP: ttnn.Tensor,
+ negative_prompt_1BLP: ttnn.Tensor,
+ N: int,
+ rope_cos_1HND: ttnn.Tensor,
+ rope_sin_1HND: ttnn.Tensor,
+ trans_mat: ttnn.Tensor,
+ timestep: ttnn.Tensor,
+ guidance_scale: ttnn.Tensor,
+ *,
+ gather_output: bool = True,
+ ) -> ttnn.Tensor:
+ cond = self.inner_step(
+ spatial_1BNI,
+ prompt_1BLP,
+ rope_cos_1HND,
+ rope_sin_1HND,
+ trans_mat,
+ N,
+ timestep,
+ gather_output=gather_output,
+ )
+ if not do_classifier_free_guidance:
+ return cond
+
+ uncond = self.inner_step(
+ spatial_1BNI,
+ negative_prompt_1BLP,
+ rope_cos_1HND,
+ rope_sin_1HND,
+ trans_mat,
+ N,
+ timestep,
+ gather_output=gather_output,
+ )
+
+ combined = ttnn.lerp(uncond, cond, guidance_scale)
+
+ return combined
+
+
+class WanCheckpoint:
+ """A Wan transformer-subfolder checkpoint: fetches weights and builds loaded transformers."""
+
+ def __init__(self, name: str, subfolder: str) -> None:
+ self._name = name
+ self._subfolder = subfolder
+ torch_transformer = TorchWanTransformer3DModel.from_pretrained(
+ name,
+ subfolder=subfolder,
+ trust_remote_code=True,
+ )
+ torch_transformer.eval()
+ self._config = torch_transformer.config
+ self._state_dict = torch_transformer.state_dict()
+
+ @property
+ def subfolder(self) -> str:
+ return self._subfolder
+
+ def state_dict(self) -> dict[str, torch.Tensor]:
+ return dict(self._state_dict)
+
+ def build(
+ self,
+ *,
+ ccl_manager: CCLManager,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool,
+ model_type: str,
+ lora_enabled: bool = False,
+ ) -> WanTransformer3DModel:
+ """Construct a ``WanTransformer3DModel`` for this checkpoint (weights NOT loaded).
+
+ Loading is deferred so the caller can manage the lifecycle (deallocate / reload).
+ """
+ c = self._config
+ return WanTransformer3DModel(
+ patch_size=c.patch_size,
+ num_heads=c.num_attention_heads,
+ dim=c.num_attention_heads * c.attention_head_dim,
+ in_channels=c.in_channels,
+ out_channels=c.out_channels,
+ text_dim=c.text_dim,
+ freq_dim=c.freq_dim,
+ ffn_dim=c.ffn_dim,
+ cross_attn_norm=c.cross_attn_norm,
+ eps=c.eps,
+ rope_max_seq_len=c.rope_max_seq_len,
+ mesh_device=ccl_manager.mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ model_type=model_type,
+ lora_enabled=lora_enabled,
+ )
+
+ def load(
+ self,
+ model: WanTransformer3DModel,
+ *,
+ mesh_device: ttnn.MeshDevice,
+ parallel_config: DiTParallelConfig,
+ is_fsdp: bool,
+ ) -> None:
+ """Load (or reload) weights for a previously-built transformer."""
+ cache.load_model(
+ tt_model=model,
+ get_torch_state_dict=lambda: self._state_dict,
+ model_name=os.path.basename(self._name),
+ subfolder=self._subfolder,
+ parallel_config=parallel_config,
+ mesh_shape=tuple(mesh_device.shape),
+ mesh_device=mesh_device,
+ is_fsdp=is_fsdp,
+ )
diff --git a/code/models/tt_dit/tests/dataset_eval/clip_encoder.py b/code/models/tt_dit/tests/dataset_eval/clip_encoder.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b082724bf02005f851e1b47af88e9e18ef34699
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/clip_encoder.py
@@ -0,0 +1,104 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+from typing import List, Optional, Union
+
+import open_clip
+import torch
+import torch.nn as nn
+from PIL import Image
+
+
+class CLIPEncoder(nn.Module):
+ """
+ A class for encoding images and texts using a specified CLIP model and computing the similarity between them.
+
+ Attributes:
+ -----------
+ clip_version: str
+ The version of the CLIP model to be used.
+ pretrained: str
+ The pre-trained weights to load.
+ model: nn.Module
+ The CLIP model.
+ preprocess: Callable
+ The preprocessing transform to apply to the input image.
+ device: str
+ The device to which the model is moved.
+ """
+
+ def __init__(
+ self,
+ clip_version: str = "ViT-B/32",
+ pretrained: Optional[str] = "",
+ cache_dir: Optional[str] = None,
+ ):
+ """
+ Initializes the CLIPEncoder with the specified CLIP model version and pre-trained weights.
+
+ Parameters:
+ -----------
+ clip_version: str, optional
+ The version of the CLIP model to be used. Defaults to 'ViT-B/32'.
+ pretrained: str, optional
+ The pre-trained weights to load. If not provided, it defaults based on clip_version.
+ cache_dir: str, optional
+ The directory to cache the model. Defaults to None.
+ device: str, optional
+ The device to which the model is moved. Defaults to 'cuda'.
+ """
+ super().__init__()
+
+ self.clip_version = clip_version
+ self.pretrained = pretrained if pretrained else self._get_default_pretrained()
+
+ self.model, _, self.preprocess = open_clip.create_model_and_transforms(
+ self.clip_version, pretrained=self.pretrained, cache_dir=cache_dir
+ )
+
+ self.model.eval()
+
+ def _get_default_pretrained(self) -> str:
+ """Returns the default pretrained weights based on the clip_version."""
+ return "openai"
+
+ @torch.no_grad()
+ def get_clip_score(self, text: Union[str, List[str]], image: Union[Image.Image, torch.Tensor]) -> torch.Tensor:
+ """
+ Computes the similarity score between the given text(s) and image using the CLIP model.
+
+ Parameters:
+ -----------
+ text: Union[str, List[str]]
+ The text or list of texts to compare with the image.
+ image: Image.Image
+ The input image.
+
+ Returns:
+ --------
+ torch.Tensor
+ The similarity score between the text(s) and image.
+ """
+ # Preprocess the image and move it to the specified device
+ image = self.preprocess(image).unsqueeze(0)
+
+ # Normalize the image features
+ image_features = self.model.encode_image(image).float()
+ image_features /= image_features.norm(dim=-1, keepdim=True)
+
+ # If a single text string is provided, convert it to a list
+ if not isinstance(text, (list, tuple)):
+ text = [text]
+
+ # Tokenize the text and move it to the specified device
+ text = open_clip.tokenize(text)
+
+ # Normalize the text features
+ text_features = self.model.encode_text(text).float()
+ text_features /= text_features.norm(dim=-1, keepdim=True)
+
+ # Compute the similarity between the image and text features
+ similarity = image_features @ text_features.T
+
+ return similarity
diff --git a/code/models/tt_dit/tests/dataset_eval/conftest.py b/code/models/tt_dit/tests/dataset_eval/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c325baff574b41eacb15aa3493e34e23fe3bea4
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/conftest.py
@@ -0,0 +1,208 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+import pytest
+
+import ttnn
+
+from ...pipelines.flux1.pipeline_flux1 import Flux1Pipeline
+from ...pipelines.mochi.pipeline_mochi import MochiPipeline
+from ...pipelines.motif.pipeline_motif import MotifPipeline
+from ...pipelines.stable_diffusion_35_large.pipeline_stable_diffusion_35_large import StableDiffusion3Pipeline
+from ...pipelines.wan.pipeline_wan import WanPipeline
+
+targets_setup = {
+ "stable-diffusion-3.5-large": {
+ "hf_id": "stabilityai/stable-diffusion-3.5-large",
+ "num_inference_steps": 28,
+ "pipeline": StableDiffusion3Pipeline,
+ "device_params": {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "l1_small_size": 32768,
+ "trace_region_size": 25000000,
+ },
+ "accuracy": {
+ "5000": {"fid_valid_range": [22.63592, 24.0361780112], "clip_valid_range": [30.38525, 32.264881565]},
+ "500": {"fid_valid_range": [79.734, 84.66634524], "clip_valid_range": [30.423468, 32.30546373048]},
+ "100": {
+ "fid_valid_range": [175.35951, 186.2072492886],
+ "clip_valid_range": [30.162635, 32.028495601100005],
+ },
+ },
+ },
+ "flux.1-dev": {
+ "hf_id": "black-forest-labs/FLUX.1-dev",
+ "num_inference_steps": 28,
+ "pipeline": Flux1Pipeline,
+ "device_params": {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "l1_small_size": 32768,
+ "trace_region_size": 50000000,
+ },
+ "accuracy": {
+ "5000": {"fid_valid_range": [28.14067, 29.8814518462], "clip_valid_range": [29.7014, 31.538728604]},
+ "500": {"fid_valid_range": [82.37143, 87.46692665980001], "clip_valid_range": [29.79258, 31.6355489988]},
+ "100": {"fid_valid_range": [174.97054, 185.79421760440002], "clip_valid_range": [29.86339, 31.7107393054]},
+ },
+ },
+ "flux.1-schnell": {
+ "hf_id": "black-forest-labs/FLUX.1-schnell",
+ "num_inference_steps": 4,
+ "pipeline": Flux1Pipeline,
+ "device_params": {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "l1_small_size": 32768,
+ "trace_region_size": 50000000,
+ },
+ "accuracy": {
+ "5000": {
+ "fid_valid_range": [22.195637, 23.568659104820004],
+ "clip_valid_range": [30.010539, 31.866990942540003],
+ },
+ "500": {"fid_valid_range": [77.21976, 81.9965743536], "clip_valid_range": [30.19901, 32.067120758600005]},
+ "100": {"fid_valid_range": [171.065611, 181.64772969646], "clip_valid_range": [30.41629, 32.2978416994]},
+ },
+ },
+ "motif-image-6b-preview": {
+ "hf_id": "Motif-Technologies/Motif-Image-6B-Preview",
+ "num_inference_steps": 28,
+ "pipeline": MotifPipeline,
+ "device_params": {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "l1_small_size": 32768,
+ "trace_region_size": 31000000,
+ },
+ "accuracy": {
+ "5000": {"fid_valid_range": [33.85203, 35.9461165758], "clip_valid_range": [29.6529, 31.487228394]},
+ "500": {"fid_valid_range": [86.02833, 91.3500424938], "clip_valid_range": [29.65581, 31.4903184066]},
+ "100": {"fid_valid_range": [180.97872, 192.1740636192], "clip_valid_range": [29.65581, 31.4903184066]},
+ },
+ },
+ "mochi-1-preview": {
+ "hf_id": "genmo/mochi-1-preview",
+ "num_inference_steps": 50,
+ "pipeline": MochiPipeline,
+ "device_params": {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ },
+ "accuracy": {"16": {"clip_valid_range": [26.20977, 27.83099]}},
+ "is_video": True,
+ "extra_args": {"num_frames": 168},
+ },
+ "wan2.2": {
+ "hf_id": "Wan-AI/Wan2.2-T2V-A14B-Diffusers",
+ "num_inference_steps": 40,
+ "pipeline": WanPipeline,
+ "device_params": {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ },
+ "accuracy": {"16": {"clip_valid_range": [28.25276, 30.00036]}},
+ "is_video": True,
+ "extra_args": {"output_type": "pil"},
+ },
+}
+
+
+def pytest_addoption(parser):
+ parser.addoption(
+ "--start-from",
+ action="store",
+ default=0,
+ help="Start from prompt number (0-4999)",
+ )
+ parser.addoption(
+ "--num-prompts",
+ action="store",
+ default=5000,
+ help="Number of prompts to process (default: 5000)",
+ )
+
+ model_id_choices = targets_setup.keys()
+ parser.addoption(
+ "--model-id",
+ action="store",
+ required=True,
+ choices=model_id_choices,
+ help=f"Model ID to use for evaluation. Options:{','.join(model_id_choices)}",
+ )
+
+ parser.addoption(
+ "--num-inference-steps",
+ action="store",
+ default=None,
+ type=int,
+ help="Number of inference steps (default: 28)",
+ )
+
+ parser.addoption(
+ "--24hrs-test",
+ action="store_true",
+ default=False,
+ help="Whether to run a 24-hour test",
+ )
+
+
+@pytest.fixture
+def evaluation_range(request):
+ start_from = request.config.getoption("--start-from")
+ num_prompts = request.config.getoption("--num-prompts")
+ if start_from is not None:
+ start_from = int(start_from)
+ else:
+ start_from = 0
+ if num_prompts is not None:
+ num_prompts = int(num_prompts)
+ else:
+ num_prompts = 5
+ return start_from, num_prompts
+
+
+@pytest.fixture
+def model_id(request):
+ return request.config.getoption("--model-id")
+
+
+@pytest.fixture
+def is_24hrs_test(request):
+ return request.config.getoption("--24hrs-test")
+
+
+@pytest.fixture
+def num_inference_steps(request):
+ return (
+ request.config.getoption("--num-inference-steps")
+ or targets_setup[request.config.getoption("--model-id")]["num_inference_steps"]
+ )
+
+
+@pytest.fixture
+def dit_pipeline(model_id):
+ return targets_setup[model_id]["pipeline"]
+
+
+@pytest.fixture
+def model_metadata(model_id):
+ return targets_setup[model_id]
+
+
+@pytest.fixture
+def device_params(request, model_id):
+ """Return device_params based on model_id, with optional parametrization override."""
+
+ # If parametrized (indirect=True), use that value as base
+ params = getattr(request, "param", {}).copy()
+
+ # If no parametrization, use model-specific params
+ if not params:
+ params = targets_setup[model_id]["device_params"]
+ # If parametrized, merge with model-specific (parametrized takes precedence)
+ else:
+ model_defaults = targets_setup[model_id]["device_params"]
+ params = {**model_defaults, **params} # Parametrized overrides model defaults
+
+ return params
+
+
+@pytest.fixture
+def is_video(model_id):
+ return targets_setup[model_id].get("is_video", False)
diff --git a/code/models/tt_dit/tests/dataset_eval/fid_score.py b/code/models/tt_dit/tests/dataset_eval/fid_score.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f210c8e3257f3cc973063736b2b5179182af7e5
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/fid_score.py
@@ -0,0 +1,214 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import os
+import urllib
+
+import numpy as np
+import torch
+from loguru import logger
+from PIL import Image
+from scipy import linalg
+from threadpoolctl import threadpool_limits
+from torchvision import transforms as TF
+
+from .inception import InceptionV3
+
+COCO_STATISTICS_DOWNLOAD_PATH = "https://github.com/mlcommons/inference/raw/4b1d1156c23965172ae56eacdd8372f8897eb771/text_to_image/tools/val2014.npz"
+
+
+def get_activations(files, model, batch_size=1, dims=2048, device="cpu", num_workers=1):
+ """Calculates the activations of the pool_3 layer for all images.
+
+ Params:
+ -- files : List of image files paths
+ -- model : Instance of inception model
+ -- batch_size : Batch size of images for the model to process at once.
+ Make sure that the number of samples is a multiple of
+ the batch size, otherwise some samples are ignored. This
+ behavior is retained to match the original FID score
+ implementation.
+ -- dims : Dimensionality of features returned by Inception
+ -- device : Device to run calculations
+ -- num_workers : Number of parallel dataloader workers
+
+ Returns:
+ -- A numpy array of dimension (num images, dims) that contains the
+ activations of the given tensor when feeding inception with the
+ query tensor.
+ """
+ model.eval()
+
+ if batch_size > len(files):
+ logger.warning("Warning: batch size is bigger than the data size. Setting batch size to data size")
+ batch_size = len(files)
+
+ dataset = ImagesDataset(files, transforms=TF.ToTensor())
+ dataloader = torch.utils.data.DataLoader(
+ dataset,
+ batch_size=batch_size,
+ num_workers=num_workers,
+ )
+
+ pred_arr = np.empty((len(files), dims))
+
+ start_idx = 0
+
+ for batch in dataloader:
+ batch = batch.to(device)
+
+ with torch.no_grad():
+ pred = model(batch)[0]
+
+ pred = pred.squeeze(3).squeeze(2).cpu().numpy()
+
+ pred_arr[start_idx : start_idx + pred.shape[0]] = pred
+
+ start_idx = start_idx + pred.shape[0]
+
+ return pred_arr
+
+
+def calculate_activation_statistics(files, model):
+ """Calculation of the statistics used by the FID.
+ Params:
+ -- files : List of images
+ -- model : Instance of inception model
+
+ Returns:
+ -- mu : The mean over samples of the activations of the pool_3 layer of
+ the inception model.
+ -- sigma : The covariance matrix of the activations of the pool_3 layer of
+ the inception model.
+ """
+ act = get_activations(files, model)
+ mu = np.mean(act, axis=0)
+ sigma = np.cov(act, rowvar=False)
+ return mu, sigma
+
+
+def compute_statistics_of_path(path):
+ if not os.path.isfile(path):
+ logger.info(f"File {path} not found. Downloading...")
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ urllib.request.urlretrieve(COCO_STATISTICS_DOWNLOAD_PATH, path)
+ logger.info("Download complete.")
+
+ with np.load(path) as f:
+ m, s = f["mu"][:], f["sigma"][:]
+
+ return m, s
+
+
+def calculate_frechet_distance(mu1, sigma1, mu2, sigma2, eps=1e-6):
+ """Numpy implementation of the Frechet Distance.
+ The Frechet distance between two multivariate Gaussians X_1 ~ N(mu_1, C_1)
+ and X_2 ~ N(mu_2, C_2) is
+ d^2 = ||mu_1 - mu_2||^2 + Tr(C_1 + C_2 - 2*sqrt(C_1*C_2)).
+
+ Stable version by Dougal J. Sutherland.
+
+ Params:
+ -- mu1 : Numpy array containing the activations of a layer of the
+ inception net (like returned by the function 'get_predictions')
+ for generated samples.
+ -- mu2 : The sample mean over activations, precalculated on an
+ representative data set.
+ -- sigma1: The covariance matrix over activations for generated samples.
+ -- sigma2: The covariance matrix over activations, precalculated on an
+ representative data set.
+
+ Returns:
+ -- : The Frechet Distance.
+ """
+
+ mu1 = np.atleast_1d(mu1)
+ mu2 = np.atleast_1d(mu2)
+
+ sigma1 = np.atleast_2d(sigma1)
+ sigma2 = np.atleast_2d(sigma2)
+
+ assert mu1.shape == mu2.shape, "Training and test mean vectors have different lengths"
+ assert sigma1.shape == sigma2.shape, "Training and test covariances have different dimensions"
+
+ diff = mu1 - mu2
+
+ # save sigma matrices in case of hangs
+ np.save("sigma1.npy", sigma1.astype(np.float32))
+ np.save("sigma2.npy", sigma2.astype(np.float32))
+ logger.info("Saved sigma1.npy and sigma2.npy for manual continuation.")
+
+ # ensure float32 to avoid massive slowdowns
+ sigma1 = sigma1.astype(np.float32)
+ sigma2 = sigma2.astype(np.float32)
+
+ # compute dot product
+ logger.info("calculating dot")
+ s_dot_s = sigma1 @ sigma2
+
+ # compute matrix square root with threadpool limit
+ logger.info("calculating sqrtm...")
+ with threadpool_limits(limits=1): # limit mkl/openblas threads only here
+ covmean, _ = linalg.sqrtm(s_dot_s, disp=False)
+ logger.info("done sqrtm")
+
+ # handle numerical issues
+ if not np.isfinite(covmean).all():
+ logger.info(f"fid calculation produces singular product; adding {eps} to diagonal of cov estimates")
+ offset = np.eye(sigma1.shape[0], dtype=np.float32) * eps
+ with threadpool_limits(limits=1):
+ covmean, _ = linalg.sqrtm((sigma1 + offset) @ (sigma2 + offset), disp=False)
+
+ # handle imaginary parts due to sqrtm precision issues
+ if np.iscomplexobj(covmean):
+ if not np.allclose(np.diagonal(covmean).imag, 0, atol=1e-3):
+ m = np.max(np.abs(covmean.imag))
+ raise ValueError(f"Imaginary component {m}")
+ covmean = covmean.real
+
+ covmean = covmean.astype(np.float64)
+ tr_covmean = np.trace(covmean)
+
+ return diff.dot(diff) + np.trace(sigma1) + np.trace(sigma2) - 2 * tr_covmean
+
+
+def calculate_fid_score(images, coco_statistics_path, inception_dims=2048):
+ """Calculates the activations of the pool_3 layer for all images.
+
+ Params:
+ -- images : List of images
+ -- coco_statistics_path : Path to the precomputed statistics of the COCO dataset
+ -- inception_dims : Dimensionality of features returned by Inception
+ """
+ block_idx = InceptionV3.BLOCK_INDEX_BY_DIM[inception_dims]
+
+ model = InceptionV3([block_idx])
+ model.eval()
+
+ m1, s1 = compute_statistics_of_path(coco_statistics_path)
+
+ m2, s2 = calculate_activation_statistics(images, model)
+
+ # Ensure dimensions match before calculating FID
+ assert s1.shape == s2.shape, f"Covariance shapes mismatch: {s1.shape} vs {s2.shape}"
+ assert m1.shape == m2.shape, f"Mean shapes mismatch: {m1.shape} vs {m2.shape}"
+
+ return calculate_frechet_distance(m1, s1, m2, s2)
+
+
+class ImagesDataset(torch.utils.data.Dataset):
+ def __init__(self, imgs, transforms=None):
+ self.imgs = imgs
+ self.transforms = transforms
+
+ def __len__(self):
+ return len(self.imgs)
+
+ def __getitem__(self, i):
+ img = self.imgs[i]
+ if not isinstance(img, Image.Image):
+ raise TypeError(f"Unsupported image type: {type(img)}")
+ if self.transforms is not None:
+ img = self.transforms(img)
+ return img
diff --git a/code/models/tt_dit/tests/dataset_eval/inception.py b/code/models/tt_dit/tests/dataset_eval/inception.py
new file mode 100644
index 0000000000000000000000000000000000000000..3cde165db84bf2e87de0ac556368bcffeab635b6
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/inception.py
@@ -0,0 +1,341 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import torchvision
+
+try:
+ from torchvision.models.utils import load_state_dict_from_url
+except ImportError:
+ from torch.utils.model_zoo import load_url as load_state_dict_from_url
+
+# Inception weights ported to Pytorch from
+FID_WEIGHTS_URL = (
+ "https://github.com/mseitzer/pytorch-fid/releases/download/fid_weights/pt_inception-2015-12-05-6726825d.pth"
+)
+
+
+class InceptionV3(nn.Module):
+ """Pretrained InceptionV3 network returning feature maps"""
+
+ # Index of default block of inception to return,
+ # corresponds to output of final average pooling
+ DEFAULT_BLOCK_INDEX = 3
+
+ # Maps feature dimensionality to their output blocks indices
+ BLOCK_INDEX_BY_DIM = {
+ 64: 0, # First max pooling features
+ 192: 1, # Second max pooling features
+ 768: 2, # Pre-aux classifier features
+ 2048: 3, # Final average pooling features
+ }
+
+ def __init__(
+ self,
+ output_blocks=(DEFAULT_BLOCK_INDEX,),
+ resize_input=True,
+ normalize_input=True,
+ requires_grad=False,
+ use_fid_inception=True,
+ ):
+ """Build pretrained InceptionV3
+
+ Parameters
+ ----------
+ output_blocks : list of int
+ Indices of blocks to return features of. Possible values are:
+ - 0: corresponds to output of first max pooling
+ - 1: corresponds to output of second max pooling
+ - 2: corresponds to output which is fed to aux classifier
+ - 3: corresponds to output of final average pooling
+ resize_input : bool
+ If true, bilinearly resizes input to width and height 299 before
+ feeding input to model. As the network without fully connected
+ layers is fully convolutional, it should be able to handle inputs
+ of arbitrary size, so resizing might not be strictly needed
+ normalize_input : bool
+ If true, scales the input from range (0, 1) to the range the
+ pretrained Inception network expects, namely (-1, 1)
+ requires_grad : bool
+ If true, parameters of the model require gradients. Possibly useful
+ for finetuning the network
+ use_fid_inception : bool
+ If true, uses the pretrained Inception model used in Tensorflow's
+ FID implementation. If false, uses the pretrained Inception model
+ available in torchvision. The FID Inception model has different
+ weights and a slightly different structure from torchvision's
+ Inception model. If you want to compute FID scores, you are
+ strongly advised to set this parameter to true to get comparable
+ results.
+ """
+ super(InceptionV3, self).__init__()
+
+ self.resize_input = resize_input
+ self.normalize_input = normalize_input
+ self.output_blocks = sorted(output_blocks)
+ self.last_needed_block = max(output_blocks)
+
+ assert self.last_needed_block <= 3, "Last possible output block index is 3"
+
+ self.blocks = nn.ModuleList()
+
+ if use_fid_inception:
+ inception = fid_inception_v3()
+ else:
+ inception = _inception_v3(weights="DEFAULT")
+
+ # Block 0: input to maxpool1
+ block0 = [
+ inception.Conv2d_1a_3x3,
+ inception.Conv2d_2a_3x3,
+ inception.Conv2d_2b_3x3,
+ nn.MaxPool2d(kernel_size=3, stride=2),
+ ]
+ self.blocks.append(nn.Sequential(*block0))
+
+ # Block 1: maxpool1 to maxpool2
+ if self.last_needed_block >= 1:
+ block1 = [
+ inception.Conv2d_3b_1x1,
+ inception.Conv2d_4a_3x3,
+ nn.MaxPool2d(kernel_size=3, stride=2),
+ ]
+ self.blocks.append(nn.Sequential(*block1))
+
+ # Block 2: maxpool2 to aux classifier
+ if self.last_needed_block >= 2:
+ block2 = [
+ inception.Mixed_5b,
+ inception.Mixed_5c,
+ inception.Mixed_5d,
+ inception.Mixed_6a,
+ inception.Mixed_6b,
+ inception.Mixed_6c,
+ inception.Mixed_6d,
+ inception.Mixed_6e,
+ ]
+ self.blocks.append(nn.Sequential(*block2))
+
+ # Block 3: aux classifier to final avgpool
+ if self.last_needed_block >= 3:
+ block3 = [
+ inception.Mixed_7a,
+ inception.Mixed_7b,
+ inception.Mixed_7c,
+ nn.AdaptiveAvgPool2d(output_size=(1, 1)),
+ ]
+ self.blocks.append(nn.Sequential(*block3))
+
+ for param in self.parameters():
+ param.requires_grad = requires_grad
+
+ def forward(self, inp):
+ """Get Inception feature maps
+
+ Parameters
+ ----------
+ inp : torch.autograd.Variable
+ Input tensor of shape Bx3xHxW. Values are expected to be in
+ range (0, 1)
+
+ Returns
+ -------
+ List of torch.autograd.Variable, corresponding to the selected output
+ block, sorted ascending by index
+ """
+ outp = []
+ x = inp
+
+ if self.resize_input:
+ x = F.interpolate(x, size=(299, 299), mode="bilinear", align_corners=False)
+
+ if self.normalize_input:
+ x = 2 * x - 1 # Scale from range (0, 1) to range (-1, 1)
+
+ for idx, block in enumerate(self.blocks):
+ x = block(x)
+ if idx in self.output_blocks:
+ outp.append(x)
+
+ if idx == self.last_needed_block:
+ break
+
+ return outp
+
+
+def _inception_v3(*args, **kwargs):
+ """Wraps `torchvision.models.inception_v3`"""
+ try:
+ version = tuple(map(int, torchvision.__version__.split(".")[:2]))
+ except ValueError:
+ # Just a caution against weird version strings
+ version = (0,)
+
+ # Skips default weight initialization if supported by torchvision
+ # version. See https://github.com/mseitzer/pytorch-fid/issues/28.
+ if version >= (0, 6):
+ kwargs["init_weights"] = False
+
+ # Backwards compatibility: `weights` argument was handled by `pretrained`
+ # argument prior to version 0.13.
+ if version < (0, 13) and "weights" in kwargs:
+ if kwargs["weights"] == "DEFAULT":
+ kwargs["pretrained"] = True
+ elif kwargs["weights"] is None:
+ kwargs["pretrained"] = False
+ else:
+ raise ValueError(
+ "weights=={} not supported in torchvision {}".format(kwargs["weights"], torchvision.__version__)
+ )
+ del kwargs["weights"]
+
+ return torchvision.models.inception_v3(*args, **kwargs)
+
+
+def fid_inception_v3():
+ """Build pretrained Inception model for FID computation
+
+ The Inception model for FID computation uses a different set of weights
+ and has a slightly different structure than torchvision's Inception.
+
+ This method first constructs torchvision's Inception and then patches the
+ necessary parts that are different in the FID Inception model.
+ """
+ inception = _inception_v3(num_classes=1008, aux_logits=False, weights=None)
+ inception.Mixed_5b = FIDInceptionA(192, pool_features=32)
+ inception.Mixed_5c = FIDInceptionA(256, pool_features=64)
+ inception.Mixed_5d = FIDInceptionA(288, pool_features=64)
+ inception.Mixed_6b = FIDInceptionC(768, channels_7x7=128)
+ inception.Mixed_6c = FIDInceptionC(768, channels_7x7=160)
+ inception.Mixed_6d = FIDInceptionC(768, channels_7x7=160)
+ inception.Mixed_6e = FIDInceptionC(768, channels_7x7=192)
+ inception.Mixed_7b = FIDInceptionE_1(1280)
+ inception.Mixed_7c = FIDInceptionE_2(2048)
+
+ state_dict = load_state_dict_from_url(FID_WEIGHTS_URL, progress=True)
+ inception.load_state_dict(state_dict)
+ return inception
+
+
+class FIDInceptionA(torchvision.models.inception.InceptionA):
+ """InceptionA block patched for FID computation"""
+
+ def __init__(self, in_channels, pool_features):
+ super(FIDInceptionA, self).__init__(in_channels, pool_features)
+
+ def forward(self, x):
+ branch1x1 = self.branch1x1(x)
+
+ branch5x5 = self.branch5x5_1(x)
+ branch5x5 = self.branch5x5_2(branch5x5)
+
+ branch3x3dbl = self.branch3x3dbl_1(x)
+ branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)
+ branch3x3dbl = self.branch3x3dbl_3(branch3x3dbl)
+
+ # Patch: Tensorflow's average pool does not use the padded zero's in
+ # its average calculation
+ branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1, count_include_pad=False)
+ branch_pool = self.branch_pool(branch_pool)
+
+ outputs = [branch1x1, branch5x5, branch3x3dbl, branch_pool]
+ return torch.cat(outputs, 1)
+
+
+class FIDInceptionC(torchvision.models.inception.InceptionC):
+ """InceptionC block patched for FID computation"""
+
+ def __init__(self, in_channels, channels_7x7):
+ super(FIDInceptionC, self).__init__(in_channels, channels_7x7)
+
+ def forward(self, x):
+ branch1x1 = self.branch1x1(x)
+
+ branch7x7 = self.branch7x7_1(x)
+ branch7x7 = self.branch7x7_2(branch7x7)
+ branch7x7 = self.branch7x7_3(branch7x7)
+
+ branch7x7dbl = self.branch7x7dbl_1(x)
+ branch7x7dbl = self.branch7x7dbl_2(branch7x7dbl)
+ branch7x7dbl = self.branch7x7dbl_3(branch7x7dbl)
+ branch7x7dbl = self.branch7x7dbl_4(branch7x7dbl)
+ branch7x7dbl = self.branch7x7dbl_5(branch7x7dbl)
+
+ # Patch: Tensorflow's average pool does not use the padded zero's in
+ # its average calculation
+ branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1, count_include_pad=False)
+ branch_pool = self.branch_pool(branch_pool)
+
+ outputs = [branch1x1, branch7x7, branch7x7dbl, branch_pool]
+ return torch.cat(outputs, 1)
+
+
+class FIDInceptionE_1(torchvision.models.inception.InceptionE):
+ """First InceptionE block patched for FID computation"""
+
+ def __init__(self, in_channels):
+ super(FIDInceptionE_1, self).__init__(in_channels)
+
+ def forward(self, x):
+ branch1x1 = self.branch1x1(x)
+
+ branch3x3 = self.branch3x3_1(x)
+ branch3x3 = [
+ self.branch3x3_2a(branch3x3),
+ self.branch3x3_2b(branch3x3),
+ ]
+ branch3x3 = torch.cat(branch3x3, 1)
+
+ branch3x3dbl = self.branch3x3dbl_1(x)
+ branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)
+ branch3x3dbl = [
+ self.branch3x3dbl_3a(branch3x3dbl),
+ self.branch3x3dbl_3b(branch3x3dbl),
+ ]
+ branch3x3dbl = torch.cat(branch3x3dbl, 1)
+
+ # Patch: Tensorflow's average pool does not use the padded zero's in
+ # its average calculation
+ branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1, count_include_pad=False)
+ branch_pool = self.branch_pool(branch_pool)
+
+ outputs = [branch1x1, branch3x3, branch3x3dbl, branch_pool]
+ return torch.cat(outputs, 1)
+
+
+class FIDInceptionE_2(torchvision.models.inception.InceptionE):
+ """Second InceptionE block patched for FID computation"""
+
+ def __init__(self, in_channels):
+ super(FIDInceptionE_2, self).__init__(in_channels)
+
+ def forward(self, x):
+ branch1x1 = self.branch1x1(x)
+
+ branch3x3 = self.branch3x3_1(x)
+ branch3x3 = [
+ self.branch3x3_2a(branch3x3),
+ self.branch3x3_2b(branch3x3),
+ ]
+ branch3x3 = torch.cat(branch3x3, 1)
+
+ branch3x3dbl = self.branch3x3dbl_1(x)
+ branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)
+ branch3x3dbl = [
+ self.branch3x3dbl_3a(branch3x3dbl),
+ self.branch3x3dbl_3b(branch3x3dbl),
+ ]
+ branch3x3dbl = torch.cat(branch3x3dbl, 1)
+
+ # Patch: The FID Inception model uses max pooling instead of average
+ # pooling. This is likely an error in this specific Inception
+ # implementation, as other Inception models use average pooling here
+ # (which matches the description in the paper).
+ branch_pool = F.max_pool2d(x, kernel_size=3, stride=1, padding=1)
+ branch_pool = self.branch_pool(branch_pool)
+
+ outputs = [branch1x1, branch3x3, branch3x3dbl, branch_pool]
+ return torch.cat(outputs, 1)
diff --git a/code/models/tt_dit/tests/dataset_eval/reference/README.md b/code/models/tt_dit/tests/dataset_eval/reference/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..fd8da6f972369274d39ae485c8e438dfba222315
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/reference/README.md
@@ -0,0 +1,103 @@
+# Reference Implementations for Dataset Evaluation
+
+This directory contains reference implementations for evaluating models on H100 hardware. These implementations use standard diffusers pipelines and provide baseline metrics for comparison with TT-Metal implementations.
+
+## Setup
+
+1. `pip install -r requirements.txt`
+2. Download COCO captions:
+ - `wget https://github.com/mlcommons/inference/raw/4b1d1156c23965172ae56eacdd8372f8897eb771/text_to_image/coco2014/captions/captions_source.tsv`
+ - Rename file to `captions.tsv`
+3. Download COCO validation statistics:
+ - `wget https://github.com/mlcommons/inference/raw/4b1d1156c23965172ae56eacdd8372f8897eb771/text_to_image/tools/val2014.npz`
+4. Login to HuggingFace: `huggingface-cli login`
+ - Enter your token
+ - Accept license agreements as required for each model
+
+## Flux Evaluation
+
+**File**: `flux.py`
+
+**Steps**:
+
+1. Load weights locally:
+ - `pip install accelerate`
+ - ```
+ huggingface-cli download black-forest-labs/FLUX.1-schnell \
+ --repo-type model \
+ --local-dir ./flux_schnell \
+ --include "*"
+ ```
+
+2. Run eval:
+ ```
+ pytest flux.py::test_accuracy_model -v -s --start-from=0 --num-prompts=5
+ ```
+
+**Notes**:
+- Default settings: 1024x1024, guidance_scale=1.0, 4 inference steps (schnell model)
+- Results are saved to `test_reports/flux_test_results.json`
+
+## Stable Diffusion 3.5 Evaluation
+
+**File**: `sd35.py`
+
+**Steps**:
+
+1. Accept the license agreement for SD3.5 at: https://huggingface.co/stabilityai/stable-diffusion-3.5-large
+
+2. Load weights locally:
+ - `pip install accelerate`
+ - ```
+ huggingface-cli download stabilityai/stable-diffusion-3.5-large \
+ --repo-type model \
+ --local-dir ./sd35_large \
+ --include "*"
+ ```
+
+3. Run eval:
+ ```
+ pytest sd35.py::test_accuracy_sd35 -v -s --start-from=0 --num-prompts=5
+ ```
+
+**Notes**:
+- SD3.5 uses three text encoders: CLIP-L, CLIP-G, and T5-XXL
+- The T5 encoder provides enhanced text understanding and longer prompt support (up to 256 tokens)
+- Default settings: 1024x1024, guidance_scale=3.5, 40 inference steps
+- Requires ~40GB VRAM on H100 for full resolution inference
+- Results are saved to `test_reports/sd35_test_results.json`
+
+## Motif Evaluation
+
+**File**: `motif.py`
+
+**Note**: There isn't an existing diffusers pipeline for Motif yet, so this uses the official Motif inference script.
+
+**Steps**:
+
+1. Clone the Motif repository:
+ ```
+ git lfs install
+ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/Motif-Technologies/Motif-Image-6B-Preview
+ cd Motif-Image-6B-Preview
+ huggingface-cli download Motif-Technologies/Motif-Image-6B-Preview --local-dir ./ --include "checkpoints/*"
+ ```
+
+2. Download captions for eval:
+ - `wget https://github.com/mlcommons/inference/raw/4b1d1156c23965172ae56eacdd8372f8897eb771/text_to_image/coco2014/captions/captions_source.tsv`
+ - `mv captions_source.tsv captions.tsv`
+
+3. Accept the license agreement for Motif if required
+
+4. Run eval:
+ ```
+ python motif.py \
+ --images-dir outputs/seed_7777/scale_4.0 \
+ --prompts-file captions.tsv \
+ --start-from 0 \
+ --num-prompts 500
+ ```
+
+**Notes**:
+- This script uses the official Motif inference.py script to generate images
+- Results are saved to `dataset_eval_results.json`
diff --git a/code/models/tt_dit/tests/dataset_eval/reference/__init__.py b/code/models/tt_dit/tests/dataset_eval/reference/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..41cc875f93477a9c1e59a76f0c7d5bf101f6621c
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/reference/__init__.py
@@ -0,0 +1,2 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/code/models/tt_dit/tests/dataset_eval/reference/flux.py b/code/models/tt_dit/tests/dataset_eval/reference/flux.py
new file mode 100644
index 0000000000000000000000000000000000000000..aac55c621b738c80e762ce566d325561932c7624
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/reference/flux.py
@@ -0,0 +1,295 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+import csv
+import json
+
+# must be the very first lines!
+import os
+import statistics
+import time
+
+# os.environ["TRANSFORMERS_NO_TF"] = "1" # block tensorflow backend
+# os.environ["TRANSFORMERS_NO_FLAX"] = "1" # block flax/jax backend
+# os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" # silence tf if it somehow loads
+import urllib
+
+import pytest
+import torch
+from diffusers import FluxPipeline
+from loguru import logger
+
+from ..clip_encoder import CLIPEncoder
+from ..fid_score import calculate_fid_score
+
+
+class SimpleProfiler:
+ def __init__(self):
+ self.times = {}
+ self.start_times = {}
+
+ def start(self, name):
+ self.start_times[name] = time.time()
+ if name not in self.times:
+ self.times[name] = []
+
+ def end(self, name):
+ if name in self.start_times:
+ elapsed = time.time() - self.start_times[name]
+ self.times[name].append(elapsed)
+ del self.start_times[name]
+
+ def get(self, name):
+ if name in self.times and self.times[name]:
+ return sum(self.times[name]) / len(self.times[name])
+ return 0.0
+
+
+profiler = SimpleProfiler()
+
+# currently same as sdxl
+COCO_CAPTIONS_DOWNLOAD_PATH = "https://github.com/mlcommons/inference/raw/4b1d1156c23965172ae56eacdd8372f8897eb771/text_to_image/coco2014/captions/captions_source.tsv"
+OUT_ROOT, RESULTS_FILE_NAME = "test_reports", "flux_test_results.json"
+
+
+@pytest.mark.parametrize(
+ "device_params",
+ [{"device": "cuda" if torch.cuda.is_available() else "cpu"}],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "model_name, image_w, image_h, guidance_scale, num_inference_steps",
+ [
+ # ("dev", 1024, 1024, 3.5, 28), # Full resolution on H100
+ ("schnell", 1024, 1024, 1.0, 4)
+ ],
+)
+@pytest.mark.parametrize("captions_path", ["captions.tsv"])
+@pytest.mark.parametrize("coco_statistics_path", ["val2014.npz"])
+def test_accuracy_model(
+ device_params,
+ model_name,
+ image_w,
+ image_h,
+ guidance_scale,
+ num_inference_steps,
+ captions_path,
+ coco_statistics_path,
+ evaluation_range,
+):
+ start_from, num_prompts = evaluation_range
+ prompts = flux_get_prompts(captions_path, start_from, num_prompts)
+ logger.info(f"start inference from prompt index: {start_from} to {start_from + num_prompts}")
+
+ device = device_params.get("device", "cuda" if torch.cuda.is_available() else "cpu")
+ logger.info(f"Using device: {device}")
+
+ if device == "cuda":
+ logger.info(f"GPU: {torch.cuda.get_device_name(0)}")
+ logger.info(f"GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f}GB")
+
+ # Initialize Diffusers pipeline
+ model_id = "./flux_schnell"
+ logger.info(f"Loading model: {model_id}")
+
+ pipeline = FluxPipeline.from_pretrained(
+ model_id,
+ dtype=torch.bfloat16 if device == "cuda" else torch.float32, # Use bfloat16 for H100
+ use_safetensors=True,
+ )
+
+ # Move to device manually
+ pipeline = pipeline.to(device)
+ logger.info(f"Pipeline loaded on {device}")
+
+ if device == "cuda":
+ # Enable optimizations for H100
+ try:
+ pipeline.enable_attention_slicing()
+ logger.info("✓ Attention slicing enabled")
+ except Exception as e:
+ logger.warning(f"Could not enable attention slicing: {e}")
+
+ # Optional: Enable VAE slicing for memory efficiency
+ try:
+ if hasattr(pipeline, "enable_vae_slicing"):
+ pipeline.enable_vae_slicing()
+ logger.info("✓ VAE slicing enabled")
+ except Exception as e:
+ logger.warning(f"Could not enable VAE slicing: {e}")
+
+ # Enable memory efficient attention for Flux
+ try:
+ pipeline.enable_memory_efficient_attention()
+ logger.info("✓ Memory efficient attention enabled")
+ except Exception as e:
+ logger.warning(f"Could not enable memory efficient attention: {e}")
+
+ # Set generator for reproducibility
+ generator = torch.Generator(device=device).manual_seed(0)
+
+ images = []
+ total_times = []
+
+ logger.info(f"Starting generation of {len(prompts)} images at {image_w}x{image_h}...")
+
+ for i, prompt in enumerate(prompts):
+ logger.info(f"Generating image {i+1}/{len(prompts)}: {prompt[:50]}...")
+
+ start_total = time.time()
+ profiler.start("denoising_loop")
+
+ # Generate image
+ with torch.no_grad():
+ generated_images = pipeline(
+ prompt=prompt,
+ height=image_h,
+ width=image_w,
+ num_inference_steps=num_inference_steps,
+ max_sequence_length=256,
+ generator=generator,
+ output_type="pil",
+ ).images
+
+ profiler.end("denoising_loop")
+ end_total = time.time()
+
+ total_time = end_total - start_total
+ total_times.append(total_time)
+
+ images.append(generated_images[0])
+ logger.info(f"Image {i+1} completed in {total_time:.2f}s")
+
+ # Optional: Save image for inspection
+ os.makedirs("generated_images", exist_ok=True)
+ generated_images[0].save(f"generated_images/image_{i+1}.png")
+
+ # Calculate metrics
+ logger.info("Calculating CLIP scores...")
+ clip = CLIPEncoder() # Remove device parameter, it auto-detects
+ clip_scores = [100 * clip.get_clip_score(prompts[i], img).item() for i, img in enumerate(images)]
+ average_clip_score = sum(clip_scores) / len(clip_scores)
+
+ deviation_clip_score = "N/A"
+ fid_score = "N/A"
+
+ # Calculate CLIP standard deviation if we have multiple prompts
+ if num_prompts >= 2:
+ deviation_clip_score = statistics.stdev(clip_scores)
+ logger.info(f"CLIP scores individual: {[f'{score:.2f}' for score in clip_scores]}")
+
+ # Calculate FID score if we have the COCO statistics file
+ if num_prompts >= 2 and os.path.isfile(coco_statistics_path):
+ logger.info("Calculating FID score...")
+ fid_score = calculate_fid_score(images, coco_statistics_path)
+ elif num_prompts >= 2 and not os.path.isfile(coco_statistics_path):
+ logger.warning(f"FID calculation skipped: COCO stats file not found at {coco_statistics_path}")
+ logger.info("To enable FID calculation, download COCO validation statistics:")
+ logger.info("wget http://bioinf.jku.at/research/ttur/ttur_stats/fid_stats_imagenet_256_hdf5.npz -O val2014.npz")
+ elif num_prompts < 2:
+ logger.info("FID calculation requires at least 2 images")
+
+ # Performance metrics
+ average_inference_time = sum(total_times) / len(total_times) if total_times else 0
+ min_inference_time = min(total_times) if total_times else 0
+ max_inference_time = max(total_times) if total_times else 0
+
+ # Results
+ logger.info("=== RESULTS ===")
+ logger.info(f"Average CLIP Score: {average_clip_score:.2f}")
+ logger.info(f"CLIP Score Std Dev: {deviation_clip_score}")
+ logger.info(f"Average Inference Time: {average_inference_time:.2f}s")
+ logger.info(f"Min/Max Inference Time: {min_inference_time:.2f}s / {max_inference_time:.2f}s")
+ logger.info(f"FID Score: {fid_score}")
+
+ print(f"FID score: {fid_score}")
+ print(f"Average CLIP Score: {average_clip_score}")
+ print(f"Standard Deviation of CLIP Scores: {deviation_clip_score}")
+
+ # Ensure values are JSON serializable
+ clip_std_serializable = float(deviation_clip_score) if deviation_clip_score != "N/A" else "N/A"
+ fid_serializable = float(fid_score) if fid_score != "N/A" else "N/A"
+
+ data = {
+ "model": "flux",
+ "metadata": {
+ "device": device.upper(),
+ "model_name": model_name,
+ "actual_model": model_id,
+ "start_from": start_from,
+ "num_prompts": num_prompts,
+ "image_width": image_w,
+ "image_height": image_h,
+ "guidance_scale": guidance_scale,
+ "num_inference_steps": num_inference_steps,
+ "backend": "diffusers",
+ "dtype": str(torch.bfloat16 if device == "cuda" else torch.float32),
+ "optimizations": ["attention_slicing", "vae_slicing", "memory_efficient_attention"],
+ },
+ "benchmarks_summary": [
+ {
+ "device": device.upper(),
+ "model": "flux",
+ "average_denoising_time": profiler.get("denoising_loop"),
+ "average_vae_time": 0.0, # VAE time is included in denoising for diffusers
+ "average_inference_time": average_inference_time,
+ "min_inference_time": min_inference_time,
+ "max_inference_time": max_inference_time,
+ "average_clip": average_clip_score,
+ "deviation_clip": clip_std_serializable,
+ "fid_score": fid_serializable,
+ "individual_clip_scores": [float(score) for score in clip_scores], # Add individual scores
+ }
+ ],
+ }
+
+ os.makedirs(OUT_ROOT, exist_ok=True)
+ with open(f"{OUT_ROOT}/{RESULTS_FILE_NAME}", "w") as f:
+ json.dump(data, f, indent=4)
+ logger.info(f"test results saved to {OUT_ROOT}/{RESULTS_FILE_NAME}")
+
+ # Cleanup
+ if device == "cuda":
+ torch.cuda.empty_cache()
+
+ print(f"\n🎉 Test completed successfully!")
+ print(f"📊 Average CLIP Score: {average_clip_score:.2f}")
+ print(f"⚡ Average Inference Time: {average_inference_time:.2f}s")
+ print(f"🖼️ Images saved to: generated_images/")
+
+
+def flux_get_prompts(captions_path, start_from, num_prompts):
+ assert (
+ 0 <= start_from < 5000 and start_from + num_prompts <= 5000
+ ), "start_from must be between 0 and 4999, and start_from + num_prompts must not exceed 5000."
+
+ prompts = []
+ if not os.path.isfile(captions_path):
+ logger.info(f"file {captions_path} not found. downloading...")
+ os.makedirs(os.path.dirname(captions_path), exist_ok=True)
+ urllib.request.urlretrieve(COCO_CAPTIONS_DOWNLOAD_PATH, captions_path)
+ logger.info("download complete.")
+
+ with open(captions_path, "r") as tsv_file:
+ reader = csv.reader(tsv_file, delimiter="\t")
+ next(reader) # skip header
+ for index, row in enumerate(reader):
+ if index < start_from:
+ continue
+ if index >= start_from + num_prompts:
+ break
+ prompts.append(row[2])
+ return prompts
+
+
+# Fixture for device parameters
+@pytest.fixture
+def device_params(request):
+ return request.param
+
+
+# Fixture for evaluation range
+# @pytest.fixture
+# def evaluation_range():
+# # Default evaluation range - can be overridden
+# return (0, 5) # start_from=0, num_prompts=5 for better statistics
diff --git a/code/models/tt_dit/tests/dataset_eval/reference/motif.py b/code/models/tt_dit/tests/dataset_eval/reference/motif.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab16176d4074b53d8f90358148c8a0c9f37d6f66
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/reference/motif.py
@@ -0,0 +1,192 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+import argparse
+import csv
+import json
+import os
+import subprocess
+import time
+import urllib
+from pathlib import Path
+
+from loguru import logger
+from PIL import Image
+
+from ..clip_encoder import CLIPEncoder
+from ..fid_score import calculate_fid_score
+
+COCO_CAPTIONS_DOWNLOAD_PATH = (
+ "https://github.com/mlcommons/inference/raw/4b1d1156c23965172ae56eacdd8372f8897eb771/"
+ "text_to_image/coco2014/captions/captions_source.tsv"
+)
+
+
+def load_coco_prompts(captions_path, start_from=0, num_prompts=16):
+ """loads prompts from COCO captions tsv, downloading if missing"""
+ prompts = []
+ if not os.path.isfile(captions_path):
+ logger.info(f"file {captions_path} not found. downloading...")
+ urllib.request.urlretrieve(COCO_CAPTIONS_DOWNLOAD_PATH, captions_path)
+ logger.info("download complete.")
+
+ with open(captions_path, "r") as tsv_file:
+ reader = csv.reader(tsv_file, delimiter="\t")
+ next(reader) # skip header
+ for index, row in enumerate(reader):
+ if index < start_from:
+ continue
+ if index >= start_from + num_prompts:
+ break
+ if len(row) >= 3:
+ prompts.append(row[2].strip())
+ logger.info(f"loaded {len(prompts)} coco prompts from {captions_path}")
+ return prompts
+
+
+def generate_images_with_inference(prompts_file, output_dir, num_prompts):
+ """forces motif inference to regenerate only the requested number of prompts"""
+ logger.info("clearing any previous images...")
+ os.makedirs(output_dir, exist_ok=True)
+ for f in Path(output_dir).rglob("*.png"):
+ f.unlink()
+
+ # create temporary subset prompt file
+ tmp_prompts_file = os.path.join(output_dir, "subset_prompts.txt")
+ with open(prompts_file, "r") as f_in, open(tmp_prompts_file, "w") as f_out:
+ lines = [line.strip() for line in f_in if line.strip()]
+ subset = lines[:num_prompts]
+ for l in subset:
+ f_out.write(l + "\n")
+
+ logger.info(f"generating {len(subset)} images using motif inference.py ...")
+
+ cmd = [
+ "python",
+ "inference.py",
+ "--model-config",
+ "configs/mmdit_xlarge_hq.json",
+ "--model-ckpt",
+ "checkpoints/pytorch_model_fsdp.bin",
+ "--seed",
+ "7777",
+ "--steps",
+ "20",
+ "--resolution",
+ "1024",
+ "--prompt-file",
+ tmp_prompts_file, # use subset
+ "--guidance-scales",
+ "4.0",
+ "--output-dir",
+ output_dir,
+ "--batch-size",
+ "1",
+ ]
+ subprocess.run(cmd, check=True)
+ logger.info("image generation complete.")
+
+
+def compute_clip_scores(images_dir, prompts):
+ """computes average clip similarity across generated images and corresponding prompts"""
+ clip_encoder = CLIPEncoder(clip_version="ViT-B/32", pretrained="openai")
+ scores = []
+
+ # find all pngs recursively
+ all_images = sorted(Path(images_dir).rglob("*.png"))
+ if len(all_images) < len(prompts):
+ logger.warning(f"found only {len(all_images)} images, expected {len(prompts)}")
+
+ for idx, (prompt, img_path) in enumerate(zip(prompts, all_images)):
+ try:
+ image = Image.open(img_path).convert("RGB")
+ score = clip_encoder.get_clip_score(prompt, image)
+ scores.append(score.item())
+ except Exception as e:
+ logger.error(f"error processing {img_path}: {e}")
+
+ if not scores:
+ return 0.0
+
+ avg_score = sum(scores) / len(scores)
+ logger.info(f"average clip score = {avg_score:.4f} ({avg_score * 100:.2f} CLIPScore)")
+ return avg_score
+
+
+def compute_fid(images_dir, coco_stats="val2014.npz"):
+ """computes fid score against coco statistics"""
+ if not os.path.exists(coco_stats):
+ logger.info(f"COCO stats not found at {coco_stats}, downloading...")
+ os.makedirs(os.path.dirname(coco_stats) or ".", exist_ok=True)
+ urllib.request.urlretrieve(
+ "https://github.com/mlcommons/inference/raw/4b1d1156c23965172ae56eacdd8372f8897eb771/"
+ "text_to_image/tools/val2014.npz",
+ coco_stats,
+ )
+ logger.info("download complete.")
+
+ all_images = [Image.open(f).convert("RGB") for f in sorted(Path(images_dir).rglob("*.png"))]
+ if not all_images:
+ logger.error("no images found for fid computation")
+ return None
+
+ logger.info(f"computing fid over {len(all_images)} images ...")
+ fid = calculate_fid_score(all_images, coco_stats)
+ logger.info(f"fid score = {fid:.4f}")
+ return fid
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Motif Dataset Evaluation")
+ parser.add_argument("--images-dir", type=str, required=True, help="directory containing generated images")
+ parser.add_argument("--prompts-file", type=str, required=True, help="path to prompts file (.tsv or .txt)")
+ parser.add_argument("--output-json", type=str, default="dataset_eval_results.json", help="output results json")
+ parser.add_argument("--start-from", type=int, default=0, help="starting index for captions.tsv")
+ parser.add_argument("--num-prompts", type=int, default=16, help="number of prompts to evaluate")
+ args = parser.parse_args()
+
+ start_time = time.time()
+
+ # load prompts dynamically depending on file type
+ if args.prompts_file.endswith(".tsv"):
+ prompts = load_coco_prompts(args.prompts_file, args.start_from, args.num_prompts)
+ else:
+ with open(args.prompts_file, "r") as f:
+ prompts = [line.strip() for line in f if line.strip()]
+
+ # always regenerate fresh images
+ generate_images_with_inference(args.prompts_file, args.images_dir, args.num_prompts)
+
+ # automatically detect where the actual images were saved
+ def find_deepest_image_dir(root):
+ """walks the output dir and returns the first subdir containing pngs"""
+ for dirpath, _, filenames in os.walk(root):
+ if any(fn.lower().endswith(".png") for fn in filenames):
+ return dirpath
+ return root
+
+ image_root = find_deepest_image_dir(args.images_dir)
+ logger.info(f"found generated images under: {image_root}")
+
+ # compute metrics at the detected path
+ clip_score = compute_clip_scores(image_root, prompts)
+ fid_score = compute_fid(image_root, coco_stats="val2014.npz")
+ total_time = time.time() - start_time
+
+ results = {
+ "num_images": len(prompts),
+ "clip_score": clip_score,
+ "fid_score": fid_score,
+ "total_time_sec": total_time,
+ "images_dir": image_root,
+ }
+
+ with open(args.output_json, "w") as f:
+ json.dump(results, f, indent=2)
+
+ logger.info(f"saved results to {args.output_json}")
+ logger.info(json.dumps(results, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/code/models/tt_dit/tests/dataset_eval/reference/requirements.txt b/code/models/tt_dit/tests/dataset_eval/reference/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..becf4d3a83deee981ec278af3cf0e626b8d2522e
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/reference/requirements.txt
@@ -0,0 +1,16 @@
+# core dependencies
+torch
+numpy
+scipy
+loguru
+pytest
+
+# model + tokenizer
+sentencepiece
+transformers==4.46.2
+# latest dev version required for SD3.5 model architecture (qk_norm, norm_added_k, etc.)
+git+https://github.com/huggingface/diffusers.git
+open_clip_torch
+
+# vision + utilities
+Pillow>=12.1.1
diff --git a/code/models/tt_dit/tests/dataset_eval/reference/sd35.py b/code/models/tt_dit/tests/dataset_eval/reference/sd35.py
new file mode 100644
index 0000000000000000000000000000000000000000..83792c9404218f3cb462ca4205b49ab43b033761
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/reference/sd35.py
@@ -0,0 +1,291 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+import csv
+import json
+
+# must be the very first lines!
+import os
+import statistics
+import time
+import urllib
+
+import pytest
+import torch
+from diffusers import StableDiffusion3Pipeline
+from loguru import logger
+
+from ..clip_encoder import CLIPEncoder
+from ..fid_score import calculate_fid_score
+
+
+class SimpleProfiler:
+ def __init__(self):
+ self.times = {}
+ self.start_times = {}
+
+ def start(self, name):
+ self.start_times[name] = time.time()
+ if name not in self.times:
+ self.times[name] = []
+
+ def end(self, name):
+ if name in self.start_times:
+ elapsed = time.time() - self.start_times[name]
+ self.times[name].append(elapsed)
+ del self.start_times[name]
+
+ def get(self, name):
+ if name in self.times and self.times[name]:
+ return sum(self.times[name]) / len(self.times[name])
+ return 0.0
+
+
+profiler = SimpleProfiler()
+
+COCO_CAPTIONS_DOWNLOAD_PATH = "https://github.com/mlcommons/inference/raw/4b1d1156c23965172ae56eacdd8372f8897eb771/text_to_image/coco2014/captions/captions_source.tsv"
+OUT_ROOT, RESULTS_FILE_NAME = "test_reports", "sd35_test_results.json"
+
+
+@pytest.mark.parametrize(
+ "device_params",
+ [{"device": "cuda" if torch.cuda.is_available() else "cpu"}],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "model_name, image_w, image_h, guidance_scale, num_inference_steps, max_sequence_length",
+ [
+ ("stable-diffusion-3.5-large", 1024, 1024, 3.5, 40, 256),
+ ],
+)
+@pytest.mark.parametrize("captions_path", ["captions.tsv"])
+@pytest.mark.parametrize("coco_statistics_path", ["val2014.npz"])
+def test_accuracy_sd35(
+ device_params,
+ model_name,
+ image_w,
+ image_h,
+ guidance_scale,
+ num_inference_steps,
+ max_sequence_length,
+ captions_path,
+ coco_statistics_path,
+ evaluation_range,
+):
+ start_from, num_prompts = evaluation_range
+ prompts = sd35_get_prompts(captions_path, start_from, num_prompts)
+ logger.info(f"start inference from prompt index: {start_from} to {start_from + num_prompts}")
+
+ device = device_params.get("device", "cuda" if torch.cuda.is_available() else "cpu")
+ logger.info(f"Using device: {device}")
+
+ if device == "cuda":
+ logger.info(f"GPU: {torch.cuda.get_device_name(0)}")
+ logger.info(f"GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f}GB")
+
+ model_id = "./sd35_large"
+ logger.info(f"Loading model: {model_id}")
+ logger.info(f"T5 text encoder enabled with max_sequence_length={max_sequence_length}")
+
+ pipeline = StableDiffusion3Pipeline.from_pretrained(
+ model_id,
+ torch_dtype=torch.bfloat16 if device == "cuda" else torch.float32, # Use bfloat16 for H100
+ use_safetensors=True,
+ )
+
+ pipeline = pipeline.to(device)
+ logger.info(f"Pipeline loaded on {device}")
+
+ if device == "cuda":
+ # enable optimizations for H100
+ try:
+ pipeline.enable_attention_slicing()
+ logger.info("✓ Attention slicing enabled")
+ except Exception as e:
+ logger.warning(f"Could not enable attention slicing: {e}")
+
+ # enable VAE slicing for memory efficiency
+ try:
+ if hasattr(pipeline, "enable_vae_slicing"):
+ pipeline.enable_vae_slicing()
+ logger.info("✓ VAE slicing enabled")
+ except Exception as e:
+ logger.warning(f"Could not enable VAE slicing: {e}")
+
+ # set generator for reproducibility
+ generator = torch.Generator(device=device).manual_seed(0)
+
+ images = []
+ total_times = []
+
+ logger.info(f"Starting generation of {len(prompts)} images at {image_w}x{image_h}...")
+
+ for i, prompt in enumerate(prompts):
+ logger.info(f"Generating image {i+1}/{len(prompts)}: {prompt[:50]}...")
+
+ start_total = time.time()
+ profiler.start("denoising_loop")
+
+ # generate image with triple prompt (SD3.5 uses 3 text encoders)
+ with torch.no_grad():
+ generated_images = pipeline(
+ prompt=prompt,
+ prompt_2=prompt,
+ prompt_3=prompt,
+ negative_prompt="",
+ negative_prompt_2="",
+ negative_prompt_3="",
+ height=image_h,
+ width=image_w,
+ num_inference_steps=num_inference_steps,
+ guidance_scale=guidance_scale,
+ max_sequence_length=max_sequence_length,
+ generator=generator,
+ output_type="pil",
+ ).images
+
+ profiler.end("denoising_loop")
+ end_total = time.time()
+
+ total_time = end_total - start_total
+ total_times.append(total_time)
+
+ images.append(generated_images[0])
+ logger.info(f"Image {i+1} completed in {total_time:.2f}s")
+
+ # optional: save image
+ os.makedirs("generated_images", exist_ok=True)
+ generated_images[0].save(f"generated_images/image_{i+1}.png")
+
+ # calculate metrics
+ logger.info("Calculating CLIP scores...")
+ clip = CLIPEncoder()
+ clip_scores = [100 * clip.get_clip_score(prompts[i], img).item() for i, img in enumerate(images)]
+ average_clip_score = sum(clip_scores) / len(clip_scores)
+
+ deviation_clip_score = "N/A"
+ fid_score = "N/A"
+
+ # Calculate CLIP standard deviation if we have multiple prompts
+ if num_prompts >= 2:
+ deviation_clip_score = statistics.stdev(clip_scores)
+ logger.info(f"CLIP scores individual: {[f'{score:.2f}' for score in clip_scores]}")
+
+ # Calculate FID score if we have the COCO statistics file
+ if num_prompts >= 2 and os.path.isfile(coco_statistics_path):
+ logger.info("Calculating FID score...")
+ fid_score = calculate_fid_score(images, coco_statistics_path)
+ elif num_prompts >= 2 and not os.path.isfile(coco_statistics_path):
+ logger.warning(f"FID calculation skipped: COCO stats file not found at {coco_statistics_path}")
+ logger.info("To enable FID calculation, download COCO validation statistics:")
+ logger.info(
+ "wget https://github.com/mlcommons/inference/raw/4b1d1156c23965172ae56eacdd8372f8897eb771/text_to_image/tools/val2014.npz"
+ )
+ elif num_prompts < 2:
+ logger.info("FID calculation requires at least 2 images")
+
+ average_inference_time = sum(total_times) / len(total_times) if total_times else 0
+ min_inference_time = min(total_times) if total_times else 0
+ max_inference_time = max(total_times) if total_times else 0
+
+ # results
+ logger.info("=== RESULTS ===")
+ logger.info(f"Average CLIP Score: {average_clip_score:.2f}")
+ logger.info(f"CLIP Score Std Dev: {deviation_clip_score}")
+ logger.info(f"Average Inference Time: {average_inference_time:.2f}s")
+ logger.info(f"Min/Max Inference Time: {min_inference_time:.2f}s / {max_inference_time:.2f}s")
+ logger.info(f"FID Score: {fid_score}")
+
+ print(f"FID score: {fid_score}")
+ print(f"Average CLIP Score: {average_clip_score}")
+ print(f"Standard Deviation of CLIP Scores: {deviation_clip_score}")
+
+ # check if T5 encoder is being used
+ t5_enabled = hasattr(pipeline, "text_encoder_3") and pipeline.text_encoder_3 is not None
+ logger.info(f"T5 text encoder enabled: {t5_enabled}")
+
+ # ensure values are JSON serializable
+ clip_std_serializable = float(deviation_clip_score) if deviation_clip_score != "N/A" else "N/A"
+ fid_serializable = float(fid_score) if fid_score != "N/A" else "N/A"
+
+ data = {
+ "model": model_name,
+ "metadata": {
+ "device": device.upper(),
+ "model_name": model_name,
+ "actual_model": model_id,
+ "start_from": start_from,
+ "num_prompts": num_prompts,
+ "image_width": image_w,
+ "image_height": image_h,
+ "guidance_scale": guidance_scale,
+ "num_inference_steps": num_inference_steps,
+ "max_sequence_length": max_sequence_length,
+ "t5_enabled": t5_enabled,
+ "backend": "diffusers",
+ "dtype": str(torch.bfloat16 if device == "cuda" else torch.float32),
+ "optimizations": ["attention_slicing", "vae_slicing"],
+ },
+ "benchmarks_summary": [
+ {
+ "device": device.upper(),
+ "model": model_name,
+ "average_denoising_time": profiler.get("denoising_loop"),
+ "average_vae_time": 0.0, # VAE time is included in denoising for diffusers
+ "average_inference_time": average_inference_time,
+ "min_inference_time": min_inference_time,
+ "max_inference_time": max_inference_time,
+ "average_clip": average_clip_score,
+ "deviation_clip": clip_std_serializable,
+ "fid_score": fid_serializable,
+ "individual_clip_scores": [float(score) for score in clip_scores],
+ }
+ ],
+ }
+
+ os.makedirs(OUT_ROOT, exist_ok=True)
+ with open(f"{OUT_ROOT}/{RESULTS_FILE_NAME}", "w") as f:
+ json.dump(data, f, indent=4)
+ logger.info(f"test results saved to {OUT_ROOT}/{RESULTS_FILE_NAME}")
+
+ # cleanup
+ if device == "cuda":
+ torch.cuda.empty_cache()
+
+ print(f"\n🎉 Test completed successfully!")
+ print(f"📊 Average CLIP Score: {average_clip_score:.2f}")
+ print(f"⚡ Average Inference Time: {average_inference_time:.2f}s")
+ print(f"🖼️ Images saved to: generated_images/")
+
+
+def sd35_get_prompts(captions_path, start_from, num_prompts):
+ assert (
+ 0 <= start_from < 5000 and start_from + num_prompts <= 5000
+ ), "start_from must be between 0 and 4999, and start_from + num_prompts must not exceed 5000."
+
+ prompts = []
+ if not os.path.isfile(captions_path):
+ logger.info(f"file {captions_path} not found. downloading...")
+ os.makedirs(os.path.dirname(captions_path), exist_ok=True)
+ urllib.request.urlretrieve(COCO_CAPTIONS_DOWNLOAD_PATH, captions_path)
+ logger.info("download complete.")
+
+ with open(captions_path, "r") as tsv_file:
+ reader = csv.reader(tsv_file, delimiter="\t")
+ next(reader) # skip header
+ for index, row in enumerate(reader):
+ if index < start_from:
+ continue
+ if index >= start_from + num_prompts:
+ break
+ prompts.append(row[2])
+ return prompts
+
+
+# fixture for device parameters
+@pytest.fixture
+def device_params(request):
+ return request.param
+
+
+# python -m pytest model.py::test_accuracy_sd35 -v --start-from=0 --num-prompts=5 -s
diff --git a/code/models/tt_dit/tests/dataset_eval/test_tt_dit.py b/code/models/tt_dit/tests/dataset_eval/test_tt_dit.py
new file mode 100644
index 0000000000000000000000000000000000000000..57174714ce649812cf62e24647f0e0f5c90f19bb
--- /dev/null
+++ b/code/models/tt_dit/tests/dataset_eval/test_tt_dit.py
@@ -0,0 +1,264 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+import csv
+import itertools
+import json
+import os
+import random
+import statistics
+import time
+import urllib
+
+import pytest
+from loguru import logger
+
+import ttnn
+
+# from .fid_score import calculate_fid_score
+from models.demos.stable_diffusion_xl_base.utils.fid_score import calculate_fid_score
+from models.perf.benchmarking_utils import BenchmarkProfiler
+from models.tt_dit.pipelines.events import profiler_event_callback
+
+from .clip_encoder import CLIPEncoder
+
+# COCO captions download path
+COCO_CAPTIONS_DOWNLOAD_PATH = "https://github.com/mlcommons/inference/raw/4b1d1156c23965172ae56eacdd8372f8897eb771/text_to_image/coco2014/captions/captions_source.tsv"
+
+
+def get_prompts(captions_path, start_from, num_prompts):
+ assert (
+ 0 <= start_from < 5000 and start_from + num_prompts <= 5000
+ ), "start_from must be between 0 and 4999, and start_from + num_prompts must not exceed 5000."
+
+ prompts = []
+ if not os.path.isfile(captions_path):
+ logger.info(f"file {captions_path} not found. downloading...")
+ os.makedirs(os.path.dirname(captions_path), exist_ok=True)
+ urllib.request.urlretrieve(COCO_CAPTIONS_DOWNLOAD_PATH, captions_path)
+ logger.info("download complete.")
+
+ with open(captions_path, "r") as tsv_file:
+ reader = csv.reader(tsv_file, delimiter="\t")
+ next(reader) # skip header
+ for index, row in enumerate(reader):
+ if index < start_from:
+ continue
+ if index >= start_from + num_prompts:
+ break
+ prompts.append(row[2])
+ return prompts
+
+
+def get_img_evals(model_id, model_setup, images, prompts, coco_statistics_path):
+ # Calculate metrics
+ logger.info("Calculating CLIP scores...")
+ num_prompts = len(prompts)
+ clip = CLIPEncoder()
+ clip_scores = [100 * clip.get_clip_score(prompts[i], img).item() for i, img in enumerate(images)]
+ average_clip_score = sum(clip_scores) / len(clip_scores)
+
+ deviation_clip_score = "N/A"
+ fid_score = "N/A"
+
+ # Calculate CLIP standard deviation if we have multiple prompts
+ if num_prompts >= 2:
+ deviation_clip_score = statistics.stdev(clip_scores)
+ logger.info(f"CLIP scores individual: {[f'{score:.2f}' for score in clip_scores]}")
+
+ # Calculate FID score if we have the COCO statistics file
+ logger.info("Calculating FID score...")
+ try:
+ fid_score = calculate_fid_score(images, coco_statistics_path)
+ except Exception as e:
+ logger.error(f"Error calculating FID score: {e}")
+ else:
+ logger.info("FID calculation requires at least 2 images")
+
+ evals = [
+ {
+ "model": model_id,
+ "average_clip": average_clip_score,
+ "clip_standard_deviation": deviation_clip_score,
+ "num_prompts": num_prompts,
+ "accuracy_check": check_accuracy(num_prompts, average_clip_score, "clip", model_setup),
+ },
+ {
+ "model": model_id,
+ "fid_score": fid_score,
+ "num_prompts": num_prompts,
+ "accuracy_check": check_accuracy(num_prompts, fid_score, "fid", model_setup),
+ },
+ ]
+ return evals
+
+
+def get_vid_evals(model_id, model_setup, videos, prompts):
+ num_prompts = len(prompts)
+ clip = CLIPEncoder()
+ clip_scores = [
+ [100 * clip.get_clip_score(prompts[i], img).item() for img in video] for i, video in enumerate(videos)
+ ]
+ clip_score_stats = [(min(c), max(c), statistics.mean(c), statistics.stdev(c)) for c in clip_scores]
+
+ for p, c in zip(prompts, clip_score_stats):
+ logger.info(f"Prompt: {p}, CLIP statistics (min, max, mean, stddev): {c}")
+
+ video_mean_scores = [c[2] for c in clip_score_stats]
+ min_clip = min(video_mean_scores)
+ max_clip = max(video_mean_scores)
+ mean_clip = statistics.mean(video_mean_scores)
+ stddev_clip = statistics.stdev(video_mean_scores)
+
+ evals = [
+ {
+ "model": model_id,
+ "average_clip": mean_clip,
+ "min_clip": min_clip,
+ "max_clip": max_clip,
+ "clip_standard_deviation": stddev_clip,
+ "num_prompts": num_prompts,
+ "accuracy_check": check_accuracy(num_prompts, mean_clip, "clip", model_setup),
+ }
+ ]
+ return evals
+
+
+def check_accuracy(num_prompts, score, score_key, model_info):
+ accuracy_status = 1
+ prompt_data = model_info["accuracy"].get(str(num_prompts), {})
+ range_key = f"{score_key}_valid_range"
+ if range_key in prompt_data:
+ score_range = prompt_data[range_key]
+ if score >= score_range[0] and score <= score_range[1]:
+ return 2
+ else:
+ return 3
+
+ return accuracy_status
+
+
+@pytest.mark.parametrize(
+ "mesh_device,device_name",
+ [
+ [(1, 2), "p300"],
+ [(2, 2), "qbge"],
+ [(2, 4), "t3K"],
+ [(4, 8), "galaxy"],
+ ],
+ ids=[
+ "1x2",
+ "2x2",
+ "2x4",
+ "4x8",
+ ],
+ indirect=["mesh_device"],
+)
+def test_tt_dit_accuracy(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ device_name: str,
+ num_inference_steps,
+ model_id,
+ model_location_generator,
+ dit_pipeline,
+ model_metadata,
+ evaluation_range,
+ is_video,
+ is_24hrs_test,
+ monkeypatch,
+) -> None:
+ """Accuracy test for TT-Metal DiT pipelines with CLIP and FID score evaluation.
+ It relies on the default configurations set in each pipeline.
+ """
+
+ if os.getenv("TT_DIT_CACHE_DIR") is None:
+ monkeypatch.setenv("TT_DIT_CACHE_DIR", "/tmp/TT_DIT_CACHE")
+ benchmark_profiler = BenchmarkProfiler()
+ if ttnn.device.is_blackhole() and device_name == "galaxy":
+ device_name = "bh_" + device_name
+ captions_path = "coco_captions/captions.tsv"
+ coco_statistics_path = "coco_statistics/val2014.npz"
+ start_from, num_prompts = evaluation_range
+ prompts = get_prompts(captions_path, start_from, num_prompts)
+ logger.info(f"start inference from prompt index: {start_from} to {start_from + num_prompts}")
+
+ logger.info(f" Mesh shape: {mesh_device.shape}")
+ logger.info(f" Model ID: {model_id}")
+ logger.info(f" Inference steps: {num_inference_steps}")
+
+ # Create pipeline similar to test_performance_flux1.py
+ pipeline = dit_pipeline.create_pipeline(
+ mesh_device=mesh_device, checkpoint_name=model_location_generator(model_metadata["hf_id"])
+ )
+
+ images = []
+ gen_type = "image" if not is_video else "video"
+ logger.info(f"Starting generation of {len(prompts)} {gen_type}s...")
+
+ # Generate images for each prompt
+ iter = itertools.cycle(prompts) if is_24hrs_test else prompts
+ start_time = time.time()
+ for i, prompt in enumerate(iter):
+ logger.info(
+ f"Generating {gen_type} {i+1} of {len(prompts) if not is_24hrs_test else '24hrs {gen_type}s'}: {prompt[:50]}..."
+ )
+
+ # Generate image using TT-Metal pipeline
+ with benchmark_profiler("inference", iteration=i):
+ generated_images = pipeline.run_single_prompt(
+ prompt=prompt,
+ num_inference_steps=num_inference_steps,
+ on_event=profiler_event_callback(benchmark_profiler, i),
+ **model_metadata.get("extra_args", {}),
+ )
+
+ images.append(generated_images[0])
+ logger.info(f"{gen_type} {i+1} completed in {benchmark_profiler.get_duration('inference', i):.2f}s")
+
+ if is_24hrs_test:
+ if (time.time() - start_time) > 24 * 60 * 60:
+ break
+ # Sleep for a random time between 0.1 and 10 seconds
+ time.sleep(random.uniform(0.1, 10))
+
+ # Performance metrics
+ average_inference_time = benchmark_profiler.get_duration_average("inference", 1) # Skip warmup iterations
+
+ data = {
+ "model": model_id,
+ "metadata": {
+ "device": f"{device_name}",
+ "model_name": model_id,
+ "start_from": start_from,
+ "num_prompts": num_prompts,
+ "num_inference_steps": num_inference_steps,
+ "mesh_shape": list(mesh_device.shape),
+ },
+ "benchmarks_summary": [
+ {
+ "device": f"{device_name}",
+ "model": model_id,
+ "average_inference_time": average_inference_time,
+ "average_vae_time": benchmark_profiler.get_duration_average("vae", 1),
+ "average_encoding_time": benchmark_profiler.get_duration_average("encoder", 1),
+ "average_denoising_time": benchmark_profiler.get_duration_average("denoising", 1),
+ }
+ ],
+ "evals": get_vid_evals(model_id, model_metadata, images, prompts)
+ if is_video
+ else get_img_evals(model_id, model_metadata, images, prompts, coco_statistics_path),
+ }
+
+ out_root = "test_reports"
+ os.makedirs(out_root, exist_ok=True)
+ file_path = f"{out_root}/sdxl_test_results.json"
+ with open(file_path, "w") as f:
+ json.dump(data, f, indent=4)
+ logger.info(f"test results saved to {file_path}")
+
+ # Synchronize devices
+ pipeline.synchronize_devices()
+
+ print(f"\n🎉 Test completed successfully!")
+ print(f"⚡ Average Inference Time: {average_inference_time:.2f}s")
diff --git a/code/models/tt_dit/tests/encoders/clip/test_clip_embeddings.py b/code/models/tt_dit/tests/encoders/clip/test_clip_embeddings.py
new file mode 100644
index 0000000000000000000000000000000000000000..03199d589e39bf566fc736c37e0e327adc9bd735
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/clip/test_clip_embeddings.py
@@ -0,0 +1,138 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import sys
+import time
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from loguru import logger
+from transformers import CLIPTextModelWithProjection, CLIPTokenizer
+
+import ttnn
+from models.tt_dit.encoders.clip.model_clip import CLIPConfig, TextEmbeddings
+from models.tt_dit.utils.check import assert_quality
+
+
+@pytest.mark.parametrize(
+ "model_name",
+ [
+ "large",
+ ],
+)
+@pytest.mark.parametrize(
+ "clip_path, tokenizer_path, expected_pcc",
+ [
+ ("text_encoder", "tokenizer", 0.99),
+ ("text_encoder_2", "tokenizer_2", 0.985),
+ ],
+ ids=["encoder_1", "encoder_2"],
+)
+@pytest.mark.parametrize("mesh_device", [(2, 4)], ids=["t3k"], indirect=True)
+@pytest.mark.parametrize("submesh_shape", [(1, 4), (2, 2)], ids=["1x4", "2x2"])
+@pytest.mark.parametrize(
+ "device_params, topology",
+ [[{"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, ttnn.Topology.Linear]],
+ indirect=["device_params"],
+)
+def test_clip_text_embeddings(
+ *,
+ mesh_device: ttnn.Device,
+ submesh_shape: ttnn.MeshShape,
+ model_name: str,
+ clip_path: str,
+ tokenizer_path: str,
+ expected_pcc: float,
+ topology: ttnn.Topology,
+) -> None:
+ parent_mesh_shape = tuple(mesh_device.shape)
+ if any(x[0] < x[1] for x in zip(parent_mesh_shape, submesh_shape)):
+ pytest.skip("submesh shape is larger than parent mesh shape, skipping")
+ encoder_submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ print(f"Running on submesh {encoder_submesh.shape} of parent mesh {mesh_device.shape}")
+
+ model_name_checkpoint = f"stabilityai/stable-diffusion-3.5-{model_name}"
+
+ hf_model = CLIPTextModelWithProjection.from_pretrained(
+ model_name_checkpoint, subfolder=clip_path, local_files_only=True
+ )
+ tokenizer = CLIPTokenizer.from_pretrained(model_name_checkpoint, subfolder=tokenizer_path, local_files_only=True)
+
+ hf_model.eval()
+
+ logger.info("=== HuggingFace CLIP Config ===")
+ logger.info(f"vocab_size: {hf_model.config.vocab_size}")
+ logger.info(f"hidden_size: {hf_model.config.hidden_size}")
+ logger.info(f"intermediate_size: {hf_model.config.intermediate_size}")
+ logger.info(f"num_attention_heads: {hf_model.config.num_attention_heads}")
+ logger.info(f"num_hidden_layers: {hf_model.config.num_hidden_layers}")
+
+ test_text = "A coffee shop on Main Street that serves excellent pastries and opens at 7 AM on weekdays"
+
+ hf_inputs = tokenizer(test_text, padding=True, truncation=True, max_length=77, return_tensors="pt")
+ tt_prompt = ttnn.from_torch(
+ hf_inputs.input_ids,
+ dtype=ttnn.uint32,
+ layout=ttnn.TILE_LAYOUT,
+ device=encoder_submesh,
+ mesh_mapper=ttnn.ReplicateTensorToMesh(encoder_submesh),
+ )
+
+ # === TT-DiT TextEmbeddings ====
+ config = CLIPConfig(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.hidden_size,
+ ff_dim=hf_model.config.intermediate_size,
+ num_heads=hf_model.config.num_attention_heads,
+ num_hidden_layers=hf_model.config.num_hidden_layers,
+ max_prompt_length=77,
+ layer_norm_eps=hf_model.config.layer_norm_eps,
+ attention_dropout=hf_model.config.attention_dropout,
+ hidden_act="quick_gelu",
+ )
+
+ tt_embedding = TextEmbeddings(config, encoder_submesh)
+ embeddings_state_dict = {}
+ for key, value in hf_model.state_dict().items():
+ if key.startswith("text_model.embeddings."):
+ new_key = key.replace("text_model.embeddings.", "")
+ embeddings_state_dict[new_key] = value
+ tt_embedding.load_state_dict(embeddings_state_dict)
+
+ # time TT model inference only
+ tt_start_time = time.time()
+ tt_embeddings_output = tt_embedding(tt_prompt)
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+
+ # get HF outputs
+ with torch.no_grad():
+ hf_start_time = time.time()
+
+ hf_token_embeddings = hf_model.text_model.embeddings.token_embedding(hf_inputs.input_ids)
+ seq_len = hf_inputs.input_ids.shape[-1]
+ position_ids = torch.arange(seq_len).expand((1, -1))
+ hf_position_embeddings = hf_model.text_model.embeddings.position_embedding(position_ids)
+ hf_embeddings_output = hf_token_embeddings + hf_position_embeddings
+
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+
+ # convert mesh tensor to torch tensor for pcc
+ # since weights are replicated, can get the tensor from any single device
+ tt_embeddings_output_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_embeddings_output)[0])
+
+ logger.info(f"TT embeddings execution time: {tt_execution_time:.4f} seconds")
+ logger.info(f"HF embeddings execution time: {hf_execution_time:.4f} seconds")
+
+ assert hf_embeddings_output.shape == tt_embeddings_output_torch.shape
+
+ assert_quality(hf_embeddings_output, tt_embeddings_output_torch, pcc=expected_pcc)
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/code/models/tt_dit/tests/encoders/clip/test_clip_encoder_all_layers.py b/code/models/tt_dit/tests/encoders/clip/test_clip_encoder_all_layers.py
new file mode 100644
index 0000000000000000000000000000000000000000..dbc11637486791d51965c1eb77417d056f9f7a99
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/clip/test_clip_encoder_all_layers.py
@@ -0,0 +1,196 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import sys
+import time
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from loguru import logger
+from transformers import CLIPTextModelWithProjection, CLIPTokenizer
+
+import ttnn
+from models.tt_dit.encoders.clip.model_clip import (
+ CLIPConfig,
+ CLIPStack,
+ TextEmbeddings,
+ create_4d_causal_attention_mask,
+)
+from models.tt_dit.parallel.config import EncoderParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+
+
+@pytest.mark.parametrize(
+ "model_name",
+ [
+ "large",
+ ],
+)
+@pytest.mark.parametrize(
+ "clip_path, tokenizer_path, expected_pcc",
+ [
+ ("text_encoder", "tokenizer", 0.99),
+ ("text_encoder_2", "tokenizer_2", 0.985),
+ ],
+ ids=["encoder_1", "encoder_2"],
+)
+@pytest.mark.parametrize("mesh_device", [(2, 4)], ids=["t3k"], indirect=True)
+@pytest.mark.parametrize("submesh_shape", [(1, 4), (2, 2)], ids=["1x4", "2x2"])
+@pytest.mark.parametrize(
+ "device_params, topology",
+ [[{"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, ttnn.Topology.Linear]],
+ indirect=["device_params"],
+)
+def test_clip_stack_all_layers(
+ *,
+ mesh_device: ttnn.Device,
+ submesh_shape: ttnn.MeshShape,
+ model_name: str,
+ clip_path: str,
+ tokenizer_path: str,
+ expected_pcc: float,
+ topology: ttnn.Topology,
+) -> None:
+ parent_mesh_shape = tuple(mesh_device.shape)
+ if any(x[0] < x[1] for x in zip(parent_mesh_shape, submesh_shape)):
+ pytest.skip("submesh shape is larger than parent mesh shape, skipping")
+ encoder_submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ print(f"Running on submesh {encoder_submesh.shape} of parent mesh {mesh_device.shape}")
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=encoder_submesh.shape[1], mesh_axis=1),
+ )
+ ccl_manager = CCLManager(
+ mesh_device=encoder_submesh,
+ num_links=1,
+ topology=ttnn.Topology.Linear,
+ )
+
+ model_name_checkpoint = f"stabilityai/stable-diffusion-3.5-{model_name}"
+
+ hf_model = CLIPTextModelWithProjection.from_pretrained(
+ model_name_checkpoint, subfolder=clip_path, local_files_only=True
+ )
+ tokenizer = CLIPTokenizer.from_pretrained(model_name_checkpoint, subfolder=tokenizer_path, local_files_only=True)
+
+ hf_model.eval()
+
+ logger.info("=== HuggingFace CLIP Config ===")
+ logger.info(f"vocab_size: {hf_model.config.vocab_size}")
+ logger.info(f"hidden_size: {hf_model.config.hidden_size}")
+ logger.info(f"intermediate_size: {hf_model.config.intermediate_size}")
+ logger.info(f"num_attention_heads: {hf_model.config.num_attention_heads}")
+ logger.info(f"num_hidden_layers: {hf_model.config.num_hidden_layers}")
+
+ test_text = "A coffee shop on Main Street that serves excellent pastries and opens at 7 AM on weekdays"
+
+ hf_inputs = tokenizer(test_text, padding=True, truncation=True, max_length=77, return_tensors="pt")
+ tt_prompt = ttnn.from_torch(
+ hf_inputs.input_ids,
+ dtype=ttnn.uint32,
+ layout=ttnn.TILE_LAYOUT,
+ device=encoder_submesh,
+ mesh_mapper=ttnn.ReplicateTensorToMesh(encoder_submesh),
+ )
+
+ # === TT-DiT CLIPStack (all encoder layers) ====
+ config = CLIPConfig(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.hidden_size,
+ ff_dim=hf_model.config.intermediate_size,
+ num_heads=hf_model.config.num_attention_heads,
+ num_hidden_layers=hf_model.config.num_hidden_layers,
+ max_prompt_length=77,
+ layer_norm_eps=hf_model.config.layer_norm_eps,
+ attention_dropout=hf_model.config.attention_dropout,
+ hidden_act=hf_model.config.hidden_act,
+ )
+
+ logger.info(f"Testing CLIPStack with {config.num_hidden_layers} encoder layers")
+
+ tt_embedding = TextEmbeddings(config, encoder_submesh)
+ embeddings_state_dict = {}
+ for key, value in hf_model.state_dict().items():
+ if key.startswith("text_model.embeddings."):
+ new_key = key.replace("text_model.embeddings.", "")
+ embeddings_state_dict[new_key] = value
+ tt_embedding.load_torch_state_dict(embeddings_state_dict)
+
+ hidden_states = tt_embedding(tt_prompt)
+
+ causal_attention_mask = create_4d_causal_attention_mask(
+ tt_prompt.shape, encoder_submesh, dtype=hidden_states.get_dtype()
+ )
+
+ tt_clip_stack = CLIPStack(config, encoder_submesh, ccl_manager, parallel_config)
+
+ encoder_state_dict = {}
+ for key, value in hf_model.state_dict().items():
+ if key.startswith("text_model.encoder."):
+ new_key = key.replace("text_model.encoder.", "")
+ encoder_state_dict[new_key] = value
+ tt_clip_stack.load_torch_state_dict(encoder_state_dict)
+
+ tt_start_time = time.time()
+ tt_all_hidden_states = tt_clip_stack(hidden_states, causal_attention_mask, ccl_manager, parallel_config)
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+
+ expected_outputs = config.num_hidden_layers + 1 # input + all encoder layers
+
+ # get HF reference outputs
+ with torch.no_grad():
+ hf_start_time = time.time()
+
+ # get HF embeddings
+ hf_token_embeddings = hf_model.text_model.embeddings.token_embedding(hf_inputs.input_ids)
+ seq_len = hf_inputs.input_ids.shape[-1]
+ position_ids = torch.arange(seq_len).expand((1, -1))
+ hf_position_embeddings = hf_model.text_model.embeddings.position_embedding(position_ids)
+ hf_hidden_states = hf_token_embeddings + hf_position_embeddings
+
+ # create causal attention mask for HF
+ batch_size, tgt_len = hf_inputs.input_ids.shape
+ hf_causal_mask = torch.full((tgt_len, tgt_len), float("-inf"))
+ mask_cond = torch.arange(hf_causal_mask.size(-1))
+ hf_causal_mask.masked_fill_(mask_cond < (mask_cond + 1).view(hf_causal_mask.size(-1), 1), 0)
+ hf_causal_mask = hf_causal_mask[None, None, :, :].expand(batch_size, 1, tgt_len, tgt_len)
+
+ # run through HF encoder layers manually to match CLIPStack behavior
+ hf_all_hidden_states = [hf_hidden_states] # input embeddings
+ current_hidden_states = hf_hidden_states
+
+ for layer_idx in range(hf_model.config.num_hidden_layers):
+ # transformers 5.x merged causal_attention_mask into attention_mask and returns
+ # the hidden states directly instead of a tuple.
+ layer_output = hf_model.text_model.encoder.layers[layer_idx](
+ current_hidden_states, hf_causal_mask # hidden_states, attention_mask
+ )
+ hf_all_hidden_states.append(layer_output)
+ current_hidden_states = layer_output
+
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+
+ # convert mesh tensor to torch tensor for pcc (final layer output)
+ # since weights are replicated, can get the tensor from any single device
+ tt_final_layer_output = ttnn.to_torch(ttnn.get_device_tensors(tt_all_hidden_states[-1])[0])
+ hf_final_layer_output = hf_all_hidden_states[-1]
+
+ logger.info(f"TT CLIPStack (all {config.num_hidden_layers} layers) execution time: {tt_execution_time:.4f} seconds")
+ logger.info(
+ f"HF encoder (all {hf_model.config.num_hidden_layers} layers) execution time: {hf_execution_time:.4f} seconds"
+ )
+
+ assert hf_final_layer_output.shape == tt_final_layer_output.shape
+
+ assert_quality(hf_final_layer_output, tt_final_layer_output, pcc=expected_pcc)
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/code/models/tt_dit/tests/encoders/clip/test_clip_encoder_single_layer.py b/code/models/tt_dit/tests/encoders/clip/test_clip_encoder_single_layer.py
new file mode 100644
index 0000000000000000000000000000000000000000..d256ef499fc13fbd850cc2099a8f67a773303928
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/clip/test_clip_encoder_single_layer.py
@@ -0,0 +1,184 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import sys
+import time
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from loguru import logger
+from transformers import CLIPTextModelWithProjection, CLIPTokenizer
+
+import ttnn
+from models.tt_dit.encoders.clip.model_clip import (
+ CLIPConfig,
+ CLIPEncoderLayer,
+ TextEmbeddings,
+ create_4d_causal_attention_mask,
+)
+from models.tt_dit.parallel.config import EncoderParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+
+
+@pytest.mark.parametrize(
+ "model_name",
+ [
+ "large",
+ ],
+)
+@pytest.mark.parametrize(
+ "clip_path, tokenizer_path, expected_pcc",
+ [
+ ("text_encoder", "tokenizer", 0.99),
+ ("text_encoder_2", "tokenizer_2", 0.985),
+ ],
+ ids=["encoder_1", "encoder_2"],
+)
+@pytest.mark.parametrize("mesh_device", [(1, 1), (2, 4)], ids=["single_device", "t3k"], indirect=True)
+@pytest.mark.parametrize("submesh_shape", [(1, 1), (1, 4), (2, 2)], ids=["1x1", "1x4", "2x2"])
+@pytest.mark.parametrize(
+ "device_params, topology",
+ [[{"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, ttnn.Topology.Linear]],
+ indirect=["device_params"],
+)
+def test_clip_encoder_layer(
+ *,
+ mesh_device: ttnn.Device,
+ submesh_shape: ttnn.MeshShape,
+ model_name: str,
+ clip_path: str,
+ tokenizer_path: str,
+ expected_pcc: float,
+ topology: ttnn.Topology,
+) -> None:
+ parent_mesh_shape = tuple(mesh_device.shape)
+ if any(x[0] < x[1] for x in zip(parent_mesh_shape, submesh_shape)):
+ pytest.skip("submesh shape is larger than parent mesh shape, skipping")
+ encoder_submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ print(f"Running on submesh {encoder_submesh.shape} of parent mesh {mesh_device.shape}")
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=encoder_submesh.shape[1], mesh_axis=1),
+ )
+ ccl_manager = CCLManager(
+ mesh_device=encoder_submesh,
+ num_links=1,
+ topology=ttnn.Topology.Linear,
+ )
+
+ model_name_checkpoint = f"stabilityai/stable-diffusion-3.5-{model_name}"
+
+ hf_model = CLIPTextModelWithProjection.from_pretrained(
+ model_name_checkpoint, subfolder=clip_path, local_files_only=True
+ )
+ tokenizer = CLIPTokenizer.from_pretrained(model_name_checkpoint, subfolder=tokenizer_path, local_files_only=True)
+
+ hf_model.eval()
+
+ logger.info("=== HuggingFace CLIP Config ===")
+ logger.info(f"vocab_size: {hf_model.config.vocab_size}")
+ logger.info(f"hidden_size: {hf_model.config.hidden_size}")
+ logger.info(f"intermediate_size: {hf_model.config.intermediate_size}")
+ logger.info(f"num_attention_heads: {hf_model.config.num_attention_heads}")
+ logger.info(f"num_hidden_layers: {hf_model.config.num_hidden_layers}")
+
+ test_text = "A coffee shop on Main Street that serves excellent pastries and opens at 7 AM on weekdays"
+
+ hf_inputs = tokenizer(test_text, padding=True, truncation=True, max_length=77, return_tensors="pt")
+ tt_prompt = ttnn.from_torch(
+ hf_inputs.input_ids,
+ dtype=ttnn.uint32,
+ layout=ttnn.TILE_LAYOUT,
+ device=encoder_submesh,
+ mesh_mapper=ttnn.ReplicateTensorToMesh(encoder_submesh),
+ )
+
+ # === TT-DiT CLIPEncoderLayer ====
+ config = CLIPConfig(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.hidden_size,
+ ff_dim=hf_model.config.intermediate_size,
+ num_heads=hf_model.config.num_attention_heads,
+ num_hidden_layers=hf_model.config.num_hidden_layers,
+ max_prompt_length=77,
+ layer_norm_eps=hf_model.config.layer_norm_eps,
+ attention_dropout=hf_model.config.attention_dropout,
+ hidden_act="quick_gelu",
+ )
+
+ tt_embedding = TextEmbeddings(config, encoder_submesh)
+ embeddings_state_dict = {}
+ for key, value in hf_model.state_dict().items():
+ if key.startswith("text_model.embeddings."):
+ new_key = key.replace("text_model.embeddings.", "")
+ embeddings_state_dict[new_key] = value
+ tt_embedding.load_state_dict(embeddings_state_dict)
+
+ hidden_states = tt_embedding(tt_prompt)
+
+ causal_attention_mask = create_4d_causal_attention_mask(
+ tt_prompt.shape, encoder_submesh, dtype=hidden_states.get_dtype()
+ )
+
+ tt_encoder_layer = CLIPEncoderLayer(config, encoder_submesh, ccl_manager, parallel_config)
+
+ first_layer_state_dict = {}
+ for key, value in hf_model.state_dict().items():
+ if key.startswith("text_model.encoder.layers.0."):
+ new_key = key.replace("text_model.encoder.layers.0.", "")
+ first_layer_state_dict[new_key] = value
+ tt_encoder_layer.load_state_dict(first_layer_state_dict)
+
+ # time TT model inference only
+ tt_start_time = time.time()
+ tt_layer_output = tt_encoder_layer(hidden_states, causal_attention_mask, ccl_manager, parallel_config)
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+
+ # get HF encoder layer output
+ with torch.no_grad():
+ hf_start_time = time.time()
+
+ # get HF embeddings
+ hf_token_embeddings = hf_model.text_model.embeddings.token_embedding(hf_inputs.input_ids)
+ seq_len = hf_inputs.input_ids.shape[-1]
+ position_ids = torch.arange(seq_len).expand((1, -1))
+ hf_position_embeddings = hf_model.text_model.embeddings.position_embedding(position_ids)
+ hf_hidden_states = hf_token_embeddings + hf_position_embeddings
+
+ # create causal attention mask for HF
+ batch_size, tgt_len = hf_inputs.input_ids.shape
+ hf_causal_mask = torch.full((tgt_len, tgt_len), float("-inf"))
+ mask_cond = torch.arange(hf_causal_mask.size(-1))
+ hf_causal_mask.masked_fill_(mask_cond < (mask_cond + 1).view(hf_causal_mask.size(-1), 1), 0)
+ hf_causal_mask = hf_causal_mask[None, None, :, :].expand(batch_size, 1, tgt_len, tgt_len)
+
+ # run the first encoder layer. transformers 5.x merged causal_attention_mask into
+ # attention_mask and returns the hidden states directly instead of a tuple.
+ hf_layer_output = hf_model.text_model.encoder.layers[0](
+ hf_hidden_states, # hidden_states
+ hf_causal_mask, # attention_mask
+ )
+
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+
+ # convert mesh tensor to torch tensor for pcc
+ # since weights are replicated, can get the tensor from any single device
+ tt_layer_output_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_layer_output)[0])
+
+ logger.info(f"TT encoder layer execution time: {tt_execution_time:.4f} seconds")
+ logger.info(f"HF encoder layer execution time: {hf_execution_time:.4f} seconds")
+
+ assert hf_layer_output.shape == tt_layer_output_torch.shape
+
+ assert_quality(hf_layer_output, tt_layer_output_torch, pcc=expected_pcc)
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/code/models/tt_dit/tests/encoders/clip/test_clip_full_no_projection.py b/code/models/tt_dit/tests/encoders/clip/test_clip_full_no_projection.py
new file mode 100644
index 0000000000000000000000000000000000000000..e6a2da3fd55a1f6ecd531b659147f47a3bf8db50
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/clip/test_clip_full_no_projection.py
@@ -0,0 +1,148 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import sys
+import time
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from loguru import logger
+from transformers import CLIPTextModel, CLIPTokenizer
+
+import ttnn
+from models.tt_dit.encoders.clip.model_clip import CLIPConfig, CLIPEncoder
+from models.tt_dit.parallel.config import EncoderParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+
+
+@pytest.mark.parametrize(
+ "model_name",
+ [
+ "large",
+ ],
+)
+@pytest.mark.parametrize(
+ "clip_path, tokenizer_path, expected_pcc",
+ [
+ ("text_encoder", "tokenizer", 0.99),
+ ("text_encoder_2", "tokenizer_2", 0.984),
+ ],
+ ids=["encoder_1", "encoder_2"],
+)
+@pytest.mark.parametrize("mesh_device", [(2, 4)], ids=["t3k"], indirect=True)
+@pytest.mark.parametrize("submesh_shape", [(1, 4), (2, 2)], ids=["1x4", "2x2"])
+@pytest.mark.parametrize(
+ "device_params, topology",
+ [[{"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, ttnn.Topology.Linear]],
+ indirect=["device_params"],
+)
+def test_clip_encoder(
+ *,
+ mesh_device: ttnn.Device,
+ submesh_shape: ttnn.MeshShape,
+ model_name: str,
+ clip_path: str,
+ tokenizer_path: str,
+ expected_pcc: float,
+ topology: ttnn.Topology,
+) -> None:
+ parent_mesh_shape = tuple(mesh_device.shape)
+ if any(x[0] < x[1] for x in zip(parent_mesh_shape, submesh_shape)):
+ pytest.skip("submesh shape is larger than parent mesh shape, skipping")
+ encoder_submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ print(f"Running on submesh {encoder_submesh.shape} of parent mesh {mesh_device.shape}")
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=encoder_submesh.shape[1], mesh_axis=1),
+ )
+ ccl_manager = CCLManager(
+ mesh_device=encoder_submesh,
+ num_links=1,
+ topology=ttnn.Topology.Linear,
+ )
+
+ model_name_checkpoint = f"stabilityai/stable-diffusion-3.5-{model_name}"
+
+ hf_model = CLIPTextModel.from_pretrained(model_name_checkpoint, subfolder=clip_path, local_files_only=True)
+ tokenizer = CLIPTokenizer.from_pretrained(model_name_checkpoint, subfolder=tokenizer_path, local_files_only=True)
+
+ hf_model.eval()
+
+ logger.info("=== HuggingFace CLIP Config ===")
+ logger.info(f"vocab_size: {hf_model.config.vocab_size}")
+ logger.info(f"hidden_size: {hf_model.config.hidden_size}")
+ logger.info(f"intermediate_size: {hf_model.config.intermediate_size}")
+ logger.info(f"num_attention_heads: {hf_model.config.num_attention_heads}")
+ logger.info(f"num_hidden_layers: {hf_model.config.num_hidden_layers}")
+
+ # Test prompt. Cannot use randn tensor due to specific HF eos token id
+ test_text = "A coffee shop on Main Street that serves excellent pastries and opens at 7 AM on weekdays"
+
+ hf_inputs = tokenizer(test_text, padding=True, truncation=True, max_length=77, return_tensors="pt")
+ tt_prompt = ttnn.from_torch(
+ hf_inputs.input_ids,
+ dtype=ttnn.uint32,
+ layout=ttnn.TILE_LAYOUT,
+ device=encoder_submesh,
+ mesh_mapper=ttnn.ReplicateTensorToMesh(encoder_submesh),
+ )
+
+ eos_token_id = hf_model.config.eos_token_id
+ logger.info(f"EOS token id: {eos_token_id}") # eos token id: 2
+
+ logger.info(f"Activation function: {hf_model.config.hidden_act}") # quick_gelu
+
+ # === TT-DiT CLIP ====
+ config = CLIPConfig(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.hidden_size,
+ ff_dim=hf_model.config.intermediate_size,
+ num_heads=hf_model.config.num_attention_heads,
+ num_hidden_layers=hf_model.config.num_hidden_layers,
+ max_prompt_length=77,
+ layer_norm_eps=hf_model.config.layer_norm_eps,
+ attention_dropout=hf_model.config.attention_dropout,
+ hidden_act=hf_model.config.hidden_act,
+ )
+
+ tt_clip = CLIPEncoder(config, encoder_submesh, ccl_manager, parallel_config, eos_token_id)
+ tt_clip.load_torch_state_dict(hf_model.state_dict())
+
+ # times TT model inference only
+ tt_start_time = time.time()
+ tt_sequence_output, tt_pooled_output = tt_clip(tt_prompt)
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+
+ # get HF reference outputs
+ with torch.no_grad():
+ hf_start_time = time.time()
+ hf_output = hf_model(hf_inputs.input_ids, output_hidden_states=True)
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+
+ hf_sequence_output = hf_output.hidden_states[-1]
+ hf_pooled_output = hf_output.pooler_output
+
+ # convert mesh tensor to torch tensor for pcc
+ # since weights are replicated, can get the tensor from any single device
+ tt_sequence_output_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_sequence_output[-1])[0])
+ tt_pooled_output_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_pooled_output)[0])
+
+ logger.info(f"TT model execution time: {tt_execution_time:.4f} seconds")
+ logger.info(f"HF model execution time: {hf_execution_time:.4f} seconds")
+
+ assert hf_sequence_output.shape == tt_sequence_output_torch.shape
+ assert hf_pooled_output.shape == tt_pooled_output_torch.shape
+
+ assert_quality(hf_sequence_output, tt_sequence_output_torch, pcc=expected_pcc)
+ assert_quality(hf_pooled_output, tt_pooled_output_torch, pcc=expected_pcc)
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/code/models/tt_dit/tests/encoders/clip/test_clip_full_projection.py b/code/models/tt_dit/tests/encoders/clip/test_clip_full_projection.py
new file mode 100644
index 0000000000000000000000000000000000000000..14b1477d8f9c47dd665b0601d36f70f186a094f9
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/clip/test_clip_full_projection.py
@@ -0,0 +1,215 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import os
+import sys
+import time
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from loguru import logger
+from transformers import CLIPTextConfig as HF_CLIPConfig
+from transformers import CLIPTextModelWithProjection, CLIPTokenizer
+
+import ttnn
+from models.tt_dit.encoders.clip.model_clip import CLIPConfig, CLIPEncoder
+from models.tt_dit.parallel.config import EncoderParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.tracing import Tracer
+
+
+def get_hf_config(model_name: str) -> HF_CLIPConfig:
+ # Clip config pulled from the model. Updated to use 2 layers for testing.
+ config_params = {
+ "text_encoder": {
+ "architectures": ["CLIPTextModelWithProjection"],
+ "bos_token_id": 0,
+ "eos_token_id": 2,
+ "hidden_act": "quick_gelu",
+ "hidden_size": 768,
+ "intermediate_size": 3072,
+ "num_attention_heads": 12,
+ "num_hidden_layers": 2,
+ "projection_dim": 768,
+ "vocab_size": 49408,
+ },
+ "text_encoder_2": {
+ "architectures": ["CLIPTextModelWithProjection"],
+ "bos_token_id": 0,
+ "eos_token_id": 2,
+ "hidden_act": "gelu",
+ "hidden_size": 1280,
+ "intermediate_size": 5120,
+ "num_attention_heads": 20,
+ "num_hidden_layers": 2,
+ "projection_dim": 1280,
+ "vocab_size": 49408,
+ },
+ }
+
+ return HF_CLIPConfig(**config_params[model_name])
+
+
+@pytest.mark.parametrize(
+ "dit_unit_test",
+ [{"1": True, "0": False}.get(os.environ.get("DIT_UNIT_TEST"), False)],
+)
+@pytest.mark.parametrize(
+ "model_name",
+ [
+ "large",
+ ],
+)
+@pytest.mark.parametrize(
+ "clip_path, tokenizer_path, expected_pcc",
+ [
+ ("text_encoder", "tokenizer", 0.98),
+ ("text_encoder_2", "tokenizer_2", 0.98),
+ ],
+ ids=["encoder_1", "encoder_2"],
+)
+@pytest.mark.parametrize("mesh_device", [(2, 4), (1, 2)], ids=["t3k", "n300"], indirect=True)
+@pytest.mark.parametrize("submesh_shape", [(1, 4), (2, 2), (1, 1), (1, 2)], ids=["1x4", "2x2", "1x1", "1x2"])
+@pytest.mark.parametrize(
+ "device_params, topology",
+ [[{"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, ttnn.Topology.Linear]],
+ indirect=["device_params"],
+)
+def test_clip_encoder(
+ *,
+ mesh_device: ttnn.Device,
+ submesh_shape: tuple[int, int],
+ model_name: str,
+ clip_path: str,
+ tokenizer_path: str,
+ expected_pcc: float,
+ topology: ttnn.Topology,
+ dit_unit_test: bool,
+) -> None:
+ parent_mesh_shape = tuple(mesh_device.shape)
+ if any(x[0] < x[1] for x in zip(parent_mesh_shape, submesh_shape)):
+ pytest.skip("submesh shape is larger than parent mesh shape, skipping")
+ encoder_submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ print(f"Running on submesh {encoder_submesh.shape} of parent mesh {mesh_device.shape}")
+
+ # For N300 with parallel factor = 1, use factor=1 regardless of submesh shape
+ if tuple(mesh_device.shape) == (1, 2) and submesh_shape in [(1, 1), (1, 2)]:
+ parallel_factor = 1
+ else:
+ parallel_factor = encoder_submesh.shape[1]
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=parallel_factor, mesh_axis=1),
+ )
+ logger.info(f"Parallel factor: {parallel_factor}")
+ ccl_manager = CCLManager(
+ mesh_device=encoder_submesh,
+ num_links=1,
+ topology=ttnn.Topology.Linear,
+ )
+
+ model_name_checkpoint = f"stabilityai/stable-diffusion-3.5-{model_name}"
+
+ # HF model
+ if dit_unit_test:
+ hf_model = CLIPTextModelWithProjection(get_hf_config(clip_path))
+ else:
+ hf_model = CLIPTextModelWithProjection.from_pretrained(
+ model_name_checkpoint, subfolder=clip_path, local_files_only=True
+ )
+
+ hf_model.eval()
+
+ logger.info("=== HuggingFace CLIP Config ===")
+ logger.info(f"vocab_size: {hf_model.config.vocab_size}")
+ logger.info(f"hidden_size: {hf_model.config.hidden_size}")
+ logger.info(f"intermediate_size: {hf_model.config.intermediate_size}")
+ logger.info(f"num_attention_heads: {hf_model.config.num_attention_heads}")
+ logger.info(f"num_hidden_layers: {hf_model.config.num_hidden_layers}")
+
+ tokenizer = CLIPTokenizer.from_pretrained(model_name_checkpoint, subfolder=tokenizer_path, local_files_only=True)
+
+ # Test prompt. Cannot use randn tensor due to specific HF eos token id
+ test_text = "A coffee shop on Main Street that serves excellent pastries and opens at 7 AM on weekdays"
+
+ hf_inputs = tokenizer(test_text, padding=True, truncation=True, max_length=77, return_tensors="pt")
+ tt_prompt = ttnn.from_torch(
+ hf_inputs.input_ids,
+ dtype=ttnn.uint32,
+ layout=ttnn.TILE_LAYOUT,
+ device=encoder_submesh,
+ mesh_mapper=ttnn.ReplicateTensorToMesh(encoder_submesh),
+ )
+
+ eos_token_id = hf_model.config.eos_token_id
+ logger.info(f"EOS token id: {eos_token_id}") # eos token id: 2
+
+ logger.info(f"Activation function: {hf_model.config.hidden_act}") # quick_gelu
+
+ # === TT-DiT CLIP ====
+ config = CLIPConfig(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.hidden_size,
+ ff_dim=hf_model.config.intermediate_size,
+ num_heads=hf_model.config.num_attention_heads,
+ num_hidden_layers=hf_model.config.num_hidden_layers,
+ max_prompt_length=77,
+ layer_norm_eps=hf_model.config.layer_norm_eps,
+ attention_dropout=hf_model.config.attention_dropout,
+ hidden_act=hf_model.config.hidden_act,
+ projection_dim=hf_model.config.projection_dim,
+ )
+
+ tt_clip = CLIPEncoder(
+ config=config,
+ mesh_device=encoder_submesh,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ eos_token_id=eos_token_id,
+ )
+ tt_clip.load_torch_state_dict(hf_model.state_dict())
+ tracer = Tracer(tt_clip.forward, device=encoder_submesh, clone_prep_inputs=False)
+
+ # times TT model inference only
+ tt_start_time = time.time()
+ tt_sequence_output, tt_normalized_output = tracer(tt_prompt, skip_pooling=True)
+ tt_projected_output = tt_clip.pooled_output(
+ prompt_tokenized=tt_prompt,
+ normalized_final_state=tt_normalized_output,
+ )
+
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+
+ # get HF reference outputs
+ with torch.no_grad():
+ hf_start_time = time.time()
+ hf_output = hf_model(hf_inputs.input_ids, output_hidden_states=True)
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+
+ hf_sequence_output = hf_output.hidden_states[-1]
+ hf_projected_output = hf_output.text_embeds # projected/pooled output
+
+ # convert mesh tensor to torch tensor for pcc
+ # since weights are replicated, can get the tensor from any single device
+ tt_sequence_output_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_sequence_output[-1])[0])
+ tt_projected_output_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_projected_output)[0])
+
+ logger.info(f"TT model execution time: {tt_execution_time:.4f} seconds")
+ logger.info(f"HF model execution time: {hf_execution_time:.4f} seconds")
+
+ assert hf_sequence_output.shape == tt_sequence_output_torch.shape
+ assert hf_projected_output.shape == tt_projected_output_torch.shape
+
+ assert_quality(hf_sequence_output, tt_sequence_output_torch, pcc=expected_pcc)
+ assert_quality(hf_projected_output, tt_projected_output_torch, pcc=expected_pcc)
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/code/models/tt_dit/tests/encoders/gemma/test_gemma_connector_blocks.py b/code/models/tt_dit/tests/encoders/gemma/test_gemma_connector_blocks.py
new file mode 100644
index 0000000000000000000000000000000000000000..58e26dd88116f6dfc763dbb9b0b2eb1fe59a4024
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/gemma/test_gemma_connector_blocks.py
@@ -0,0 +1,123 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Isolated parity: device video connector vs diffusers ``LTX2ConnectorTransformer1d``
+on identical random features, asserting PCC. Isolating from the Gemma encoder keeps a
+regression here distinguishable from upstream bf16 drift.
+"""
+
+import glob
+import os
+import sys
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from huggingface_hub.constants import HF_HUB_CACHE
+from loguru import logger
+from safetensors import safe_open
+
+import ttnn
+from models.tt_dit.encoders.gemma.embeddings_connector import EmbeddingsConnector
+from models.tt_dit.parallel.config import EncoderParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.mochi import get_rot_transformation_mat
+from models.tt_dit.utils.tensor import bf16_tensor
+
+VIDEO_PREFIX = "model.diffusion_model.video_embeddings_connector."
+
+
+def _raw_video_connector_to_diffusers(raw: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """Raw Lightricks video-connector weights → diffusers LTX2ConnectorTransformer1d keys
+ (transformer_1d_blocks → transformer_blocks, attn1.q_norm/k_norm → attn1.norm_q/norm_k)."""
+ out: dict[str, torch.Tensor] = {}
+ for k, v in raw.items():
+ sub = k[len(VIDEO_PREFIX) :]
+ sub = sub.replace("transformer_1d_blocks.", "transformer_blocks.")
+ sub = sub.replace(".attn1.q_norm.", ".attn1.norm_q.").replace(".attn1.k_norm.", ".attn1.norm_k.")
+ out[sub] = v
+ return out
+
+
+def _ltx_ckpt() -> str | None:
+ explicit = os.environ.get("LTX_CHECKPOINT")
+ if explicit and os.path.exists(explicit):
+ return explicit
+ # HF_HUB_CACHE resolves HF_HUB_CACHE / HF_HOME / the default. Hardcoding ~/.cache/huggingface
+ # here made this unfindable in CI, which points HF_HUB_CACHE at the shared weights mount.
+ cands = glob.glob(os.path.join(HF_HUB_CACHE, "models--Lightricks--LTX-2.3/snapshots/*/ltx-2.3-22b-dev.safetensors"))
+ return cands[0] if cands else None
+
+
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=["mesh_device"])
+@pytest.mark.parametrize("device_params", [{"l1_small_size": 8192}], indirect=["device_params"])
+def test_connector_blocks_isolated(*, mesh_device):
+ ckpt = _ltx_ckpt()
+ if not ckpt:
+ pytest.skip("LTX checkpoint not found")
+
+ pytest.importorskip("diffusers")
+ from diffusers.pipelines.ltx2.connectors import LTX2ConnectorTransformer1d
+
+ # --- device connector built directly (no pipeline): TP=1 on 1x1, weights from raw ckpt ---
+ enc_parallel = EncoderParallelConfig(tensor_parallel=ParallelFactor(factor=1, mesh_axis=1))
+ enc_ccl = CCLManager(mesh_device, num_links=1, topology=ttnn.Topology.Linear)
+ connector = EmbeddingsConnector(
+ output_dim=4096,
+ num_blocks=8,
+ num_heads=32,
+ mesh_device=mesh_device,
+ ccl_manager=enc_ccl,
+ parallel_config=enc_parallel,
+ )
+ raw_video = {}
+ with safe_open(ckpt, "pt") as f:
+ for k in f.keys():
+ if k.startswith(VIDEO_PREFIX):
+ raw_video[k] = f.get_tensor(k)
+ # The TT loader strips the prefix, renames blocks, and permutes Q/K split→interleaved.
+ connector.load_torch_state_dict({k[len(VIDEO_PREFIX) :]: v for k, v in raw_video.items()}, strict=False)
+
+ # --- reference: diffusers video connector (SPLIT rope = checkpoint's rotation, CPU fp32) ---
+ ref = (
+ LTX2ConnectorTransformer1d(
+ num_attention_heads=32,
+ attention_head_dim=128,
+ num_layers=8,
+ num_learnable_registers=128,
+ rope_base_seq_len=4096,
+ rope_theta=10000.0,
+ rope_double_precision=False,
+ rope_type="split",
+ gated_attention=True,
+ )
+ .float()
+ .eval()
+ )
+ inc = ref.load_state_dict(_raw_video_connector_to_diffusers(raw_video), strict=False)
+ logger.info(f"ref connector load: missing={len(inc.missing_keys)} unexpected={len(inc.unexpected_keys)}")
+
+ # --- identical random input WITH PADDING (left-pad) → register replacement active ---
+ # Mirrors the real encode path: only n_real real tokens, the rest become registers.
+ seq, dim, n_real = 256, 4096, 17
+ torch.manual_seed(0)
+ x = (torch.randn(1, seq, dim) * 0.5).bfloat16()
+ binary_mask = torch.zeros(1, seq, dtype=torch.long)
+ binary_mask[:, seq - n_real :] = 1 # left-pad: real tokens at the end
+
+ # reference: diffusers binarizes the additive mask at >= -9000.
+ add_mask = torch.where(binary_mask[:, None, None, :].bool(), 0.0, -1e9) # (1,1,1,seq)
+ with torch.no_grad():
+ ref_out = ref(x.float().clone(), add_mask)[0].float()
+
+ # device: connector forward (register replacement → on-device RoPE → blocks → norm).
+ tt_feat = ttnn.from_torch(x, device=mesh_device, layout=ttnn.TILE_LAYOUT, dtype=ttnn.bfloat16)
+ trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+ dev_out = connector(tt_feat, binary_mask, trans_mat=trans_mat)
+
+ logger.info(f"CONNECTOR (with registers) ref={tuple(ref_out.shape)} dev={tuple(dev_out.shape)}")
+ assert_quality(ref_out, dev_out, pcc=0.998)
diff --git a/code/models/tt_dit/tests/encoders/gemma/test_gemma_encoder_all_layers.py b/code/models/tt_dit/tests/encoders/gemma/test_gemma_encoder_all_layers.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6e5ebb5778ceae522bb5d00484db087fcf9ff04
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/gemma/test_gemma_encoder_all_layers.py
@@ -0,0 +1,149 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Per-layer parity: TTNN GemmaEncoder vs HF reference, real-token PCC over the first
+``N_LAYERS`` decoder layers (the full 12B encoder doesn't fit one chip at tp=1).
+
+ pytest models/tt_dit/tests/encoders/gemma/test_gemma_encoder_all_layers.py -s
+"""
+
+import glob
+import os
+import sys
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import gc
+
+import pytest
+import torch
+from huggingface_hub.constants import HF_HUB_CACHE
+from loguru import logger
+from safetensors.torch import load_file
+
+import ttnn
+from models.tt_dit.encoders.gemma.model_gemma import GemmaConfig, GemmaEncoder
+from models.tt_dit.parallel.config import EncoderParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+
+N_LAYERS = int(os.environ.get("N_LAYERS", "6"))
+SEQ_LEN = 128 # must be >= SDPA q/k chunk size (128) in model_gemma to avoid a device hang
+# Per-state real-token PCC floor at the default N_LAYERS=6 (measured min ~0.9963 at the final
+# norm; shallower states ride ~0.9998). bf16 drifts with depth, so a much larger N_LAYERS may
+# need a lower floor.
+PCC_BAR = 0.995
+# Long enough to fill all SEQ_LEN slots with real tokens (no padding), so the reference
+# runs pure-causal and the device is_causal path matches exactly — no mask confound.
+PROMPT = (
+ "A plump orange tabby cat sits on a worn piano bench in a sunlit living room, "
+ "carefully playing the keys with its soft paws while dust motes drift through "
+ "the warm afternoon light. Outside the tall windows a gentle breeze stirs the "
+ "garden, and the faint melody echoes down the hallway past framed photographs, "
+ "old books stacked on the floor, and a sleeping dog curled on a faded rug near "
+ "the fireplace, utterly content in the quiet rhythm of the unhurried day. "
+ "Down the street a baker pulls warm loaves from the oven, their crust crackling "
+ "as steam rises into the cool morning air, and children chase a bright red ball "
+ "across the cobblestones while pigeons scatter and wheel above the rooftops. "
+ "Far beyond the town the mountains stand blue and patient under a wide pale sky, "
+ "their snowcaps catching the first gold of dawn as a river winds slowly through "
+ "the valley, carrying leaves and the reflections of clouds toward the distant sea."
+)
+
+
+def _gemma_path() -> str:
+ explicit = os.environ.get("GEMMA_PATH")
+ if explicit:
+ return explicit
+ # HF_HUB_CACHE resolves HF_HUB_CACHE / HF_HOME / the default. Hardcoding ~/.cache/huggingface
+ # here made this unfindable in CI, which points HF_HUB_CACHE at the shared weights mount.
+ cands = glob.glob(os.path.join(HF_HUB_CACHE, "models--google--gemma-3-12b-it-qat-q4_0-unquantized/snapshots/*/"))
+ if cands:
+ return cands[0].rstrip("/")
+ return "google/gemma-3-12b-it-qat-q4_0-unquantized"
+
+
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=["mesh_device"])
+@pytest.mark.parametrize("device_params", [{"l1_small_size": 8192}], indirect=["device_params"])
+def test_gemma_layers_individually(*, mesh_device):
+ # _gemma_path falls back to the bare repo id, which from_pretrained resolves cache-then-hub.
+ # Gating an os.path.isdir on it made that fallback dead and skipped before any HF call.
+ gemma_path = _gemma_path()
+
+ from transformers import AutoModelForCausalLM, AutoTokenizer
+
+ tok = AutoTokenizer.from_pretrained(gemma_path)
+ tokens = tok(PROMPT, return_tensors="pt", padding="max_length", max_length=SEQ_LEN, truncation=True)
+ n_real = int(tokens.attention_mask.sum().item())
+ logger.info(f"tokens {tuple(tokens.input_ids.shape)}, real={n_real} (left-pad)")
+
+ # --- HF reference hidden states (first N_LAYERS only, for speed) ---
+ ref_model = AutoModelForCausalLM.from_pretrained(gemma_path, torch_dtype=torch.bfloat16).eval()
+ # Truncate the decoder stack to N_LAYERS so the CPU forward is ~8x cheaper.
+ lm = ref_model
+ for attr in ("model", "language_model", "layers"):
+ if hasattr(lm, attr):
+ if attr == "layers":
+ break
+ lm = getattr(lm, attr)
+ assert hasattr(lm, "layers"), "could not locate decoder layers on reference model"
+ lm.layers = lm.layers[:N_LAYERS]
+ if hasattr(lm, "config"):
+ lm.config.num_hidden_layers = N_LAYERS
+ with torch.no_grad():
+ out = ref_model(
+ input_ids=tokens.input_ids,
+ attention_mask=tokens.attention_mask,
+ output_hidden_states=True,
+ )
+ ref_hs = [h.float() for h in out.hidden_states] # embed + N_LAYERS
+ del ref_model, out
+ gc.collect()
+
+ # --- device encoder: first N_LAYERS only (tp=1) ---
+ weight_files = sorted(glob.glob(f"{gemma_path}/model-*.safetensors"))
+ full_sd = {}
+ for f in weight_files:
+ full_sd.update(load_file(f))
+
+ # keep embed_tokens, final norm, and first N_LAYERS decoder layers
+ keep = {}
+ for k, v in full_sd.items():
+ if k.startswith("language_model.model.layers."):
+ li = int(k.split("language_model.model.layers.")[1].split(".")[0])
+ if li >= N_LAYERS:
+ continue
+ keep[k] = v
+ del full_sd
+ gc.collect()
+
+ config = GemmaConfig(num_hidden_layers=N_LAYERS, max_position_embeddings=SEQ_LEN)
+ enc_parallel = EncoderParallelConfig(tensor_parallel=ParallelFactor(factor=1, mesh_axis=1))
+ enc_ccl = CCLManager(mesh_device, num_links=1, topology=ttnn.Topology.Linear)
+ encoder = GemmaEncoder(config, mesh_device, enc_ccl, enc_parallel)
+ inc = encoder.load_torch_state_dict(keep, strict=False)
+ logger.info(f"load: missing={len(inc.missing_keys)} unexpected={len(inc.unexpected_keys)}")
+ del keep
+ gc.collect()
+
+ tt_ids = ttnn.from_torch(tokens.input_ids, device=mesh_device, dtype=ttnn.uint32, layout=ttnn.ROW_MAJOR_LAYOUT)
+ real_slice = slice(SEQ_LEN - n_real, SEQ_LEN) # left-pad → real tokens at the end
+
+ # Pure-causal path (no mask): all positions real, so the device is_causal path
+ # matches the reference exactly.
+ hs = encoder(tt_ids, attention_mask=None) # device: [embed, L0..L_{N-1}, final_norm]
+ # HF output_hidden_states is [embed, L0..L_{N-2}, final_norm(L_{N-1})] — its last entry is
+ # post-final-norm, not the raw last layer. Align the shared prefix directly and the final
+ # norm at [-1]; the device's raw last-layer state has no HF counterpart.
+ device_states = list(hs[: len(ref_hs) - 1]) + [hs[-1]]
+ for i, (ref, dev_tt) in enumerate(zip(ref_hs, device_states)):
+ dev = ttnn.to_torch(ttnn.get_device_tensors(dev_tt)[0]).float()
+ if dev.dim() == 4:
+ dev = dev[0]
+ assert torch.isfinite(dev).all(), f"state {i}: device output has NaN/Inf"
+ logger.info(" final_norm" if i == len(ref_hs) - 1 else f" L{i:02d}")
+ assert_quality(ref[:, real_slice, :], dev[:, real_slice, :], pcc=PCC_BAR)
+ for h in hs:
+ ttnn.deallocate(h)
diff --git a/code/models/tt_dit/tests/encoders/gemma/test_gemma_full.py b/code/models/tt_dit/tests/encoders/gemma/test_gemma_full.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c2b36be7629b87711cc326ed7d960e2bdc79c33
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/gemma/test_gemma_full.py
@@ -0,0 +1,294 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""End-to-end parity: on-device ``encode_prompts`` vs a torch reference built from HF
+Gemma-3 + diffusers ``LTX2TextConnectors`` (mirrors the diffusers ``LTX2Pipeline`` text
+path). PCC of the final video (4096) and audio (2048) context embeddings.
+
+``test_prof_gemma_ltx_devicetime`` (same file) is a Tracy per-op device-time profile of the
+encode; it skips unless GEMMA_PROF=1, so it never silently no-ops the default (asserting) suite.
+
+Do NOT run the 2x4 case under TT_METAL_WATCHER — the watcher overflows the active-eth
+fabric-router kernel-config buffer at device open.
+
+ pytest models/tt_dit/tests/encoders/gemma/test_gemma_full.py -s
+"""
+
+import glob
+import os
+import sys
+import time
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from huggingface_hub.constants import HF_HUB_CACHE
+from loguru import logger
+from safetensors import safe_open
+
+import ttnn
+from models.tt_dit.encoders.gemma.model_gemma import GemmaEncoderLayer
+from models.tt_dit.pipelines.ltx.pipeline_ltx import LTXPipeline
+from models.tt_dit.utils.check import assert_quality
+
+
+def _connector_block_subkey(sub: str) -> str:
+ """Per-block raw Lightricks key → diffusers connector key: only the block-list name and
+ the QK-norm names differ (q_norm/k_norm → norm_q/norm_k); the rest is shared."""
+ sub = sub.replace("transformer_1d_blocks.", "transformer_blocks.")
+ sub = sub.replace(".attn1.q_norm.", ".attn1.norm_q.")
+ sub = sub.replace(".attn1.k_norm.", ".attn1.norm_k.")
+ return sub
+
+
+def _raw_connectors_to_diffusers(raw: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """Raw Lightricks connector + aggregate_embed weights → diffusers LTX2TextConnectors keys.
+ aggregate_embed → text_proj_in; the connector blocks map 1:1 modulo _connector_block_subkey."""
+ VP = "model.diffusion_model.video_embeddings_connector."
+ AP = "model.diffusion_model.audio_embeddings_connector."
+ VA = "text_embedding_projection.video_aggregate_embed."
+ AA = "text_embedding_projection.audio_aggregate_embed."
+ out: dict[str, torch.Tensor] = {}
+ for k, v in raw.items():
+ if k.startswith(VA):
+ out["video_text_proj_in." + k[len(VA) :]] = v
+ elif k.startswith(AA):
+ out["audio_text_proj_in." + k[len(AA) :]] = v
+ elif k.startswith(VP):
+ out["video_connector." + _connector_block_subkey(k[len(VP) :])] = v
+ elif k.startswith(AP):
+ out["audio_connector." + _connector_block_subkey(k[len(AP) :])] = v
+ return out
+
+
+PROMPT = "A plump orange tabby cat sits on a piano bench playing keys with its paws."
+
+CONNECTOR_PREFIXES = (
+ "text_embedding_projection.video_aggregate_embed.",
+ "text_embedding_projection.audio_aggregate_embed.",
+ "model.diffusion_model.video_embeddings_connector.",
+ "model.diffusion_model.audio_embeddings_connector.",
+)
+
+
+def _gemma_path() -> str:
+ explicit = os.environ.get("GEMMA_PATH")
+ if explicit:
+ return explicit
+ # HF_HUB_CACHE resolves HF_HUB_CACHE / HF_HOME / the default. Hardcoding ~/.cache/huggingface
+ # here made this unfindable in CI, which points HF_HUB_CACHE at the shared weights mount.
+ cands = glob.glob(os.path.join(HF_HUB_CACHE, "models--google--gemma-3-12b-it-qat-q4_0-unquantized/snapshots/*/"))
+ return cands[0].rstrip("/") if cands else "google/gemma-3-12b-it-qat-q4_0-unquantized"
+
+
+def _ltx_ckpt() -> str | None:
+ explicit = os.environ.get("LTX_CHECKPOINT")
+ if explicit and os.path.exists(explicit):
+ return explicit
+ # HF_HUB_CACHE resolves HF_HUB_CACHE / HF_HOME / the default. Hardcoding ~/.cache/huggingface
+ # here made this unfindable in CI, which points HF_HUB_CACHE at the shared weights mount.
+ cands = glob.glob(os.path.join(HF_HUB_CACHE, "models--Lightricks--LTX-2.3/snapshots/*/ltx-2.3-22b-dev.safetensors"))
+ return cands[0] if cands else None
+
+
+def _encode_prompts_reference(
+ checkpoint_path: str, gemma_root: str, prompts: list[str]
+) -> tuple[torch.Tensor, torch.Tensor]:
+ """Torch reference: HF Gemma-3 text encoder + diffusers ``LTX2TextConnectors``, mirroring
+ the diffusers ``LTX2Pipeline`` text path (stacked per-layer hidden states → per-modality
+ feature extraction + connectors). Connector/aggregate weights come from the raw checkpoint;
+ the LTX-2 reference repo is no longer used. Returns (video_encoding, audio_encoding)."""
+ pytest.importorskip("diffusers")
+ from diffusers.pipelines.ltx2.connectors import LTX2TextConnectors
+ from transformers import AutoModelForCausalLM, AutoTokenizer
+
+ # LTX-2.3-22b connector config: per-modality projections, 8 blocks each, video 32x128=4096,
+ # audio 32x64=2048, gated attention, SPLIT rope (the raw checkpoint's rotation convention).
+ connectors = (
+ LTX2TextConnectors(
+ caption_channels=3840,
+ text_proj_in_factor=49,
+ video_connector_num_attention_heads=32,
+ video_connector_attention_head_dim=128,
+ video_connector_num_layers=8,
+ audio_connector_num_attention_heads=32,
+ audio_connector_attention_head_dim=64,
+ audio_connector_num_layers=8,
+ video_gated_attn=True,
+ audio_gated_attn=True,
+ per_modality_projections=True,
+ video_hidden_dim=4096,
+ audio_hidden_dim=2048,
+ rope_type="split",
+ rope_double_precision=False,
+ proj_bias=True,
+ )
+ .float()
+ .eval()
+ )
+ raw = {}
+ with safe_open(checkpoint_path, "pt") as f:
+ for k in f.keys():
+ if k.startswith(CONNECTOR_PREFIXES):
+ raw[k] = f.get_tensor(k)
+ inc = connectors.load_state_dict(_raw_connectors_to_diffusers(raw), strict=False)
+ logger.info(f"ref connectors load: missing={len(inc.missing_keys)} unexpected={len(inc.unexpected_keys)}")
+
+ tok = AutoTokenizer.from_pretrained(gemma_root)
+ tok.padding_side = "left" # Gemma-3 / LTX-2 use left padding
+ if tok.pad_token is None:
+ tok.pad_token = tok.eos_token
+ text_encoder = AutoModelForCausalLM.from_pretrained(gemma_root, torch_dtype=torch.bfloat16).eval()
+
+ ti = tok(
+ prompts,
+ padding="max_length",
+ max_length=1024,
+ truncation=True,
+ add_special_tokens=True,
+ return_tensors="pt",
+ )
+ with torch.no_grad():
+ out = text_encoder(input_ids=ti.input_ids, attention_mask=ti.attention_mask, output_hidden_states=True)
+ # Pack the 49 per-layer hidden states D-major (B, seq, 3840, 49) → (B, seq, 188160).
+ hidden = torch.stack(out.hidden_states, dim=-1).flatten(2, 3).float()
+ video, audio, _ = connectors(hidden, ti.attention_mask)
+ return video.float(), audio.float()
+
+
+# 2x4 drives the encoder's TP all-gathers over CCL, which needs the 1D fabric up;
+# 1x1 is single-chip with no CCL, so it must run without fabric.
+@pytest.mark.parametrize(
+ ("mesh_device", "device_params"),
+ [
+ pytest.param((1, 1), {"l1_small_size": 8192}, id="1x1"),
+ pytest.param((1, 8), {"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, id="1x8"),
+ pytest.param((2, 4), {"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, id="2x4"),
+ pytest.param((4, 8), {"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D_RING}, id="4x8"),
+ ],
+ indirect=["mesh_device", "device_params"],
+)
+def test_gemma_encoder(*, mesh_device):
+ # _gemma_path falls back to the bare repo id, which from_pretrained resolves cache-then-hub.
+ # Gating an os.path.isdir on it made that fallback dead and skipped before any HF call.
+ gemma = _gemma_path()
+ ckpt = _ltx_ckpt()
+ if not ckpt:
+ pytest.skip("LTX checkpoint not found")
+
+ # Bare pipeline: checkpoint_name=None skips the heavy transformer/VAE load. dynamic_load=False
+ # keeps the encoder resident so the whole-encode trace (default when resident) is exercised.
+ pipe = LTXPipeline.create_pipeline(
+ mesh_device, checkpoint_name=None, gemma_path=gemma, mode="av", dynamic_load=False
+ )
+
+ # On-device Gemma encoder (full 48 layers). TP follows the T5 pattern (axis-1 width):
+ # TP=1 on 1x1, TP=4 on 2x4 — set inside the loader, no override needed.
+ pipe.gemma_encoder_pair.load_gemma_encoder(gemma)
+
+ # Load only the connector weights from the 46GB checkpoint.
+ conn_state = {}
+ with safe_open(ckpt, "pt") as f:
+ for k in f.keys():
+ if k.startswith(CONNECTOR_PREFIXES):
+ conn_state[k] = f.get_tensor(k)
+ logger.info(f"connector weights: {len(conn_state)} tensors")
+ pipe.gemma_encoder_pair.load_embeddings_connectors(conn_state, audio_num_blocks=8)
+
+ # Reference embeds (HF Gemma-3 + diffusers LTX2TextConnectors), local to this test.
+ v_ref, a_ref = _encode_prompts_reference(ckpt, gemma, [PROMPT])
+
+ # Device embeds. First call compiles + populates the program cache; the second is warm
+ # and is the one we time. use_cache=False to measure the real encode, not a cache load.
+ pipe.encode_prompts([PROMPT], use_cache=False)
+ t0 = time.perf_counter()
+ dev = pipe.encode_prompts([PROMPT], use_cache=False)
+ t_warm_ms = (time.perf_counter() - t0) * 1e3
+
+ v_dev = torch.as_tensor(dev[0][0]).float()
+ a_dev = torch.as_tensor(dev[0][1]).float()
+
+ logger.info(f"ENCODE warm wall-clock (mesh {tuple(mesh_device.shape)}): {t_warm_ms:.1f} ms")
+
+ # Device and reference both pad to GEMMA_SEQUENCE_LENGTH=1024 (the reference text
+ # encoder fixes its tokenizer to 1024 too), so the outputs are the same shape and
+ # compare directly — no alignment needed.
+ logger.info(f"VIDEO ref={tuple(v_ref.shape)} dev={tuple(v_dev.shape)}")
+ assert_quality(v_ref, v_dev, pcc=0.9995)
+ # fp32 connector weights (HiFi4) match the host reference, so audio holds ~0.9999 like video.
+ logger.info(f"AUDIO ref={tuple(a_ref.shape)} dev={tuple(a_dev.shape)}")
+ assert_quality(a_ref, a_dev, pcc=0.9995)
+
+
+def _walk(m):
+ yield m
+ for _, c in m.named_children():
+ yield from _walk(c)
+
+
+def _flush_after(mod, mesh_device):
+ """Flush the on-device profiler after each mod.forward so each Tracy zone window stays under
+ the 1000-zone buffer."""
+ orig = mod.forward
+
+ def timed(*a, _orig=orig, **k):
+ r = _orig(*a, **k)
+ ttnn.ReadDeviceProfiler(mesh_device)
+ return r
+
+ mod.forward = timed
+
+
+@pytest.mark.skipif(
+ os.environ.get("GEMMA_PROF") != "1",
+ reason="Tracy profiling harness — run: GEMMA_PROF=1 python -m tracy -p -r -m pytest "
+ "'::test_prof_gemma_ltx_devicetime[2x4]' -s",
+)
+@pytest.mark.parametrize(
+ "device_params", [{"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}], indirect=True
+)
+@pytest.mark.parametrize("mesh_device", [(2, 4)], indirect=True)
+def test_prof_gemma_ltx_devicetime(mesh_device, device_params):
+ """Tracy per-op device-time profile of the full encode (tokenize -> 48 layers -> feature
+ extractor -> connectors). Per-layer ReadDeviceProfiler flush keeps each zone window under
+ Tracy's 1000-zone buffer; no PCC — pure timing. Sum DEVICE FW DURATION from the CSV vs the
+ printed GEMMA_ENCODE_HOST_WALL_MS — the gap is host dispatch."""
+ gemma = _gemma_path()
+ ckpt = _ltx_ckpt()
+ if not ckpt:
+ pytest.skip("LTX checkpoint not found")
+
+ pipe = LTXPipeline.create_pipeline(mesh_device, checkpoint_name=None, gemma_path=gemma, mode="av")
+ pipe.gemma_encoder_pair.load_gemma_encoder(gemma)
+
+ conn_state = {}
+ with safe_open(ckpt, "pt") as f:
+ for k in f.keys():
+ if k.startswith(CONNECTOR_PREFIXES):
+ conn_state[k] = f.get_tensor(k)
+ pipe.gemma_encoder_pair.load_embeddings_connectors(conn_state, audio_num_blocks=8)
+
+ # Flush after each decoder layer plus the feature extractor and each connector — keeps every
+ # window well under Tracy's 1000-zone buffer.
+ pair = pipe.gemma_encoder_pair
+ for mod in _walk(pair.gemma_encoder):
+ if isinstance(mod, GemmaEncoderLayer):
+ _flush_after(mod, mesh_device)
+ _flush_after(pair.feature_extractor, mesh_device)
+ _flush_after(pair.video_connector, mesh_device)
+ if pair.audio_connector is not None:
+ _flush_after(pair.audio_connector, mesh_device)
+
+ pipe.encode_prompts([PROMPT], use_cache=False) # warm program cache
+ ttnn.synchronize_device(mesh_device)
+ ttnn.ReadDeviceProfiler(mesh_device)
+
+ t0 = time.perf_counter()
+ pipe.encode_prompts([PROMPT], use_cache=False)
+ ttnn.synchronize_device(mesh_device)
+ host_wall = (time.perf_counter() - t0) * 1000
+ ttnn.ReadDeviceProfiler(mesh_device)
+ print(f"\nGEMMA_ENCODE_HOST_WALL_MS={host_wall:.2f}", flush=True)
diff --git a/code/models/tt_dit/tests/encoders/mistral3/test_mistral3.py b/code/models/tt_dit/tests/encoders/mistral3/test_mistral3.py
new file mode 100644
index 0000000000000000000000000000000000000000..c83d6296465c95c96b7dcebabbc31e69771fb3d9
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/mistral3/test_mistral3.py
@@ -0,0 +1,398 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+import time
+
+import pytest
+import torch
+import transformers
+import transformers.generation.utils
+import transformers.models.mistral.modeling_mistral
+from loguru import logger
+
+import ttnn
+
+from ....blocks.rope import RopeConfig
+from ....encoders.mistral3.model_mistral3 import Mistral3Encoder
+from ....parallel.config import EncoderParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils import cache, tensor
+from ....utils.check import assert_quality
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "skip_layers"),
+ [
+ pytest.param((1, 4), 0, id="1x4"),
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [{"fabric_config": ttnn.FabricConfig.FABRIC_1D}],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "masked",
+ [
+ pytest.param(True, id="masked"),
+ # pytest.param(False, id="unmasked"),
+ ],
+)
+def test_generation(*, mesh_device: ttnn.MeshDevice, skip_layers: int, masked: bool) -> None:
+ torch.manual_seed(0)
+
+ tp_axis = 1
+ max_length = 40
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+ parallel_config = (
+ EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=mesh_device.shape[tp_axis], mesh_axis=tp_axis),
+ )
+ if tp_axis is not None
+ else None
+ )
+
+ tokenizer = transformers.LlamaTokenizerFast.from_pretrained("black-forest-labs/FLUX.2-dev", subfolder="tokenizer")
+ assert isinstance(tokenizer, transformers.LlamaTokenizerFast)
+
+ torch_model = transformers.Mistral3ForConditionalGeneration.from_pretrained(
+ "black-forest-labs/FLUX.2-dev", subfolder="text_encoder"
+ )
+ config = torch_model.model.language_model.config
+
+ num_layers = len(torch_model.model.language_model.layers)
+ del torch_model.model.language_model.layers[num_layers - skip_layers :]
+
+ generation_config = torch_model.generation_config
+ assert isinstance(generation_config, transformers.GenerationConfig)
+
+ model = Mistral3Encoder(
+ vocab_size=config.vocab_size,
+ head_size=config.head_dim,
+ embed_size=config.hidden_size,
+ ff_size=config.intermediate_size,
+ num_layers=config.num_hidden_layers - skip_layers,
+ num_heads=config.num_attention_heads,
+ num_kv_heads=config.num_key_value_heads,
+ norm_eps=config.rms_norm_eps,
+ attn_qkv_bias=False,
+ attn_out_bias=False,
+ rope_config=RopeConfig(theta=config.rope_parameters["rope_theta"]),
+ device=mesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+
+ state_dict = torch_model.state_dict()
+ state_dict = Mistral3Encoder.convert_state(state_dict)
+ cache.load_model(
+ model,
+ get_torch_state_dict=lambda: state_dict,
+ model_name="flux2",
+ subfolder="text_encoder",
+ parallel_config=parallel_config,
+ mesh_shape=tuple(mesh_device.shape),
+ )
+
+ # This makes unmasked generation more similar in the two implementations, possibly because
+ # `transformers` masks out padding tokens.
+ tokenizer.pad_token_id = generation_config.bos_token_id
+
+ out = tokenizer.__call__(
+ ["Once upon a time", "Hello"],
+ padding="longest",
+ # padding side does not matter for our implementation but the
+ # transformers library complains if right padding is used
+ padding_side="left",
+ return_tensors="pt",
+ return_attention_mask=True,
+ )
+ tokens = out["input_ids"].to(torch_model.device)
+ mask = out["attention_mask"].to(torch_model.device) if masked else None
+
+ tt_tokens = tensor.from_torch(tokens, device=mesh_device, dtype=ttnn.uint32)
+ tt_mask = tensor.from_torch(mask, device=mesh_device) if mask is not None else None
+
+ generation_config.max_length = max_length
+ generation_config.repetition_penalty = None # repetition penalty is not implemented
+ generation_config.return_dict_in_generate = True
+ generation_config.output_logits = True
+
+ print("running ttnn model...")
+
+ start_time = time.time()
+ tt_out = model.generate(
+ tt_tokens,
+ mask=tt_mask,
+ eos_tokens=generation_config.eos_token_id,
+ max_length=generation_config.max_length,
+ top_k=generation_config.top_k if generation_config.do_sample else 1,
+ top_p=generation_config.top_p or 1,
+ temperature=generation_config.temperature,
+ )
+ tt_tokens_out = tensor.to_torch(tt_out.tokens)
+
+ print(f"generation took {time.time() - start_time:.2f} seconds")
+
+ for i in range(tt_tokens_out.size(0)):
+ print(tokenizer.decode(tt_tokens_out[i]))
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "skip_layers"),
+ [
+ pytest.param((1, 4), 0, id="1x4"),
+ pytest.param((1, 8), 0, id="1x8"),
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [{"fabric_config": ttnn.FabricConfig.FABRIC_1D}],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "masked",
+ [
+ pytest.param(True, id="masked"),
+ pytest.param(False, id="unmasked"),
+ ],
+)
+def test_guided_generation(*, mesh_device: ttnn.MeshDevice, skip_layers: int, masked: bool) -> None:
+ torch.manual_seed(0)
+
+ tp_axis = 1
+ max_length = 20
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+ parallel_config = (
+ EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=mesh_device.shape[tp_axis], mesh_axis=tp_axis),
+ )
+ if tp_axis is not None
+ else None
+ )
+
+ tokenizer = transformers.LlamaTokenizerFast.from_pretrained("black-forest-labs/FLUX.2-dev", subfolder="tokenizer")
+ assert isinstance(tokenizer, transformers.LlamaTokenizerFast)
+
+ torch_model = transformers.Mistral3ForConditionalGeneration.from_pretrained(
+ "black-forest-labs/FLUX.2-dev", subfolder="text_encoder"
+ )
+ config = torch_model.model.language_model.config
+
+ num_layers = len(torch_model.model.language_model.layers)
+ del torch_model.model.language_model.layers[num_layers - skip_layers :]
+
+ generation_config = torch_model.generation_config
+ assert isinstance(generation_config, transformers.GenerationConfig)
+
+ model = Mistral3Encoder(
+ vocab_size=config.vocab_size,
+ head_size=config.head_dim,
+ embed_size=config.hidden_size,
+ ff_size=config.intermediate_size,
+ num_layers=config.num_hidden_layers - skip_layers,
+ num_heads=config.num_attention_heads,
+ num_kv_heads=config.num_key_value_heads,
+ norm_eps=config.rms_norm_eps,
+ attn_qkv_bias=False,
+ attn_out_bias=False,
+ rope_config=RopeConfig(theta=config.rope_parameters["rope_theta"]),
+ device=mesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+
+ state_dict = torch_model.state_dict()
+ state_dict = Mistral3Encoder.convert_state(state_dict)
+ cache.load_model(
+ model,
+ get_torch_state_dict=lambda: state_dict,
+ model_name="flux2",
+ subfolder="text_encoder",
+ parallel_config=parallel_config,
+ mesh_shape=tuple(mesh_device.shape),
+ )
+
+ # This makes unmasked generation more similar in the two implementations, possibly because
+ # `transformers` masks out padding tokens.
+ tokenizer.pad_token_id = generation_config.bos_token_id
+
+ out = tokenizer.__call__(
+ ["Once upon a time", "Hello"],
+ padding="longest",
+ # padding side does not matter for our implementation but the
+ # transformers library complains if right padding is used
+ padding_side="left",
+ return_tensors="pt",
+ return_attention_mask=True,
+ )
+ tokens = out["input_ids"].to(torch_model.device)
+ mask = out["attention_mask"].to(torch_model.device) if masked else None
+
+ tt_tokens = tensor.from_torch(tokens, device=mesh_device, dtype=ttnn.uint32)
+ tt_mask = tensor.from_torch(mask, device=mesh_device) if mask is not None else None
+
+ generation_config.max_length = max_length
+ generation_config.repetition_penalty = None # repetition penalty is not implemented
+ generation_config.return_dict_in_generate = True
+ generation_config.output_logits = True
+
+ print("running torch model...")
+ torch_mask_input = mask if mask is not None else torch.ones_like(tokens)
+ out_ref = torch_model.generate(tokens, attention_mask=torch_mask_input)
+ assert isinstance(out_ref, transformers.generation.utils.GenerateOutput)
+
+ tokens_out = out_ref.sequences
+ logits = torch.stack(out_ref.logits, dim=1)
+
+ # diffusers somtimes generates longer sequences than max_length, in particular when `max_length
+ # = 20` for whatever reason.
+ tokens_out = tokens_out[:, :max_length]
+ logits = logits[:, : max_length - tokens.size(1)]
+
+ print("running ttnn model...")
+ tt_out = model.generate(
+ tt_tokens,
+ guide=tokens_out,
+ mask=tt_mask,
+ eos_tokens=generation_config.eos_token_id,
+ max_length=generation_config.max_length,
+ top_k=generation_config.top_k if generation_config.do_sample else 1,
+ top_p=generation_config.top_p or 1,
+ temperature=generation_config.temperature,
+ return_logits=True,
+ )
+
+ tt_tokens_out = tensor.to_torch(tt_out.tokens)
+ tt_logits = tensor.to_torch(ttnn.stack(tt_out.logits, dim=1), mesh_axes=[..., tp_axis])
+
+ # To compare generated tokens, remove `guide` in the call to `model.generate`!
+ # for i in range(tt_tokens_out.size(0)):
+ # print(tokenizer.decode(tokens_out[i]))
+ # print(tokenizer.decode(tt_tokens_out[i]))
+
+ if mask is not None:
+ # Masked positions on the start of the sequence contain random values from computing softmax over all -inf
+ # so we remove them before comparison.
+ _, s, d = logits.shape
+ padded_mask = torch.nn.functional.pad(mask.bool(), [0, s - mask.size(1)], value=True)
+ logits = logits.masked_select(padded_mask.unsqueeze(-1)).view([-1, d])
+ tt_logits = tt_logits.masked_select(padded_mask.unsqueeze(-1)).view([-1, d])
+
+ assert_quality(logits, tt_logits, ccc=0.9980, relative_rmse=0.063)
+
+ assert tt_tokens_out.eq(tokens_out).all()
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "batch_size", "skip_layers"),
+ [
+ pytest.param((1, 1), 2, 32, id="1x1"),
+ pytest.param((1, 2), 2, 22, id="1x2"),
+ pytest.param((1, 4), 2, 0, id="1x4"),
+ pytest.param((1, 8), 2, 0, id="1x8"),
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [{"fabric_config": ttnn.FabricConfig.FABRIC_1D}],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "masked",
+ [
+ pytest.param(True, id="masked"),
+ pytest.param(False, id="unmasked"),
+ ],
+)
+def test_transformer(*, mesh_device: ttnn.MeshDevice, batch_size: int, skip_layers: int, masked: bool) -> None:
+ torch.manual_seed(0)
+
+ sequence_length = 512
+ tp_axis = 1
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+ parallel_config = (
+ EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=mesh_device.shape[tp_axis], mesh_axis=tp_axis),
+ )
+ if tp_axis is not None
+ else None
+ )
+
+ torch_model = transformers.Mistral3ForConditionalGeneration.from_pretrained(
+ "black-forest-labs/FLUX.2-dev", subfolder="text_encoder"
+ )
+ config = torch_model.model.language_model.config
+
+ num_layers = len(torch_model.model.language_model.layers)
+ del torch_model.model.language_model.layers[num_layers - skip_layers :]
+
+ model = Mistral3Encoder(
+ vocab_size=config.vocab_size,
+ head_size=config.head_dim,
+ embed_size=config.hidden_size,
+ ff_size=config.intermediate_size,
+ num_layers=config.num_hidden_layers - skip_layers,
+ num_heads=config.num_attention_heads,
+ num_kv_heads=config.num_key_value_heads,
+ norm_eps=config.rms_norm_eps,
+ attn_qkv_bias=False,
+ attn_out_bias=False,
+ rope_config=RopeConfig(theta=config.rope_parameters["rope_theta"]),
+ device=mesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+
+ state_dict = torch_model.state_dict()
+ state_dict = Mistral3Encoder.convert_state(state_dict)
+ cache.load_model(
+ model,
+ get_torch_state_dict=lambda: state_dict,
+ model_name="flux2",
+ subfolder="text_encoder",
+ parallel_config=parallel_config,
+ mesh_shape=tuple(mesh_device.shape),
+ )
+
+ tokens = torch.randint(0, config.vocab_size, [batch_size, sequence_length])
+ lengths = torch.randint(sequence_length // 4, 3 * sequence_length // 4, [batch_size])
+ mask = torch.arange(sequence_length).flip([0]) < lengths.unsqueeze(1) if masked else None
+
+ tt_tokens = tensor.from_torch(tokens, device=mesh_device, dtype=ttnn.uint32)
+ tt_mask = tensor.from_torch(mask, device=mesh_device) if mask is not None else None
+
+ logger.info("running ttnn model...")
+ tt_hidden_states = model.forward(
+ tt_tokens,
+ mask=tt_mask,
+ skip_final_linear=True,
+ output_hidden_states=True,
+ )
+ tt_hidden_states_torch = [tensor.to_torch(t) for t in tt_hidden_states]
+
+ logger.info("running torch model...")
+ torch_mask_input = mask if mask is not None else torch.ones_like(tokens)
+ with torch.no_grad():
+ hidden_states = torch_model.forward(
+ tokens, attention_mask=torch_mask_input, output_hidden_states=True
+ ).hidden_states
+
+ if mask is not None:
+ # Masked positions on the start of the sequence contain undefined values from computing softmax over all -inf
+ # so we remove them before comparison.
+ _, _, d = hidden_states[0].shape
+ hidden_states = [t.masked_select(mask.unsqueeze(-1)).view([-1, d]) for t in hidden_states]
+ tt_hidden_states_torch = [t.masked_select(mask.unsqueeze(-1)).view([-1, d]) for t in tt_hidden_states_torch]
+
+ assert len(hidden_states) == len(tt_hidden_states_torch)
+
+ for x, tt_x in zip(hidden_states[-4:], tt_hidden_states_torch[-4:], strict=True):
+ assert_quality(x, tt_x, pcc=0.9979, relative_rmse=0.065)
diff --git a/code/models/tt_dit/tests/encoders/qwen25vl/test_qwen25vl.py b/code/models/tt_dit/tests/encoders/qwen25vl/test_qwen25vl.py
new file mode 100644
index 0000000000000000000000000000000000000000..8be2bc4da7df80aa85425ab6255d537628d2c3ea
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/qwen25vl/test_qwen25vl.py
@@ -0,0 +1,354 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import diffusers.pipelines.qwenimage.pipeline_qwenimage
+import pytest
+import torch
+import transformers
+import transformers.models.qwen2_5_vl.modeling_qwen2_5_vl
+from loguru import logger
+
+import ttnn
+
+from ....encoders.qwen25vl.encoder_pair import Qwen25VlTokenizerEncoderPair
+from ....encoders.qwen25vl.model_qwen25vl import (
+ Qwen25VlAttention,
+ Qwen25VlContext,
+ Qwen25VlTextEncoder,
+ create_rope_tensors,
+ prepare_attention_bias,
+)
+from ....parallel.config import EncoderParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils import tensor
+from ....utils.check import assert_quality
+from ....utils.test import line_params_req_exact_devices
+
+
+def _text_config(config):
+ """transformers 5.x nests the language-model fields under `.text_config`; 4.x had them at the
+ top level of `Qwen2_5_VLConfig`. Returns `config` unchanged when already a text config."""
+ return getattr(config, "text_config", config)
+
+
+def _rope_params(config):
+ """transformers 5.x moved `rope_theta` into `rope_parameters` (aliased as `rope_scaling`)."""
+ params = getattr(config, "rope_parameters", None) or config.rope_scaling
+ rope_theta = params["rope_theta"] if "rope_theta" in params else config.rope_theta
+ return rope_theta, params["mrope_section"]
+
+
+def _real_tokens_only(x: torch.Tensor, attention_mask: torch.Tensor | None) -> torch.Tensor:
+ """Drop padding rows before comparing against the transformers reference.
+
+ We and transformers assign different position numbers to *padding* tokens, and that
+ disagreement is the only thing separating the two outputs. For 5 real tokens and 3 padding:
+
+ real tokens padding
+ ours (create_rope_tensors) 0, 1, 2, 3, 4 1, 1, 1
+ theirs (auto-inferred) 0, 1, 2, 3, 4 5, 6, 7
+
+ Both tests here compare against the auto-inferred convention: test_qwen25vl_text_encoder
+ lets Qwen2_5_VLTextModel infer positions internally, and test_qwen25vl_attention builds the
+ same arange positions by hand because it drives a single attention module directly.
+
+ Real tokens get the same position number either way, so the outputs agree everywhere a caller
+ actually reads. Padding output is discarded downstream by every caller, so comparing it
+ measures nothing but the convention mismatch -- it dragged PCC to ~91.5% against a 95.2%
+ threshold while the real tokens were exact.
+
+ transformers 4.x used our convention, which is why this only surfaced with the 5.12.1 pin.
+ Whether `create_rope_tensors` should switch to theirs is a live question for the Qwen-Image
+ owners; it changes model code, not just this test, so it is deliberately not decided here.
+ """
+ if attention_mask is None:
+ return x
+ return x[attention_mask.bool()]
+
+
+@pytest.mark.parametrize(
+ "mesh_device",
+ [
+ pytest.param((1, 2), id="1x2"),
+ pytest.param((1, 8), id="1x8"),
+ ],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [{"fabric_config": ttnn.FabricConfig.FABRIC_1D}],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "masked",
+ [
+ pytest.param(True, id="masked"),
+ pytest.param(False, id="unmasked"),
+ ],
+)
+def test_qwen25vl_attention(*, mesh_device: ttnn.MeshDevice, masked: bool) -> None:
+ torch.manual_seed(0)
+
+ batch_size = 10
+ sequence_length = 512
+ tp_axis = 1
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+
+ parent_torch_model = transformers.Qwen2_5_VLForConditionalGeneration.from_pretrained(
+ "Qwen/Qwen-Image", subfolder="text_encoder"
+ )
+ torch_model = parent_torch_model.model.language_model.layers[0].self_attn
+ assert isinstance(torch_model, transformers.models.qwen2_5_vl.modeling_qwen2_5_vl.Qwen2_5_VLAttention)
+
+ attn_config = _text_config(torch_model.config)
+ rope_theta, mrope_section = _rope_params(attn_config)
+
+ model = Qwen25VlAttention(
+ hidden_size=attn_config.hidden_size,
+ num_heads=attn_config.num_attention_heads,
+ num_key_value_heads=attn_config.num_key_value_heads,
+ ctx=Qwen25VlContext(mesh_device, tp_axis, ccl_manager),
+ )
+ model.load_torch_state_dict(torch_model.state_dict())
+
+ sequence = torch.randn([batch_size, sequence_length, attn_config.hidden_size])
+ m = torch.randint(0, sequence_length + 1, [batch_size])
+ attention_mask = torch.arange(sequence_length) < m.unsqueeze(1) if masked else None
+ cos, sin = create_rope_tensors(
+ batch_size,
+ sequence_length,
+ attention_mask,
+ head_dim=attn_config.hidden_size // attn_config.num_attention_heads,
+ rope_theta=rope_theta,
+ mrope_section=mrope_section,
+ )
+
+ tt_sequence = tensor.from_torch(sequence, device=mesh_device)
+ tt_attention_mask = tensor.from_torch(attention_mask, device=mesh_device) if attention_mask is not None else None
+ tt_pos_embeds_cos = tensor.from_torch(cos, device=mesh_device)
+ tt_pos_embeds_sin = tensor.from_torch(sin, device=mesh_device)
+
+ tt_attention_bias = prepare_attention_bias(tt_attention_mask) if tt_attention_mask is not None else None
+
+ logger.info("running ttnn model...")
+ tt_out = model.forward(
+ tt_sequence,
+ attention_bias=tt_attention_bias,
+ pos_embeds=(tt_pos_embeds_cos, tt_pos_embeds_sin),
+ )
+ tt_out_torch = tensor.to_torch(tt_out)
+
+ logger.info("running torch model...")
+ # get_rope_index is a vision+text utility; its own docstring says pure-text callers should
+ # "rely on model's auto-inferred position ids". For a text-only sequence Qwen2_5_VLTextModel
+ # infers plain arange positions replicated across all three mrope axes, so build that directly
+ # instead of calling get_rope_index off-label (which additionally requires a mm_token_type_ids
+ # argument and an integer input_ids carrier it has no real use for here).
+ position_ids = torch.arange(sequence_length).view(1, 1, -1).expand(3, batch_size, -1)
+ # transformers 5.x loads at the checkpoint dtype (bf16) where 4.x loaded fp32, so the fp32
+ # `sequence` no longer matches the weights. Feed the reference what the device already sees.
+ ref_sequence = sequence.to(next(parent_torch_model.parameters()).dtype)
+ # transformers 5.x moved rotary_emb off the attention module up to Qwen2_5_VLTextModel.
+ position_embeddings = parent_torch_model.model.language_model.rotary_emb(ref_sequence, position_ids)
+ if attention_mask is not None:
+ causal_attention_mask = attention_mask.unsqueeze(1).unsqueeze(1)
+ causal_attention_mask = causal_attention_mask.expand([-1, -1, sequence_length, -1])
+ causal_attention_mask = causal_attention_mask.tril()
+ causal_attention_mask = causal_attention_mask.bool()
+ else:
+ causal_attention_mask = None
+
+ with torch.no_grad():
+ out, _ = torch_model.forward(
+ ref_sequence,
+ attention_mask=causal_attention_mask,
+ position_embeddings=position_embeddings,
+ )
+
+ assert_quality(
+ _real_tokens_only(out, attention_mask),
+ _real_tokens_only(tt_out_torch, attention_mask),
+ pcc=0.988,
+ relative_rmse=0.15,
+ )
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "batch_size", "skip_layers"),
+ [
+ pytest.param((1, 2), 1, 0, id="1x2"),
+ pytest.param((1, 4), 1, 0, id="1x4"),
+ pytest.param((1, 8), 1, 0, id="1x8"),
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [{"fabric_config": ttnn.FabricConfig.FABRIC_1D}],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "masked",
+ [
+ pytest.param(True, id="masked"),
+ pytest.param(False, id="unmasked"),
+ ],
+)
+def test_qwen25vl_text_encoder(
+ *, mesh_device: ttnn.MeshDevice, batch_size: int, skip_layers: int, masked: bool
+) -> None:
+ torch.manual_seed(0)
+
+ sequence_length = 512
+ tp_axis = 1
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=mesh_device.shape[tp_axis], mesh_axis=tp_axis),
+ )
+
+ torch_model = transformers.Qwen2_5_VLForConditionalGeneration.from_pretrained(
+ "Qwen/Qwen-Image", subfolder="text_encoder"
+ )
+ torch_text_model = torch_model.model.language_model
+
+ mid = len(torch_text_model.layers) // 2
+ del torch_text_model.layers[mid - skip_layers // 2 : mid - (-skip_layers // 2)]
+
+ text_config = _text_config(torch_model.config)
+ rope_theta, mrope_section = _rope_params(text_config)
+
+ model = Qwen25VlTextEncoder(
+ vocab_size=text_config.vocab_size,
+ hidden_size=text_config.hidden_size,
+ intermediate_size=text_config.intermediate_size,
+ hidden_act=text_config.hidden_act,
+ num_hidden_layers=text_config.num_hidden_layers - skip_layers,
+ num_attention_heads=text_config.num_attention_heads,
+ num_key_value_heads=text_config.num_key_value_heads,
+ rms_norm_eps=text_config.rms_norm_eps,
+ rope_theta=rope_theta,
+ mrope_section=mrope_section,
+ device=mesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+ model.load_torch_state_dict(torch_text_model.state_dict())
+
+ tokens = torch.randint(0, text_config.vocab_size, [batch_size, sequence_length])
+ m = torch.randint(0, sequence_length + 1, [batch_size])
+ attention_mask = torch.arange(sequence_length) < m.unsqueeze(1) if masked else None
+ cos, sin = model.create_rope_tensors(batch_size, sequence_length, attention_mask)
+
+ tt_tokens = tensor.from_torch(tokens, device=mesh_device, dtype=ttnn.uint32)
+ tt_attention_mask = tensor.from_torch(attention_mask, device=mesh_device) if attention_mask is not None else None
+ tt_pos_embeds_cos = tensor.from_torch(cos, device=mesh_device)
+ tt_pos_embeds_sin = tensor.from_torch(sin, device=mesh_device)
+
+ logger.info("running ttnn model...")
+ tt_hidden_states = model.forward(
+ tt_tokens,
+ attention_mask=tt_attention_mask,
+ pos_embeds=(tt_pos_embeds_cos, tt_pos_embeds_sin),
+ )
+ tt_prompt_embeds = tt_hidden_states[-1]
+ tt_prompt_embeds_torch = tensor.to_torch(tt_prompt_embeds)
+
+ logger.info("running torch model...")
+ with torch.no_grad():
+ out = torch_model.forward(
+ tokens,
+ attention_mask=attention_mask,
+ output_hidden_states=True,
+ )
+ prompt_embeds = out.hidden_states[-1]
+
+ prompt_embeds = _real_tokens_only(prompt_embeds, attention_mask)
+ tt_prompt_embeds_torch = _real_tokens_only(tt_prompt_embeds_torch, attention_mask)
+ if masked:
+ assert_quality(prompt_embeds, tt_prompt_embeds_torch, pcc=0.994, relative_rmse=0.09)
+ else:
+ assert_quality(prompt_embeds, tt_prompt_embeds_torch, pcc=0.994, relative_rmse=0.09)
+
+
+@pytest.mark.parametrize(
+ "mesh_device , submesh_shape",
+ [[(2, 2), (1, 2)], [(2, 4), (1, 4)], [(4, 8), (1, 4)]],
+ ids=["2x2_1x2", "2x4_1x4", "4x8_1x4"],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "prompts",
+ [
+ [
+ "",
+ "Neon-lit cyberpunk alley, rain-soaked, cinematic wide shot",
+ ],
+ ],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [{**line_params_req_exact_devices, "trace_region_size": 31000000}],
+ ids=["line"],
+ indirect=True,
+)
+def test_qwen25vl_encoder_pair(
+ *, mesh_device: ttnn.MeshDevice, submesh_shape: tuple[int, int], prompts: list[str]
+) -> None:
+ # There is a bug in the HF implementation where the prompt_embeds_mask is incorrectly repeated
+ # if num_images_per_prompt != 1.
+ # https://github.com/huggingface/diffusers/blob/v0.35.2/src/diffusers/pipelines/qwenimage/pipeline_qwenimage.py#L262
+ # is
+ # prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt, 1)
+ # but should be
+ # prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt)
+ num_images_per_prompt = 1
+ submesh_device = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ checkpoint = "Qwen/Qwen-Image"
+
+ torch_pipeline = diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline.from_pretrained(checkpoint)
+
+ template = torch_pipeline.prompt_template_encode
+ start_idx = torch_pipeline.prompt_template_encode_start_idx
+ sequence_length = 512
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=submesh_shape[1], mesh_axis=1),
+ )
+ ccl_manager = CCLManager(mesh_device=submesh_device, num_links=1, topology=ttnn.Topology.Linear)
+
+ tt_encoder_pair = Qwen25VlTokenizerEncoderPair(
+ checkpoint,
+ tokenizer_subfolder="tokenizer",
+ encoder_subfolder="text_encoder",
+ use_torch=False,
+ device=submesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+
+ logger.info("running torch model...")
+ with torch.no_grad():
+ embeds, mask = torch_pipeline.encode_prompt(
+ prompts,
+ num_images_per_prompt=num_images_per_prompt,
+ max_sequence_length=sequence_length,
+ )
+ embeds = torch.nn.functional.pad(embeds, [0, 0, 0, sequence_length - embeds.shape[1]], value=0)
+ mask = torch.nn.functional.pad(mask, [0, sequence_length - mask.shape[1]], value=0)
+
+ logger.info("running TT model...")
+ formatted_prompts = [template.format(e) for e in prompts]
+ tt_embeds, tt_mask = tt_encoder_pair.encode(
+ formatted_prompts,
+ num_images_per_prompt=num_images_per_prompt,
+ sequence_length=sequence_length + start_idx,
+ )
+ tt_embeds = tt_embeds[:, start_idx:]
+ tt_mask = tt_mask[:, start_idx:]
+ tt_embeds *= tt_mask.unsqueeze(-1)
+
+ assert torch.allclose(mask, tt_mask)
+ assert_quality(embeds, tt_embeds, pcc=0.988, relative_rmse=0.15)
diff --git a/code/models/tt_dit/tests/encoders/qwen3vl/common.py b/code/models/tt_dit/tests/encoders/qwen3vl/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..11be16895c62d3bc13e569631404882889bd1ef5
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/qwen3vl/common.py
@@ -0,0 +1,134 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# Shared helpers and constants for the Qwen3-VL encoder tests.
+
+import contextlib
+
+import pytest
+import torch
+import transformers
+
+import ttnn
+
+from ....encoders.qwen3vl.model_qwen3vl import Qwen3VlTextEncoder
+from ....parallel.config import EncoderParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils.tensor import bf16_tensor
+
+# FABRIC_1D on a 1x1 mesh has no ethernet partner and times out router init, so fabric is per-config, CCL-only.
+L1_SMALL = 32768
+FABRIC = {"fabric_config": ttnn.FabricConfig.FABRIC_1D, "l1_small_size": L1_SMALL}
+NO_FABRIC = {"l1_small_size": L1_SMALL}
+
+# MiniMax-H3's Qwen3-VL vision tower (also Qwen3-VL-32B's).
+HIDDEN_SIZE = 1152
+NUM_HEADS = 16
+HEAD_DIM = HIDDEN_SIZE // NUM_HEADS # 72 -- deliberately not tile-aligned
+INTERMEDIATE_SIZE = 4304
+SPATIAL_MERGE_SIZE = 2
+OUT_HIDDEN_SIZE = 5120
+NUM_POSITION_EMBEDDINGS = 2304
+NORM_EPS = 1e-6
+HIDDEN_ACT = "gelu_pytorch_tanh"
+
+
+def vision_config(depth, **overrides):
+ kwargs = dict(
+ depth=depth,
+ hidden_size=HIDDEN_SIZE,
+ num_heads=NUM_HEADS,
+ intermediate_size=INTERMEDIATE_SIZE,
+ in_channels=3,
+ patch_size=16,
+ temporal_patch_size=2,
+ spatial_merge_size=SPATIAL_MERGE_SIZE,
+ num_position_embeddings=NUM_POSITION_EMBEDDINGS,
+ out_hidden_size=OUT_HIDDEN_SIZE,
+ hidden_act=HIDDEN_ACT,
+ deepstack_visual_indexes=[0],
+ initializer_range=0.02,
+ )
+ kwargs.update(overrides)
+ return transformers.Qwen3VLVisionConfig(**kwargs)
+
+
+VISION_MESH = [
+ pytest.param((1, 1), (1, 1), None, None, 1, NO_FABRIC, id="single"),
+ pytest.param((8, 4), (8, 4), 0, None, 2, FABRIC, id="tp8_sp1"),
+ pytest.param((8, 4), (8, 4), 0, 1, 2, FABRIC, id="tp8_sp4"),
+]
+VISION_PARAMS = pytest.mark.parametrize(
+ ("mesh_device", "submesh_shape", "tp_axis", "sp_axis", "num_links", "device_params"),
+ VISION_MESH,
+ indirect=["mesh_device", "device_params"],
+)
+
+
+def resolve_parallel(submesh, tp_axis, sp_axis, num_links):
+ if tp_axis is None and sp_axis is None:
+ return None, None
+ shape = tuple(submesh.shape)
+ cfg = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=shape[tp_axis] if tp_axis is not None else 1, mesh_axis=tp_axis),
+ sequence_parallel=(ParallelFactor(factor=shape[sp_axis], mesh_axis=sp_axis) if sp_axis is not None else None),
+ )
+ return cfg, CCLManager(submesh, num_links=num_links, topology=ttnn.Topology.Linear)
+
+
+def skip_if_sp_misaligned(total, submesh, sp_axis):
+ """Ring SDPA needs N_local_q % 32 == 0; misalignment is model geometry, not a port bug, so skip."""
+ if sp_axis is None:
+ return
+ sp = tuple(submesh.shape)[sp_axis]
+ if total % (sp * 32) != 0:
+ pytest.skip(f"{total} patches do not divide into {sp} tile-aligned shards (needs a multiple of {sp * 32})")
+
+
+def sp_shard(x, submesh, sp_axis):
+ if sp_axis is None:
+ return bf16_tensor(x, device=submesh)
+ return bf16_tensor(x, device=submesh, mesh_axis=sp_axis, shard_dim=0)
+
+
+@contextlib.contextmanager
+def capture_layer_outputs(lm, layers):
+ caps: dict[int, torch.Tensor] = {}
+ handles = [
+ lm.layers[i].register_forward_hook(
+ lambda m, i_, o, i=i: caps.__setitem__(i, (o[0] if isinstance(o, tuple) else o).detach())
+ )
+ for i in layers
+ ]
+ try:
+ yield caps
+ finally:
+ for h in handles:
+ h.remove()
+
+
+def hf_rope_params(cfg):
+ """transformers>=5 has no top-level cfg.rope_theta, so it must not be an (eagerly evaluated) dict.get default."""
+ rope_params = getattr(cfg, "rope_parameters", None) or cfg.rope_scaling
+ mrope_section = rope_params["mrope_section"]
+ rope_theta = rope_params["rope_theta"] if "rope_theta" in rope_params else cfg.rope_theta
+ return rope_theta, mrope_section
+
+
+def encoder_from_hf_config(cfg, **overrides):
+ rope_theta, mrope_section = hf_rope_params(cfg)
+ kwargs = dict(
+ vocab_size=cfg.vocab_size,
+ hidden_size=cfg.hidden_size,
+ intermediate_size=cfg.intermediate_size,
+ hidden_act="silu",
+ num_hidden_layers=cfg.num_hidden_layers,
+ num_attention_heads=cfg.num_attention_heads,
+ num_key_value_heads=cfg.num_key_value_heads,
+ rms_norm_eps=cfg.rms_norm_eps,
+ rope_theta=rope_theta,
+ mrope_section=mrope_section,
+ )
+ kwargs.update(overrides)
+ return Qwen3VlTextEncoder(**kwargs)
diff --git a/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl.py b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd9989a09f4c06455be76d5aeb0f6334997bbb8d
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl.py
@@ -0,0 +1,107 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# Qwen3-VL-8B text encoder for Ideogram 4.0: qwen25vl port + per-head QK-RMSNorm,
+# no qkv bias, and the 13-layer feature tap. HF reference is built from config
+# (no 8B pull); "real" overlays the shipped Ideogram fp8 text_encoder weights.
+
+import os
+
+import pytest
+import torch
+import transformers
+from loguru import logger
+from safetensors.torch import load_file
+
+import ttnn
+
+from ....encoders.qwen3vl.model_qwen3vl import create_rope_tensors
+from ....parallel.config import EncoderParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....reference.ideogram4.constants import QWEN3_VL_ACTIVATION_LAYERS
+from ....reference.ideogram4.dequant import dequant_fp8_state_dict
+from ....utils import tensor
+from ....utils.check import assert_quality
+from ....utils.tensor import bf16_tensor
+from .common import capture_layer_outputs, encoder_from_hf_config, hf_rope_params
+
+REPO = "Qwen/Qwen3-VL-8B-Instruct"
+FP8 = os.environ.get("IDEOGRAM4_WEIGHTS")
+_NEEDS_WEIGHTS = pytest.mark.skipif(not FP8, reason="IDEOGRAM4_WEIGHTS not set (gated fp8 checkpoint)")
+
+
+def _reference_lm(weights: str):
+ cfg = transformers.AutoConfig.from_pretrained(REPO)
+ hf = transformers.AutoModel.from_config(cfg).to(torch.bfloat16)
+ lm = hf.language_model if hasattr(hf, "language_model") else hf.model.language_model
+ if weights == "real":
+ sd = dequant_fp8_state_dict(load_file(f"{FP8}/text_encoder/model.safetensors"))
+ sd = {k[len("language_model.") :]: v for k, v in sd.items() if k.startswith("language_model.")}
+ incompat = lm.load_state_dict(sd, strict=False)
+ # empty missing/unexpected proves the weights landed; else both sides share the random init and PCC is vacuous
+ assert not incompat.missing_keys and not incompat.unexpected_keys, (
+ f"real Qwen3-VL load key mismatch: missing={incompat.missing_keys[:5]} "
+ f"unexpected={incompat.unexpected_keys[:5]}"
+ )
+ return lm.eval()
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "submesh_shape", "tp_axis"),
+ [
+ pytest.param((2, 4), (2, 4), 1, id="tp4_fsdp2"),
+ pytest.param((2, 4), (2, 4), 0, id="tp2_fsdp4"),
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "device_params", [{"fabric_config": ttnn.FabricConfig.FABRIC_1D, "l1_small_size": 32768}], indirect=True
+)
+@pytest.mark.parametrize(
+ "weights",
+ [pytest.param("random", id="random"), pytest.param("real", id="real", marks=_NEEDS_WEIGHTS)],
+)
+@pytest.mark.parametrize("masked", [pytest.param(False, id="nomask"), pytest.param(True, id="masked")])
+@pytest.mark.parametrize("seq_len", [128])
+def test_qwen3vl_text_encoder(
+ *, mesh_device: ttnn.MeshDevice, submesh_shape, tp_axis, weights: str, masked: bool, seq_len: int
+) -> None:
+ torch.manual_seed(0)
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ tp_factor = tuple(submesh.shape)[tp_axis]
+
+ lm = _reference_lm(weights)
+ cfg = lm.config
+ head_dim = cfg.hidden_size // cfg.num_attention_heads
+ rope_theta, mrope_section = hf_rope_params(cfg)
+
+ ids = torch.randint(0, cfg.vocab_size, (1, seq_len))
+ with capture_layer_outputs(lm, QWEN3_VL_ACTIVATION_LAYERS) as caps:
+ with torch.no_grad():
+ lm(input_ids=ids, attention_mask=torch.ones_like(ids), use_cache=False)
+ golden = [caps[i].float() for i in QWEN3_VL_ACTIVATION_LAYERS]
+
+ enc = encoder_from_hf_config(
+ cfg,
+ activation_layers=QWEN3_VL_ACTIVATION_LAYERS,
+ device=submesh,
+ parallel_config=EncoderParallelConfig(tensor_parallel=ParallelFactor(factor=tp_factor, mesh_axis=tp_axis)),
+ ccl_manager=CCLManager(submesh, num_links=1, topology=ttnn.Topology.Linear),
+ is_fsdp=True,
+ )
+ enc.load_torch_state_dict(lm.state_dict())
+
+ cos, sin = create_rope_tensors(1, seq_len, None, head_dim, rope_theta, mrope_section)
+ tt_ids = ttnn.from_torch(ids, dtype=ttnn.uint32, layout=ttnn.ROW_MAJOR_LAYOUT, device=submesh)
+ attn_mask = tensor.from_torch(torch.ones(1, seq_len, dtype=torch.bool), device=submesh) if masked else None
+ tt_caps = enc.forward(
+ tt_ids,
+ attention_mask=attn_mask,
+ pos_embeds=(bf16_tensor(cos, device=submesh), bf16_tensor(sin, device=submesh)),
+ )
+
+ pcc = 0.99 if weights == "real" else 0.98 # random weights over 36 layers accumulate more bf16 error
+ for layer_idx, g, tt_t in zip(QWEN3_VL_ACTIVATION_LAYERS, golden, tt_caps):
+ logger.info(f"qwen3vl [{weights}] TP={tp_factor} layer {layer_idx}:")
+ assert_quality(g, tensor.to_torch(tt_t, mesh_axes=[None, None, None]), pcc=pcc)
diff --git a/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_decoder_block.py b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_decoder_block.py
new file mode 100644
index 0000000000000000000000000000000000000000..e487baf3c3af87ae04800b022dfb081664a43e53
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_decoder_block.py
@@ -0,0 +1,191 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# One Qwen3-VL text decoder layer -- plus its attention and MLP in isolation -- at
+# the MiniMax-H3 conditioner dimensions, sized so a Tracy capture measures the
+# layer's arithmetic instead of a 32B whole-stack weight load.
+
+import pytest
+import torch
+import transformers
+from loguru import logger
+
+import ttnn
+
+from ....encoders.qwen3vl.model_qwen3vl import (
+ Qwen3VlAttention,
+ Qwen3VlContext,
+ Qwen3VlDecoderLayer,
+ Qwen3VlMlp,
+ create_rope_tensors,
+)
+from ....parallel.manager import CCLManager
+from ....utils import tensor
+from ....utils.check import assert_quality
+from ....utils.tensor import bf16_tensor
+from .common import FABRIC, NO_FABRIC
+
+# MiniMax-H3's `text_encoder/config.json` (text_config).
+HIDDEN_SIZE = 5120
+INTERMEDIATE_SIZE = 25600
+NUM_HEADS = 64
+NUM_KV_HEADS = 8
+HEAD_DIM = 128 # explicit in the checkpoint; 5120 // 64 would give 80
+ROPE_THETA = 5_000_000.0
+MROPE_SECTION = [24, 20, 20] # sums to HEAD_DIM // 2
+NORM_EPS = 1e-6
+HIDDEN_ACT = "silu"
+
+VOCAB_SIZE = 256 # the layer never sees the embedding table; tiny keeps the fp32 reference small
+SEQ_LEN = 128 # == SEQ_BUCKET_SIZE, so the encoder's prompt bucketing does not pad
+
+_MESH = [
+ pytest.param((1, 1), (1, 1), None, 1, NO_FABRIC, id="single"),
+ pytest.param((4, 8), (4, 8), 1, 2, FABRIC, id="tp8_axis1"),
+ pytest.param((8, 4), (8, 4), 0, 2, FABRIC, id="tp8_axis0"),
+]
+_MESH_REPRESENTATIVE = [_MESH[1]] # tp8_axis1: the (4, 8) mesh the MiniMax-H3 pipeline actually deploys on
+
+_PARAMS = pytest.mark.parametrize(
+ ("mesh_device", "submesh_shape", "tp_axis", "num_links", "device_params"),
+ _MESH,
+ indirect=["mesh_device", "device_params"],
+)
+_PARAMS_REPRESENTATIVE = pytest.mark.parametrize(
+ ("mesh_device", "submesh_shape", "tp_axis", "num_links", "device_params"),
+ _MESH_REPRESENTATIVE,
+ indirect=["mesh_device", "device_params"],
+)
+
+
+def _config():
+ return transformers.Qwen3VLTextConfig(
+ vocab_size=VOCAB_SIZE,
+ hidden_size=HIDDEN_SIZE,
+ intermediate_size=INTERMEDIATE_SIZE,
+ num_hidden_layers=1,
+ num_attention_heads=NUM_HEADS,
+ num_key_value_heads=NUM_KV_HEADS,
+ head_dim=HEAD_DIM,
+ rms_norm_eps=NORM_EPS,
+ hidden_act=HIDDEN_ACT,
+ rope_theta=ROPE_THETA,
+ rope_scaling={"rope_type": "default", "mrope_section": MROPE_SECTION, "mrope_interleaved": True},
+ )
+
+
+@pytest.fixture(scope="module")
+def golden():
+ # fp32 reference: casting it to bf16 would degrade the rotary's inv_freq and poison the golden
+ torch.manual_seed(0)
+ cfg = _config()
+ assert sum(MROPE_SECTION) == HEAD_DIM // 2, f"mrope_section sums to {sum(MROPE_SECTION)}, want {HEAD_DIM // 2}"
+
+ lm = transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextModel._from_config(cfg).eval()
+ layer = lm.layers[0]
+
+ cap: dict[str, torch.Tensor | None] = {}
+
+ def grab(key):
+ def hook(_m, args, kwargs, out):
+ inp = args[0] if args else kwargs.get("hidden_states")
+ assert inp is not None, f"{key}: could not find hidden states in args/kwargs"
+ cap[f"{key}_in"] = inp.detach()
+ cap[f"{key}_out"] = (out[0] if isinstance(out, tuple) else out).detach()
+ if key == "layer":
+ cap["mask"] = kwargs.get("attention_mask")
+ cap["pos"] = kwargs.get("position_embeddings")
+
+ return hook
+
+ handles = [
+ layer.register_forward_hook(grab("layer"), with_kwargs=True),
+ layer.self_attn.register_forward_hook(grab("attn"), with_kwargs=True),
+ layer.mlp.register_forward_hook(grab("mlp"), with_kwargs=True),
+ ]
+ ids = torch.randint(0, VOCAB_SIZE, (1, SEQ_LEN))
+ with torch.no_grad():
+ lm(input_ids=ids, attention_mask=torch.ones_like(ids), use_cache=False)
+ for h in handles:
+ h.remove()
+
+ # a future transformers materializing a mask here would silently break causal parity; fail loudly
+ assert cap["mask"] is None, (
+ f"reference passed an explicit attention_mask ({type(cap['mask'])}) to the decoder layer; "
+ "the is_causal=True path on our side no longer provably matches it"
+ )
+ return {"state": layer.state_dict(), **cap}
+
+
+def _ctx(mesh_device, submesh_shape, tp_axis, num_links):
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ ccl = CCLManager(submesh, num_links=num_links, topology=ttnn.Topology.Linear) if tp_axis is not None else None
+ return submesh, Qwen3VlContext(device=submesh, tp_axis=tp_axis, ccl_manager=ccl, fsdp_mesh_axis=None)
+
+
+def _rope(submesh):
+ # interleaved stays False (as in production): the layouts coincide while all three MRoPE axes share the position
+ cos, sin = create_rope_tensors(1, SEQ_LEN, None, HEAD_DIM, ROPE_THETA, MROPE_SECTION)
+ return bf16_tensor(cos, device=submesh), bf16_tensor(sin, device=submesh)
+
+
+@_PARAMS
+def test_decoder_block_on_device(golden, mesh_device, submesh_shape, tp_axis, num_links):
+ submesh, ctx = _ctx(mesh_device, submesh_shape, tp_axis, num_links)
+
+ block = Qwen3VlDecoderLayer(
+ hidden_size=HIDDEN_SIZE,
+ intermediate_size=INTERMEDIATE_SIZE,
+ hidden_act=HIDDEN_ACT,
+ num_attention_heads=NUM_HEADS,
+ num_key_value_heads=NUM_KV_HEADS,
+ head_dim=HEAD_DIM,
+ rms_norm_eps=NORM_EPS,
+ ctx=ctx,
+ )
+ block.load_torch_state_dict(golden["state"])
+
+ out = block.forward(
+ bf16_tensor(golden["layer_in"], device=submesh),
+ attention_bias=None,
+ pos_embeds=_rope(submesh),
+ )
+
+ tp_factor = tuple(submesh.shape)[tp_axis] if tp_axis is not None else 1
+ logger.info(f"qwen3vl decoder layer TP={tp_factor} (axis {tp_axis}):")
+ assert_quality(golden["layer_out"].float(), tensor.to_torch(out, mesh_axes=[None, None, None]), pcc=0.99)
+
+
+@_PARAMS_REPRESENTATIVE
+def test_decoder_attention_on_device(golden, mesh_device, submesh_shape, tp_axis, num_links):
+ submesh, ctx = _ctx(mesh_device, submesh_shape, tp_axis, num_links)
+
+ attn = Qwen3VlAttention(
+ hidden_size=HIDDEN_SIZE,
+ num_heads=NUM_HEADS,
+ num_key_value_heads=NUM_KV_HEADS,
+ head_dim=HEAD_DIM,
+ rms_norm_eps=NORM_EPS,
+ ctx=ctx,
+ )
+ prefix = "self_attn."
+ attn.load_torch_state_dict({k[len(prefix) :]: v for k, v in golden["state"].items() if k.startswith(prefix)})
+
+ out = attn.forward(
+ bf16_tensor(golden["attn_in"], device=submesh),
+ attention_bias=None,
+ pos_embeds=_rope(submesh),
+ )
+ assert_quality(golden["attn_out"].float(), tensor.to_torch(out, mesh_axes=[None, None, None]), pcc=0.99)
+
+
+@_PARAMS_REPRESENTATIVE
+def test_decoder_mlp_on_device(golden, mesh_device, submesh_shape, tp_axis, num_links):
+ submesh, ctx = _ctx(mesh_device, submesh_shape, tp_axis, num_links)
+
+ mlp = Qwen3VlMlp(hidden_size=HIDDEN_SIZE, intermediate_size=INTERMEDIATE_SIZE, hidden_act=HIDDEN_ACT, ctx=ctx)
+ mlp.load_torch_state_dict({k[len("mlp.") :]: v for k, v in golden["state"].items() if k.startswith("mlp.")})
+
+ out = mlp.forward(bf16_tensor(golden["mlp_in"], device=submesh))
+ assert_quality(golden["mlp_out"].float(), tensor.to_torch(out, mesh_axes=[None, None, None]), pcc=0.99)
diff --git a/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_mrope.py b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_mrope.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e33bbbc7893fd614c5cc7ecada4d8d50ea30d3e
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_mrope.py
@@ -0,0 +1,195 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# Qwen3-VL M-RoPE: the multimodal position grid and the interleaved layout,
+# against the HF reference. Pure torch, no device. Text-only prompts make the
+# chunked and interleaved layouts coincide; vision runs make the layout observable.
+
+import pytest
+import torch
+import transformers
+
+from ....encoders.qwen3vl.model_qwen3vl import create_rope_tensors, mrope_position_ids, vision_position_ids
+
+# MiniMax-H3's conditioner: head_dim 128 (not hidden_size // num_heads), interleaved M-RoPE.
+HEAD_DIM = 128
+MROPE_SECTION = [24, 20, 20]
+ROPE_THETA = 5_000_000.0
+SPATIAL_MERGE_SIZE = 2
+
+
+@pytest.fixture(scope="module")
+def reference():
+ config = transformers.Qwen3VLConfig(
+ text_config={
+ "num_hidden_layers": 2,
+ "hidden_size": 64,
+ "intermediate_size": 128,
+ "num_attention_heads": 4,
+ "num_key_value_heads": 2,
+ "head_dim": 16,
+ },
+ vision_config={
+ "depth": 2,
+ "hidden_size": 32,
+ "num_heads": 2,
+ "out_hidden_size": 64,
+ "spatial_merge_size": SPATIAL_MERGE_SIZE,
+ },
+ )
+ return transformers.AutoModel.from_config(config).eval()
+
+
+_TIMESTAMP_TOKENS = 2 # frames are separated by timestamp text in the presentation, so each frame is its own run
+
+
+def _prompt(*runs):
+ """(mm_token_type_ids, image_grid_thw, video_grid_thw) from ("text", n) / ("image"|"video", (t, h, w)) runs."""
+ type_ids, images, videos = [], [], []
+ for kind, spec in runs:
+ t, h, w = spec if kind != "text" else (0, 0, 0)
+ if kind == "text":
+ type_ids += [0] * spec
+ elif kind == "image":
+ type_ids += [1] * (t * h * w // (SPATIAL_MERGE_SIZE**2))
+ images.append([t, h, w])
+ else:
+ videos.append([t, h, w])
+ for _ in range(t):
+ type_ids += [0] * _TIMESTAMP_TOKENS
+ type_ids += [2] * (h * w // (SPATIAL_MERGE_SIZE**2))
+ return (
+ torch.tensor([type_ids], dtype=torch.long),
+ torch.tensor(images, dtype=torch.long) if images else None,
+ torch.tensor(videos, dtype=torch.long) if videos else None,
+ )
+
+
+PROMPTS = {
+ "text_only": (("text", 24),),
+ "one_image": (("text", 6), ("image", (1, 4, 6)), ("text", 5)),
+ "two_images": (("text", 3), ("image", (1, 4, 4)), ("text", 2), ("image", (1, 6, 2)), ("text", 4)),
+ "image_first": (("image", (1, 2, 2)), ("text", 8)),
+ "one_video": (("text", 4), ("video", (2, 4, 4)), ("text", 3)),
+ "video_and_image": (("text", 2), ("video", (2, 2, 4)), ("text", 3), ("image", (1, 4, 4)), ("text", 2)),
+ "video_3_frames": (("text", 2), ("video", (3, 4, 2)), ("text", 2)),
+ # production fl2va working point; rope tables only hit rounding boundaries at production length
+ "keyframe_768x1344": (("text", 5), ("image", (1, 48, 84)), ("text", 41)),
+ "two_keyframes_768x1344": (
+ ("text", 5),
+ ("image", (1, 48, 84)),
+ ("text", 5),
+ ("image", (1, 48, 84)),
+ ("text", 41),
+ ),
+}
+
+
+@pytest.mark.parametrize("name", list(PROMPTS))
+def test_mrope_position_ids_matches_reference(reference, name):
+ type_ids, image_grid, video_grid = _prompt(*PROMPTS[name])
+ input_ids = torch.zeros_like(type_ids)
+
+ expected, _ = reference.get_rope_index(
+ input_ids,
+ mm_token_type_ids=type_ids,
+ image_grid_thw=None if image_grid is None else image_grid.clone(),
+ video_grid_thw=None if video_grid is None else video_grid.clone(),
+ )
+ actual = mrope_position_ids(
+ type_ids,
+ image_grid_thw=image_grid,
+ video_grid_thw=video_grid,
+ spatial_merge_size=SPATIAL_MERGE_SIZE,
+ )
+
+ assert actual.shape == expected.shape, f"{tuple(actual.shape)} != {tuple(expected.shape)}"
+ assert torch.equal(actual, expected), (
+ f"{name}: position grid differs at "
+ f"{(actual != expected).nonzero()[:5].tolist()}\n ours={actual[:, 0, :12].tolist()}\n"
+ f" ref ={expected[:, 0, :12].tolist()}"
+ )
+
+ grids = ([] if image_grid is None else list(image_grid)) + ([] if video_grid is None else list(video_grid))
+ for grid in grids:
+ for start in (0, 7):
+ block_expected = reference.get_vision_position_ids(start, grid, 1, SPATIAL_MERGE_SIZE, device=None)
+ block_actual = vision_position_ids(start, grid, spatial_merge_size=SPATIAL_MERGE_SIZE)
+ assert torch.equal(block_actual, block_expected), (
+ f"{name} grid {grid.tolist()} start {start}: vision block grid differs\n"
+ f" ours={block_actual[:, :12].tolist()}\n ref ={block_expected[:, :12].tolist()}"
+ )
+
+
+def test_text_only_layouts_are_identical():
+ seq = 64
+ chunked = create_rope_tensors(1, seq, None, HEAD_DIM, ROPE_THETA, MROPE_SECTION, interleaved=False)
+ interleaved = create_rope_tensors(1, seq, None, HEAD_DIM, ROPE_THETA, MROPE_SECTION, interleaved=True)
+ for a, b, which in zip(chunked, interleaved, ("cos", "sin")):
+ assert torch.equal(a, b), f"text-only {which} differs between layouts"
+
+
+@pytest.mark.parametrize("name", list(PROMPTS))
+def test_interleaved_rope_tensors_match_reference(name):
+ """Toleranced, not bitwise: theta**-x vs the reference's 1/theta**x inv_freq differ by one fp32 ulp."""
+ text_config = transformers.Qwen3VLTextConfig(
+ hidden_size=5120,
+ num_attention_heads=64,
+ head_dim=HEAD_DIM,
+ rope_parameters={
+ "rope_type": "default",
+ "rope_theta": ROPE_THETA,
+ "mrope_section": MROPE_SECTION,
+ "mrope_interleaved": True,
+ },
+ )
+ rotary = transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextRotaryEmbedding(text_config)
+
+ type_ids, image_grid, video_grid = _prompt(*PROMPTS[name])
+ seq = type_ids.shape[1]
+ position_ids = mrope_position_ids(
+ type_ids, image_grid_thw=image_grid, video_grid_thw=video_grid, spatial_merge_size=SPATIAL_MERGE_SIZE
+ )
+
+ expected_cos, expected_sin = rotary(torch.zeros(1, seq, text_config.hidden_size), position_ids)
+ cos, sin = create_rope_tensors(
+ 1, seq, None, HEAD_DIM, ROPE_THETA, MROPE_SECTION, position_ids=position_ids, interleaved=True
+ )
+
+ for actual, expected, which in ((cos, expected_cos, "cos"), (sin, expected_sin, "sin")):
+ actual = actual.squeeze(1)
+ assert actual.shape == expected.shape, f"{which}: {tuple(actual.shape)} != {tuple(expected.shape)}"
+ max_diff = (actual - expected).abs().max().item()
+ assert max_diff < 1e-5, f"{name} {which}: max abs diff {max_diff:.2e} exceeds the 1-ulp inv_freq budget"
+
+
+def test_position_ids_default_is_the_shared_token_index():
+ seq = 32
+ implicit = create_rope_tensors(1, seq, None, HEAD_DIM, ROPE_THETA, MROPE_SECTION)
+ explicit = create_rope_tensors(
+ 1,
+ seq,
+ None,
+ HEAD_DIM,
+ ROPE_THETA,
+ MROPE_SECTION,
+ position_ids=torch.arange(seq).view(1, 1, -1).expand(3, 1, -1),
+ )
+ for a, b, which in zip(implicit, explicit, ("cos", "sin")):
+ assert torch.equal(a, b), f"{which} differs between the implicit and explicit text grid"
+
+
+def test_missing_grid_is_an_error(expect_error):
+ type_ids, image_grid, _ = _prompt(*PROMPTS["one_image"])
+ del image_grid
+ with expect_error(ValueError, "no matching grid"):
+ mrope_position_ids(type_ids, spatial_merge_size=SPATIAL_MERGE_SIZE)
+
+
+def test_adjacent_same_modality_blocks_are_unsupported(expect_error):
+ """The reference rejects this format too; do not "fix" ours to accept an input it cannot express."""
+ type_ids = torch.tensor([[0, 0] + [1] * 4 + [1] * 4 + [0, 0]], dtype=torch.long)
+ grids = torch.tensor([[1, 4, 4], [1, 4, 4]], dtype=torch.long)
+ with expect_error(RuntimeError, "must match the existing size"):
+ mrope_position_ids(type_ids, image_grid_thw=grids, spatial_merge_size=SPATIAL_MERGE_SIZE)
diff --git a/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_vision_block.py b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_vision_block.py
new file mode 100644
index 0000000000000000000000000000000000000000..6776134986fefa1a78a4a3594421d4d6d25504b9
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_vision_block.py
@@ -0,0 +1,115 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# Qwen3-VL vision transformer block on device against the HF reference; the
+# aspect sweep, capacity grids and perf mode live in test_qwen3vl_vision_tower.py.
+
+import pytest
+import torch
+import transformers
+
+import ttnn
+
+from ....encoders.qwen3vl.vision_qwen3vl import (
+ Qwen3VlVisionBlock,
+ resolve_vision_parallel,
+ vision_cu_seqlens,
+ vision_rope_tensors,
+)
+from ....utils import tensor
+from ....utils.check import assert_quality
+from .common import (
+ HEAD_DIM,
+ HIDDEN_ACT,
+ HIDDEN_SIZE,
+ INTERMEDIATE_SIZE,
+ NORM_EPS,
+ NUM_HEADS,
+ SPATIAL_MERGE_SIZE,
+ VISION_PARAMS,
+ resolve_parallel,
+ skip_if_sp_misaligned,
+ sp_shard,
+ vision_config,
+)
+
+PADDED_HEAD_DIM = 96 # head_dim 72 is not tile-aligned; q/k/v/o zero-pad to 96, scale must stay 72**-0.5
+
+GRIDS = {
+ "canvas_1to1": [[1, 48, 48]], # 2304 rows, 1 block -- cheapest real shape
+ "image_and_video": [[1, 128, 128], [3, 48, 48]], # 23296, 4 blocks, both sizing rules
+}
+
+
+@pytest.fixture(scope="module")
+def reference():
+ torch.manual_seed(0)
+ return transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLVisionModel._from_config(vision_config(depth=1)).eval()
+
+
+def _golden_pos_embeds(grid):
+ from transformers.vision_utils import get_vision_position_ids
+
+ position_ids = get_vision_position_ids(grid, SPATIAL_MERGE_SIZE)
+ rot = transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLVisionRotaryEmbedding(HEAD_DIM // 2)
+ freqs = rot(position_ids)
+ emb = torch.cat((freqs, freqs), dim=-1)
+ return emb.cos(), emb.sin()
+
+
+def _parallel(submesh, tp_axis, sp_axis, num_links):
+ cfg, ccl = resolve_parallel(submesh, tp_axis, sp_axis, num_links)
+ if cfg is None:
+ return None
+ return resolve_vision_parallel(submesh, cfg, ccl)
+
+
+@VISION_PARAMS
+@pytest.mark.parametrize("name", list(GRIDS))
+def test_block_on_device(reference, mesh_device, submesh_shape, tp_axis, sp_axis, num_links, name):
+ """The whole pre-norm block; multi-block grids exercise the per-frame SDPA loop and the padded-head-dim scale."""
+ import math
+
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ grid = torch.tensor(GRIDS[name], dtype=torch.long)
+ total = sum(t * h * w for t, h, w in GRIDS[name])
+ skip_if_sp_misaligned(total, submesh, sp_axis)
+
+ assert HEAD_DIM == 72 and HEAD_DIM % 32 != 0, "the whole padding question presumes a misaligned 72"
+ assert math.ceil(HEAD_DIM / 32) * 32 == PADDED_HEAD_DIM
+ assert HEAD_DIM**-0.5 != pytest.approx(PADDED_HEAD_DIM**-0.5), "the two temperatures must differ"
+
+ cu_seqlens = vision_cu_seqlens(grid)
+ assert len(cu_seqlens) - 1 == int(grid[:, 0].sum()), f"expected one block per frame, got {cu_seqlens}"
+ assert cu_seqlens[0] == 0 and cu_seqlens[-1] == total, f"cu_seqlens must span [0, {total}]: {cu_seqlens}"
+
+ torch.manual_seed(0)
+ x = torch.randn(total, HIDDEN_SIZE)
+ cos, sin = _golden_pos_embeds(grid)
+ with torch.no_grad():
+ golden = reference.blocks[0](
+ x,
+ cu_seqlens=torch.tensor(list(cu_seqlens), dtype=torch.int32),
+ position_embeddings=(cos, sin),
+ ).float()
+
+ block = Qwen3VlVisionBlock(
+ hidden_size=HIDDEN_SIZE,
+ num_heads=NUM_HEADS,
+ intermediate_size=INTERMEDIATE_SIZE,
+ hidden_act=HIDDEN_ACT,
+ norm_eps=NORM_EPS,
+ mesh_device=submesh,
+ parallel=_parallel(submesh, tp_axis, sp_axis, num_links),
+ )
+ block.load_torch_state_dict(reference.blocks[0].state_dict())
+ tt_cos, tt_sin = vision_rope_tensors(grid, head_dim=HEAD_DIM, spatial_merge_size=SPATIAL_MERGE_SIZE)
+ out = block.forward(
+ sp_shard(x, submesh, sp_axis),
+ pos_embeds=(sp_shard(tt_cos, submesh, sp_axis), sp_shard(tt_sin, submesh, sp_axis)),
+ cu_seqlens=cu_seqlens,
+ )
+ actual = tensor.to_torch(out, mesh_axes=[sp_axis, None])
+
+ assert_quality(golden, actual, pcc=0.99)
diff --git a/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_vision_injection.py b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_vision_injection.py
new file mode 100644
index 0000000000000000000000000000000000000000..4aa7577394f5ee67da1b39d5c6af4fd569169435
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_vision_injection.py
@@ -0,0 +1,242 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# How vision reaches the Qwen3-VL decoder: the tower's merged tokens REPLACE the
+# <|image_pad|> row embeddings before the block stack; its deepstack features are
+# ADDED to those same rows after each of the first len(deepstack) decoder layers.
+
+import pytest
+import torch
+import transformers
+from loguru import logger
+
+import ttnn
+
+from ....encoders.qwen3vl.model_qwen3vl import _scatter_rows, create_rope_tensors, vision_token_runs
+from ....parallel.config import EncoderParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils import tensor
+from ....utils.tensor import bf16_tensor
+from .common import encoder_from_hf_config
+
+IMAGE_TOKEN_ID = 151655 # <|image_pad|>
+HIDDEN = 128
+SEQ = 64
+
+
+def test_vision_token_runs_finds_one_run_per_image():
+ ids = torch.tensor([[1, 2, 3] + [IMAGE_TOKEN_ID] * 4 + [9, 9] + [IMAGE_TOKEN_ID] * 6 + [7]])
+ assert vision_token_runs(ids, IMAGE_TOKEN_ID) == [(3, 4), (9, 6)]
+
+
+def test_vision_token_runs_handles_the_edges():
+ assert vision_token_runs(torch.tensor([[IMAGE_TOKEN_ID] * 3 + [1, 2]]), IMAGE_TOKEN_ID) == [(0, 3)]
+ assert vision_token_runs(torch.tensor([[1, 2] + [IMAGE_TOKEN_ID] * 3]), IMAGE_TOKEN_ID) == [(2, 3)]
+ assert vision_token_runs(torch.tensor([[1, 2, 3]]), IMAGE_TOKEN_ID) == []
+
+
+def test_vision_token_runs_rejects_a_batch(expect_error):
+ """One request is one sequence; a batch would silently take only the first row."""
+ with expect_error(ValueError, "expected a single sequence"):
+ vision_token_runs(torch.zeros(2, 8, dtype=torch.long), IMAGE_TOKEN_ID)
+
+
+_MESH = [pytest.param((1, 1), (1, 1), id="single")]
+
+
+@pytest.mark.parametrize(("mesh_device", "submesh_shape"), _MESH, indirect=["mesh_device"])
+def test_scatter_rows_rejects_a_row_count_mismatch(mesh_device, submesh_shape, expect_error):
+ """Too few or too many value rows is a caller error, not a silent partial write."""
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ base = bf16_tensor(torch.zeros(1, SEQ, HIDDEN), device=submesh)
+ values = bf16_tensor(torch.zeros(1, 5, HIDDEN), device=submesh)
+ with expect_error(ValueError, "runs cover 16 rows but values has 5"):
+ _scatter_rows(base, values, [(8, 16)], add=False)
+
+
+def _reference_text_model(layers):
+ config = transformers.Qwen3VLTextConfig(
+ vocab_size=256,
+ hidden_size=HIDDEN,
+ intermediate_size=256,
+ num_hidden_layers=layers,
+ num_attention_heads=4,
+ num_key_value_heads=2,
+ head_dim=32,
+ rms_norm_eps=1e-6,
+ rope_parameters={
+ "rope_type": "default",
+ "rope_theta": 10000.0,
+ "mrope_section": [6, 5, 5],
+ "mrope_interleaved": True,
+ },
+ )
+ torch.manual_seed(0)
+ return transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextModel._from_config(config).eval()
+
+
+def _encoder(submesh, *, layers, activation_layers):
+ reference = _reference_text_model(layers)
+ enc = encoder_from_hf_config(
+ reference.config,
+ head_dim=32,
+ activation_layers=activation_layers,
+ device=submesh,
+ parallel_config=EncoderParallelConfig(tensor_parallel=ParallelFactor(factor=1, mesh_axis=0)),
+ ccl_manager=CCLManager(submesh, num_links=1, topology=ttnn.Topology.Linear),
+ )
+ enc.load_torch_state_dict(reference.state_dict())
+ return enc
+
+
+@pytest.mark.parametrize(("mesh_device", "submesh_shape"), _MESH, indirect=["mesh_device"])
+def test_text_only_path_is_unchanged(mesh_device, submesh_shape):
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ torch.manual_seed(0)
+ enc = _encoder(submesh, layers=4, activation_layers=(3,))
+ ids = ttnn.from_torch(
+ torch.randint(0, 256, (1, SEQ)), dtype=ttnn.uint32, layout=ttnn.ROW_MAJOR_LAYOUT, device=submesh
+ )
+ cos, sin = create_rope_tensors(1, SEQ, None, 32, 10000.0, [6, 5, 5])
+ pe = (bf16_tensor(cos, device=submesh), bf16_tensor(sin, device=submesh))
+
+ plain = enc.forward(ids, attention_mask=None, pos_embeds=pe)[0]
+ explicit_none = enc.forward(
+ ids, attention_mask=None, pos_embeds=pe, vision_embeds=None, vision_runs=None, deepstack_embeds=None
+ )[0]
+ a = tensor.to_torch(plain, mesh_axes=[None, None, None])
+ b = tensor.to_torch(explicit_none, mesh_axes=[None, None, None])
+ assert torch.equal(a, b), "explicitly passing None diverges from omitting the arguments"
+
+
+@pytest.mark.parametrize(("mesh_device", "submesh_shape"), _MESH, indirect=["mesh_device"])
+def test_vision_arguments_must_be_paired(mesh_device, submesh_shape, expect_error):
+ """Embeds without runs (or the reverse) is a caller error rather than a silent no-op."""
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ enc = _encoder(submesh, layers=2, activation_layers=(1,))
+ ids = ttnn.from_torch(
+ torch.zeros(1, SEQ, dtype=torch.long), dtype=ttnn.uint32, layout=ttnn.ROW_MAJOR_LAYOUT, device=submesh
+ )
+ cos, sin = create_rope_tensors(1, SEQ, None, 32, 10000.0, [6, 5, 5])
+ pe = (bf16_tensor(cos, device=submesh), bf16_tensor(sin, device=submesh))
+ embeds = bf16_tensor(torch.zeros(1, 16, HIDDEN), device=submesh)
+
+ with expect_error(ValueError, "must be passed together"):
+ enc.forward(ids, attention_mask=None, pos_embeds=pe, vision_embeds=embeds)
+ with expect_error(ValueError, "needs vision_runs"):
+ enc.forward(ids, attention_mask=None, pos_embeds=pe, deepstack_embeds=[embeds])
+
+
+@pytest.mark.parametrize(("mesh_device", "submesh_shape"), _MESH, indirect=["mesh_device"])
+def test_deepstack_is_applied_at_the_leading_layers(mesh_device, submesh_shape):
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ torch.manual_seed(0)
+ enc = _encoder(submesh, layers=4, activation_layers=(3,))
+ ids = ttnn.from_torch(
+ torch.randint(0, 256, (1, SEQ)), dtype=ttnn.uint32, layout=ttnn.ROW_MAJOR_LAYOUT, device=submesh
+ )
+ cos, sin = create_rope_tensors(1, SEQ, None, 32, 10000.0, [6, 5, 5])
+ pe = (bf16_tensor(cos, device=submesh), bf16_tensor(sin, device=submesh))
+ runs = [(8, 16)]
+ vision = bf16_tensor(torch.randn(1, 16, HIDDEN), device=submesh)
+ feat = [bf16_tensor(torch.randn(1, 16, HIDDEN), device=submesh) for _ in range(2)]
+
+ def run(deepstack):
+ out = enc.forward(
+ ids,
+ attention_mask=None,
+ pos_embeds=pe,
+ vision_embeds=vision,
+ vision_runs=runs,
+ deepstack_embeds=deepstack,
+ )[0]
+ return tensor.to_torch(out, mesh_axes=[None, None, None])
+
+ none, one, two = run(None), run(feat[:1]), run(feat[:2])
+ assert not torch.allclose(none, one, atol=1e-2), "a deepstack feature had no effect"
+ assert not torch.allclose(one, two, atol=1e-2), "the second deepstack feature had no effect"
+
+
+HIDDEN_REAL = 5120 # real conditioner width
+
+# production keyframe runs cross 31/63 tile boundaries; runs inside one 32-row tile never exercise the slicing hazard
+_TILE_RUNS = [
+ pytest.param(1054, [(5, 1008)], HIDDEN_REAL, id="production_keyframe_crosses_31"),
+ pytest.param(2067, [(5, 1008), (1018, 1008)], HIDDEN_REAL, id="production_two_keyframes"),
+ pytest.param(SEQ, [(0, 8)], HIDDEN, id="edge_run_at_row_0"),
+ pytest.param(SEQ, [(SEQ - 8, 8)], HIDDEN, id="edge_run_at_seq_end"),
+]
+
+
+@pytest.mark.parametrize(("mesh_device", "submesh_shape"), _MESH, indirect=["mesh_device"])
+@pytest.mark.parametrize(("seq", "runs", "hidden"), _TILE_RUNS)
+def test_scatter_rows_is_exact_across_tile_boundaries(mesh_device, submesh_shape, seq, runs, hidden):
+ """Replace is pure data movement, so torch.equal, not PCC (PCC would tolerate rows landing a tile off)."""
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ total = sum(length for _, length in runs)
+ assert all(
+ start % ttnn.TILE_SIZE or length % ttnn.TILE_SIZE for start, length in runs
+ ), "every run should be unaligned on at least one end, or this gates nothing"
+
+ torch.manual_seed(0)
+ # pre-rounded through bf16 so the comparison measures the scatter, not the host's fp32 -> bf16 cast
+ base = torch.randn(1, seq, hidden).bfloat16().float()
+ values = torch.randn(1, total, hidden).bfloat16().float()
+
+ golden = base.clone()
+ taken = 0
+ for start, length in runs:
+ golden[:, start : start + length, :] = values[:, taken : taken + length, :]
+ taken += length
+
+ out = _scatter_rows(bf16_tensor(base, device=submesh), bf16_tensor(values, device=submesh), runs, add=False)
+ actual = tensor.to_torch(out, mesh_axes=[None, None, None]).float()
+ assert actual.shape[-2:] == (seq, hidden), f"{tuple(actual.shape)} != (…, {seq}, {hidden})"
+
+ if not torch.equal(golden, actual):
+ wrong = (golden != actual).any(dim=-1).nonzero().flatten().tolist()
+ raise AssertionError(
+ f"{len(wrong)} of {seq} rows differ; first 10 at {wrong[:10]} "
+ f"(tile row-blocks {sorted({r // ttnn.TILE_SIZE for r in wrong[:10]})}), runs={runs}"
+ )
+
+
+@pytest.mark.parametrize(("mesh_device", "submesh_shape"), _MESH, indirect=["mesh_device"])
+@pytest.mark.parametrize(("seq", "runs", "hidden"), _TILE_RUNS)
+def test_scatter_rows_add_is_exact_across_tile_boundaries(mesh_device, submesh_shape, seq, runs, hidden):
+ """add=True: untouched rows bit-exact; written rows within 2 unbiased bf16 ulps (ttnn.add rounds differently)."""
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ total = sum(length for _, length in runs)
+
+ torch.manual_seed(0)
+ base = torch.randn(1, seq, hidden).bfloat16().float()
+ values = torch.randn(1, total, hidden).bfloat16().float()
+
+ golden = base.clone()
+ written = torch.zeros(seq, dtype=torch.bool)
+ taken = 0
+ for start, length in runs:
+ golden[:, start : start + length, :] += values[:, taken : taken + length, :]
+ written[start : start + length] = True
+ taken += length
+
+ out = _scatter_rows(bf16_tensor(base, device=submesh), bf16_tensor(values, device=submesh), runs, add=True)
+ actual = tensor.to_torch(out, mesh_axes=[None, None, None]).float()
+
+ assert torch.equal(golden[:, ~written], actual[:, ~written]), (
+ f"pass-through rows changed under add; "
+ f"{(golden[:, ~written] != actual[:, ~written]).any(dim=-1).sum().item()} of "
+ f"{int((~written).sum())} untouched rows differ"
+ )
+ expected, got = golden[:, written], actual[:, written]
+ # one bf16 ulp = 2**-7 of the binade; 2 ulps lets boundary entries fall either way
+ ulp = torch.ldexp(torch.ones_like(expected), torch.floor(torch.log2(expected.abs().clamp(min=1e-30))).int() - 7)
+ diff = (expected - got).abs()
+ worst = (diff / ulp).max().item()
+ assert worst <= 2.0, f"written rows are {worst:.2f} bf16 ulps out, past the 2-ulp floor"
+
+ # No systematic bias: round-to-nearest is unbiased, truncation is not.
+ bias = ((got - expected) / ulp).mean().item()
+ assert abs(bias) < 0.1, f"mean error {bias:+.3f} ulps suggests truncation rather than round-to-nearest"
+ logger.info(f"scatter add @ seq={seq}: worst {worst:.2f} ulps, mean bias {bias:+.3f} ulps")
diff --git a/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_vision_tower.py b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_vision_tower.py
new file mode 100644
index 0000000000000000000000000000000000000000..c0333f5e6047a4b1f0268d7699f4685134350805
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/qwen3vl/test_qwen3vl_vision_tower.py
@@ -0,0 +1,158 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# Qwen3-VL vision tower end to end on device against the HF reference: patch
+# embed, position embeddings, every block, the output and deepstack mergers, at
+# the released depth and only production grid shapes.
+
+import pytest
+import torch
+import transformers
+
+import ttnn
+
+from ....encoders.qwen3vl.vision_qwen3vl import Qwen3VlVisionModel, vision_cu_seqlens
+from ....utils import tensor
+from ....utils.check import assert_quality
+from .common import (
+ HIDDEN_ACT,
+ HIDDEN_SIZE,
+ INTERMEDIATE_SIZE,
+ NORM_EPS,
+ NUM_HEADS,
+ NUM_POSITION_EMBEDDINGS,
+ OUT_HIDDEN_SIZE,
+ SPATIAL_MERGE_SIZE,
+ VISION_PARAMS,
+ resolve_parallel,
+ skip_if_sp_misaligned,
+ sp_shard,
+ vision_config,
+)
+
+DEPTH = 27 # released tower: 27 blocks, deepstack taps at 8/16/24
+DEEPSTACK_INDEXES = [8, 16, 24]
+
+# Grids measured through the checkpoint's own image processor; orientation pairs catch h-vs-w swaps.
+# The 4032-patch canvases are 31.5 x 128 rows, so SP=4 skips there (model geometry, not a port bug).
+_CANVAS = {
+ "canvas_1to1": [[1, 48, 48]],
+ "canvas_4to3": [[1, 48, 64]],
+ "canvas_3to4": [[1, 64, 48]],
+ "canvas_16to9": [[1, 48, 84]],
+ "canvas_9to16": [[1, 84, 48]],
+ "canvas_4to1": [[1, 32, 126]], # area cap binds first, short edge forced to 32
+ "canvas_1to4": [[1, 126, 32]],
+}
+_REFERENCE = {
+ "ref_1to1": [[1, 128, 128]],
+ "ref_4to3": [[1, 128, 170]],
+ "ref_3to4": [[1, 170, 128]],
+ "ref_16to9": [[1, 128, 228]],
+ "ref_9to16": [[1, 228, 128]],
+ "ref_4to1": [[1, 128, 512]],
+ "ref_1to4": [[1, 512, 128]],
+}
+# an image is one block, a video one block PER FRAME; a tower ignoring boundaries passes single-block grids
+_MULTI = {
+ "two_refs": [[1, 128, 128], [1, 128, 170]],
+ "video_3_frames": [[3, 48, 48]],
+ "image_and_video": [[1, 128, 128], [3, 48, 48]],
+ "max_load": [[1, 128, 128]] * 9 + [[3, 48, 48]] * 3, # documented ceiling: 9 ref images + 3 ref videos
+}
+
+GRIDS = {**_CANVAS, **_REFERENCE, **_MULTI}
+
+
+@pytest.fixture(scope="module")
+def reference():
+ torch.manual_seed(0)
+ config = vision_config(depth=DEPTH, deepstack_visual_indexes=DEEPSTACK_INDEXES)
+ return transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLVisionModel._from_config(config).eval()
+
+
+def _tower(reference, submesh, parallel_config=None, ccl_manager=None):
+ tower = Qwen3VlVisionModel(
+ hidden_size=HIDDEN_SIZE,
+ num_heads=NUM_HEADS,
+ depth=DEPTH,
+ intermediate_size=INTERMEDIATE_SIZE,
+ spatial_merge_size=SPATIAL_MERGE_SIZE,
+ num_position_embeddings=NUM_POSITION_EMBEDDINGS,
+ out_hidden_size=OUT_HIDDEN_SIZE,
+ hidden_act=HIDDEN_ACT,
+ norm_eps=NORM_EPS,
+ deepstack_visual_indexes=DEEPSTACK_INDEXES,
+ mesh_device=submesh,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+ tower.load_torch_state_dict(reference.state_dict())
+ return tower
+
+
+# perf mode skips the quadratic CPU golden (shapes/finiteness only) and says nothing about accuracy
+_PERF_GRIDS = ("max_load", "ref_4to1", "ref_1to4", "image_and_video")
+assert all(name in GRIDS for name in _PERF_GRIDS)
+_CASES = [pytest.param(name, True, id=f"check-{name}") for name in GRIDS] + [
+ pytest.param(name, False, id=f"perf-{name}") for name in _PERF_GRIDS
+]
+
+
+@VISION_PARAMS
+@pytest.mark.parametrize(("name", "check_pcc"), _CASES)
+def test_tower_on_device(reference, mesh_device, submesh_shape, tp_axis, sp_axis, num_links, name, check_pcc):
+ """The full tower through both outputs: merged tokens and one deepstack feature per tap."""
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ grid = torch.tensor(GRIDS[name], dtype=torch.long)
+ total = sum(t * h * w for t, h, w in GRIDS[name])
+ skip_if_sp_misaligned(total, submesh, sp_axis)
+ patch_dim = 3 * 2 * 16 * 16
+
+ torch.manual_seed(0)
+ patches = torch.randn(total, patch_dim)
+ golden_tokens, golden_deepstack = None, None
+ if check_pcc:
+ with torch.no_grad():
+ ref_out = reference(patches, grid_thw=grid, return_dict=True)
+ golden_tokens = ref_out.pooler_output.float()
+ golden_deepstack = [f.float() for f in ref_out.deepstack_features]
+ assert len(golden_deepstack) == len(DEEPSTACK_INDEXES), "reference did not emit one feature per index"
+
+ cu_seqlens = vision_cu_seqlens(grid)
+ assert len(cu_seqlens) - 1 == int(grid[:, 0].sum()), f"expected one block per frame, got {cu_seqlens}"
+ assert cu_seqlens[0] == 0 and cu_seqlens[-1] == total, f"cu_seqlens must span [0, {total}]: {cu_seqlens}"
+
+ tower = _tower(reference, submesh, *resolve_parallel(submesh, tp_axis, sp_axis, num_links))
+ cos, sin = tower.prepare_rope(grid)
+ pos = tower.prepare_pos_embeds(grid)
+ tokens, deepstack = tower.forward(
+ sp_shard(patches, submesh, sp_axis),
+ pos_embeds=sp_shard(pos, submesh, sp_axis),
+ rope=(sp_shard(cos, submesh, sp_axis), sp_shard(sin, submesh, sp_axis)),
+ cu_seqlens=cu_seqlens,
+ )
+
+ merged = total // SPATIAL_MERGE_SIZE**2
+ actual_tokens = tensor.to_torch(tokens, mesh_axes=[None, None])
+ assert actual_tokens.shape[-2:] == (merged, OUT_HIDDEN_SIZE), f"{tuple(actual_tokens.shape)}"
+ assert len(deepstack) == len(DEEPSTACK_INDEXES), f"expected {len(DEEPSTACK_INDEXES)} features"
+ features = [tensor.to_torch(f, mesh_axes=[None, None]) for f in deepstack]
+ for i, feature in enumerate(features):
+ assert feature.shape[-2:] == (merged, OUT_HIDDEN_SIZE), f"deepstack {i}: {tuple(feature.shape)}"
+
+ if check_pcc:
+ assert_quality(golden_tokens, actual_tokens, pcc=0.99)
+ for golden_feature, feature in zip(golden_deepstack, features):
+ assert_quality(golden_feature, feature, pcc=0.99)
+ else:
+ assert torch.isfinite(actual_tokens).all(), "merged tokens contain NaN or Inf"
+ for i, feature in enumerate(features):
+ assert torch.isfinite(feature).all(), f"deepstack {i} contains NaN or Inf"
+
+ # Routing by list index can yield the right count and shapes while tapping one layer twice.
+ for i in range(len(features) - 1):
+ assert not torch.allclose(
+ features[i], features[i + 1], atol=1e-2
+ ), f"deepstack features {i} and {i + 1} are identical; layer routing is wrong"
diff --git a/code/models/tt_dit/tests/encoders/t5/test_t5_embeddings.py b/code/models/tt_dit/tests/encoders/t5/test_t5_embeddings.py
new file mode 100644
index 0000000000000000000000000000000000000000..6efa05e1a6334adfe6b735628acbd29b25bb434b
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/t5/test_t5_embeddings.py
@@ -0,0 +1,150 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import sys
+import time
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from loguru import logger
+from transformers.models.t5.modeling_t5 import T5EncoderModel
+
+import ttnn
+from models.tt_dit.encoders.t5.model_t5 import RelativePositionEmbeddings, T5Config
+from models.tt_dit.layers.embeddings import Embedding
+from models.tt_dit.parallel.config import EncoderParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+
+
+@pytest.mark.parametrize(
+ "model_name",
+ [
+ "large",
+ ],
+)
+@pytest.mark.parametrize(
+ "mesh_device,submesh_shape", [[(2, 4), (1, 4)], [(4, 8), (1, 8)]], ids=["t3k", "glx"], indirect=["mesh_device"]
+)
+@pytest.mark.parametrize(
+ "device_params, topology",
+ [[{"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, ttnn.Topology.Linear]],
+ indirect=["device_params"],
+)
+def test_t5_embeddings(
+ *,
+ mesh_device: ttnn.Device,
+ submesh_shape: ttnn.MeshShape,
+ model_name: str,
+ topology: ttnn.Topology,
+) -> None:
+ parent_mesh_shape = tuple(mesh_device.shape)
+ if any(x[0] < x[1] for x in zip(parent_mesh_shape, submesh_shape)):
+ pytest.skip("submesh shape is larger than parent mesh shape, skipping")
+ encoder_submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ print(f"Running on submesh {encoder_submesh.shape} of parent mesh {mesh_device.shape}")
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=encoder_submesh.shape[1], mesh_axis=1),
+ )
+ ccl_manager = CCLManager(
+ mesh_device=encoder_submesh,
+ num_links=1,
+ topology=ttnn.Topology.Linear,
+ )
+
+ model_name_checkpoint = f"stabilityai/stable-diffusion-3.5-{model_name}"
+
+ hf_model = T5EncoderModel.from_pretrained(model_name_checkpoint, subfolder="text_encoder_3", local_files_only=True)
+
+ hf_model.eval()
+
+ logger.info("=== HuggingFace T5 Config ===")
+ logger.info(f"vocab_size: {hf_model.config.vocab_size}")
+ logger.info(f"hidden_size: {hf_model.config.d_model}")
+ logger.info(f"intermediate_size: {hf_model.config.d_ff}")
+ logger.info(f"d_kv: {hf_model.config.d_kv}")
+ logger.info(f"num_attention_heads: {hf_model.config.num_heads}")
+ logger.info(f"num_hidden_layers: {hf_model.config.num_layers}")
+ logger.info(f"relative_attention_num_buckets: {hf_model.config.relative_attention_num_buckets}")
+ logger.info(f"relative_attention_max_distance: {hf_model.config.relative_attention_max_distance}")
+ logger.info(f"layer_norm_epsilon: {hf_model.config.layer_norm_epsilon}")
+
+ max_prompt_length = 256
+ torch.manual_seed(0)
+ tokens = torch.randint(hf_model.config.vocab_size, [1, max_prompt_length])
+
+ tt_prompt = ttnn.from_torch(
+ tokens,
+ layout=ttnn.TILE_LAYOUT,
+ device=encoder_submesh,
+ mesh_mapper=ttnn.ReplicateTensorToMesh(encoder_submesh),
+ )
+
+ # === TT-DiT T5 ====
+ config = T5Config(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.d_model,
+ ff_dim=hf_model.config.d_ff,
+ kv_dim=hf_model.config.d_kv,
+ num_heads=hf_model.config.num_heads,
+ num_hidden_layers=hf_model.config.num_layers,
+ max_prompt_length=max_prompt_length,
+ layer_norm_eps=hf_model.config.layer_norm_epsilon,
+ relative_attention_num_buckets=hf_model.config.relative_attention_num_buckets,
+ relative_attention_max_distance=hf_model.config.relative_attention_max_distance,
+ )
+
+ state_dict = hf_model.state_dict()
+
+ tt_token_embed = Embedding(config.vocab_size, config.embed_dim, device=encoder_submesh)
+ tt_token_embed.load_torch_state_dict({"weight": state_dict["encoder.embed_tokens.weight"]})
+ tt_relative_position_embed = RelativePositionEmbeddings(config, encoder_submesh, ccl_manager, parallel_config)
+ tt_relative_position_embed.load_torch_state_dict(
+ {"weight": state_dict["encoder.block.0.layer.0.SelfAttention.relative_attention_bias.weight"]}
+ )
+
+ tt_start_time = time.time()
+ tt_embeddings_output = tt_token_embed(tt_prompt)
+ tt_position_bias = tt_relative_position_embed(tt_embeddings_output.shape[1])
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+
+ with torch.no_grad():
+ hf_start_time = time.time()
+
+ hf_token_embeddings = hf_model.encoder.embed_tokens(tokens)
+
+ hf_position_bias = (
+ hf_model.encoder.block[0]
+ .layer[0]
+ .SelfAttention.compute_bias(
+ hf_token_embeddings.size(1), hf_token_embeddings.size(1), device=hf_token_embeddings.device
+ )
+ )
+
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+
+ # convert mesh tensor to torch tensor for pcc
+ # since weights are replicated, can get the tensor from any single device
+ tt_embeddings_output_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_embeddings_output)[0])
+ mesh_shape = list(encoder_submesh.shape)
+ mesh_shape[1 - parallel_config.tensor_parallel.mesh_axis] = 1
+
+ tt_position_bias_torch = ttnn.to_torch(
+ tt_position_bias,
+ mesh_composer=ttnn.create_mesh_composer(
+ encoder_submesh, ttnn.MeshComposerConfig([0, 1], ttnn.MeshShape(mesh_shape))
+ ), # [0,1] is the mesh dimensions to concatenate. Set replicated dimensions to 1.
+ )
+
+ logger.info(f"TT embeddings execution time: {tt_execution_time:.4f} seconds")
+ logger.info(f"HF embeddings execution time: {hf_execution_time:.4f} seconds")
+
+ assert_quality(hf_token_embeddings, tt_embeddings_output_torch, pcc=0.99)
+ assert_quality(hf_position_bias, tt_position_bias_torch, pcc=0.99)
diff --git a/code/models/tt_dit/tests/encoders/t5/test_t5_encoder_all_layers.py b/code/models/tt_dit/tests/encoders/t5/test_t5_encoder_all_layers.py
new file mode 100644
index 0000000000000000000000000000000000000000..fdd1616db4923dbf89ad6557907797cbaa21c136
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/t5/test_t5_encoder_all_layers.py
@@ -0,0 +1,197 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import sys
+import time
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from loguru import logger
+from transformers.models.t5.modeling_t5 import T5EncoderModel
+
+import ttnn
+from models.tt_dit.encoders.t5.model_t5 import RelativePositionEmbeddings, T5Config, T5EncoderLayer
+from models.tt_dit.layers.embeddings import Embedding
+from models.tt_dit.parallel.config import EncoderParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.substate import substate
+
+
+@pytest.mark.parametrize(
+ "model_name",
+ [
+ "large",
+ ],
+)
+@pytest.mark.parametrize(
+ "mesh_device,submesh_shape", [[(2, 4), (1, 4)], [(4, 8), (1, 8)]], ids=["t3k", "glx"], indirect=["mesh_device"]
+)
+@pytest.mark.parametrize(
+ "device_params, topology",
+ [[{"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, ttnn.Topology.Linear]],
+ indirect=["device_params"],
+)
+def test_t5_layers_individually(
+ *,
+ mesh_device: ttnn.Device,
+ submesh_shape: ttnn.MeshShape,
+ model_name: str,
+ topology: ttnn.Topology,
+) -> None:
+ parent_mesh_shape = tuple(mesh_device.shape)
+ if any(x[0] < x[1] for x in zip(parent_mesh_shape, submesh_shape)):
+ pytest.skip("submesh shape is larger than parent mesh shape, skipping")
+ encoder_submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ print(f"Running on submesh {encoder_submesh.shape} of parent mesh {mesh_device.shape}")
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=encoder_submesh.shape[1], mesh_axis=1),
+ )
+ ccl_manager = CCLManager(
+ mesh_device=encoder_submesh,
+ num_links=1,
+ topology=ttnn.Topology.Linear,
+ )
+
+ model_name_checkpoint = f"stabilityai/stable-diffusion-3.5-{model_name}"
+
+ hf_model = T5EncoderModel.from_pretrained(model_name_checkpoint, subfolder="text_encoder_3", local_files_only=False)
+
+ hf_model.eval()
+
+ logger.info("=== HuggingFace T5 Config ===")
+ logger.info(f"vocab_size: {hf_model.config.vocab_size}")
+ logger.info(f"hidden_size: {hf_model.config.d_model}")
+ logger.info(f"intermediate_size: {hf_model.config.d_ff}")
+ logger.info(f"d_kv: {hf_model.config.d_kv}")
+ logger.info(f"num_attention_heads: {hf_model.config.num_heads}")
+ logger.info(f"num_hidden_layers: {hf_model.config.num_layers}")
+ logger.info(f"relative_attention_num_buckets: {hf_model.config.relative_attention_num_buckets}")
+ logger.info(f"relative_attention_max_distance: {hf_model.config.relative_attention_max_distance}")
+ logger.info(f"layer_norm_epsilon: {hf_model.config.layer_norm_epsilon}")
+
+ max_prompt_length = 256
+ torch.manual_seed(0)
+ tokens = torch.randint(hf_model.config.vocab_size, [1, max_prompt_length])
+
+ tt_prompt = ttnn.from_torch(
+ tokens,
+ layout=ttnn.TILE_LAYOUT,
+ device=encoder_submesh,
+ mesh_mapper=ttnn.ReplicateTensorToMesh(encoder_submesh),
+ )
+
+ # === TT-DiT T5 ====
+ config = T5Config(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.d_model,
+ ff_dim=hf_model.config.d_ff,
+ kv_dim=hf_model.config.d_kv,
+ num_heads=hf_model.config.num_heads,
+ num_hidden_layers=hf_model.config.num_layers,
+ max_prompt_length=max_prompt_length,
+ layer_norm_eps=hf_model.config.layer_norm_epsilon,
+ relative_attention_num_buckets=hf_model.config.relative_attention_num_buckets,
+ relative_attention_max_distance=hf_model.config.relative_attention_max_distance,
+ )
+
+ state_dict = hf_model.state_dict()
+
+ tt_token_embed = Embedding(config.vocab_size, config.embed_dim, device=encoder_submesh)
+ tt_token_embed.load_torch_state_dict({"weight": state_dict["encoder.embed_tokens.weight"]})
+ tt_relative_position_embed = RelativePositionEmbeddings(config, encoder_submesh, ccl_manager, parallel_config)
+ tt_relative_position_embed.load_torch_state_dict(
+ {"weight": state_dict["encoder.block.0.layer.0.SelfAttention.relative_attention_bias.weight"]}
+ )
+
+ # time TT model inference only
+ tt_start_time = time.time()
+ tt_embeddings_output = tt_token_embed(tt_prompt)
+ tt_position_bias = tt_relative_position_embed(tt_embeddings_output.shape[1])
+
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+ logger.info(f"TT embedding execution time: {tt_execution_time:.4f} seconds")
+
+ hf_start_time = time.time()
+ with torch.no_grad():
+ hf_token_embeddings = hf_model.encoder.embed_tokens(tokens)
+
+ # get position bias from first layer's attention (block 0 always has the bias)
+ hf_position_bias = (
+ hf_model.encoder.block[0] # Always use block[0] for position bias
+ .layer[0]
+ .SelfAttention.compute_bias(
+ hf_token_embeddings.size(1), hf_token_embeddings.size(1), device=hf_token_embeddings.device
+ )
+ )
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+ logger.info(f"HF embedding and position bias time: {hf_execution_time:.4f} seconds")
+
+ logger.info("\n=== Testing each layer individually ===")
+ pcc_values = []
+
+ tt_embeddings_output = tt_token_embed(tt_prompt)
+ tt_position_bias = tt_relative_position_embed(tt_embeddings_output.shape[1])
+ with torch.no_grad():
+ hf_token_embeddings = hf_model.encoder.embed_tokens(tokens)
+ hf_position_bias = (
+ hf_model.encoder.block[0] # Always use block[0] for position bias
+ .layer[0]
+ .SelfAttention.compute_bias(
+ hf_token_embeddings.size(1), hf_token_embeddings.size(1), device=hf_token_embeddings.device
+ )
+ )
+
+ for i in range(config.num_hidden_layers):
+ logger.info(f"\nTesting layer {i}")
+ tt_start_time = time.time()
+ layer = i
+
+ # Block 0 is the only one carrying relative_attention_bias in the HF checkpoint, so it
+ # must be built with the bias parameter or the strict state-dict load rejects the key.
+ # This mirrors T5Stack, which passes config.use_relative_position_bias[i].
+ tt_encoder_layer = T5EncoderLayer(
+ config, encoder_submesh, ccl_manager, parallel_config, config.use_relative_position_bias[i]
+ )
+ tt_encoder_layer.load_torch_state_dict(substate(hf_model.state_dict(), f"encoder.block.{layer}"))
+
+ tt_layer_output = tt_encoder_layer(tt_embeddings_output, tt_position_bias)
+
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+ logger.info(f"TT layer {i} execution time: {tt_execution_time:.4f} seconds")
+
+ hf_start_time = time.time()
+
+ hf_layer_output = hf_model.encoder.block[layer](
+ hf_token_embeddings, attention_mask=None, position_bias=hf_position_bias
+ )[0]
+
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+ logger.info(f"HF layer {i} execution time: {hf_execution_time:.4f} seconds")
+
+ tt_layer_output = ttnn.to_torch(ttnn.get_device_tensors(tt_layer_output)[0])
+
+ assert hf_layer_output.shape == tt_layer_output.shape
+
+ try:
+ assert_quality(hf_layer_output, tt_layer_output, pcc=0.947)
+ logger.info(f"Layer {i} passed PCC threshold")
+ except Exception as e:
+ # extract PCC value from error message
+ error_msg = str(e)
+ pcc = float(error_msg.split("%")[0].split("=")[1].strip()) / 100
+ logger.warning(f"Layer {i} failed: PCC = {pcc:.4f}")
+ pcc_values.append(pcc)
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/code/models/tt_dit/tests/encoders/t5/test_t5_encoder_single_layer.py b/code/models/tt_dit/tests/encoders/t5/test_t5_encoder_single_layer.py
new file mode 100644
index 0000000000000000000000000000000000000000..09813d51c4fa31e6507f23eda022e48c1749aebd
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/t5/test_t5_encoder_single_layer.py
@@ -0,0 +1,163 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import sys
+import time
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from loguru import logger
+from transformers.models.t5.modeling_t5 import T5EncoderModel
+
+import ttnn
+from models.tt_dit.encoders.t5.model_t5 import RelativePositionEmbeddings, T5Config, T5EncoderLayer
+from models.tt_dit.layers.embeddings import Embedding
+from models.tt_dit.parallel.config import EncoderParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.substate import substate
+
+
+@pytest.mark.parametrize(
+ "model_name",
+ [
+ "large",
+ ],
+)
+@pytest.mark.parametrize(
+ "mesh_device,submesh_shape", [[(2, 4), (1, 4)], [(4, 8), (1, 8)]], ids=["t3k", "glx"], indirect=["mesh_device"]
+)
+@pytest.mark.parametrize(
+ "device_params, topology",
+ [[{"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, ttnn.Topology.Linear]],
+ indirect=["device_params"],
+)
+def test_t5_single_layer(
+ *,
+ mesh_device: ttnn.Device,
+ submesh_shape: ttnn.MeshShape,
+ model_name: str,
+ topology: ttnn.Topology,
+) -> None:
+ parent_mesh_shape = tuple(mesh_device.shape)
+ if any(x[0] < x[1] for x in zip(parent_mesh_shape, submesh_shape)):
+ pytest.skip("submesh shape is larger than parent mesh shape, skipping")
+ encoder_submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ print(f"Running on submesh {encoder_submesh.shape} of parent mesh {mesh_device.shape}")
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=encoder_submesh.shape[1], mesh_axis=1),
+ )
+ ccl_manager = CCLManager(
+ mesh_device=encoder_submesh,
+ num_links=1,
+ topology=ttnn.Topology.Linear,
+ )
+
+ model_name_checkpoint = f"stabilityai/stable-diffusion-3.5-{model_name}"
+
+ hf_model = T5EncoderModel.from_pretrained(model_name_checkpoint, subfolder="text_encoder_3", local_files_only=True)
+
+ hf_model.eval()
+
+ logger.info("=== HuggingFace T5 Config ===")
+ logger.info(f"vocab_size: {hf_model.config.vocab_size}")
+ logger.info(f"hidden_size: {hf_model.config.d_model}")
+ logger.info(f"intermediate_size: {hf_model.config.d_ff}")
+ logger.info(f"d_kv: {hf_model.config.d_kv}")
+ logger.info(f"num_attention_heads: {hf_model.config.num_heads}")
+ logger.info(f"num_hidden_layers: {hf_model.config.num_layers}")
+ logger.info(f"relative_attention_num_buckets: {hf_model.config.relative_attention_num_buckets}")
+ logger.info(f"relative_attention_max_distance: {hf_model.config.relative_attention_max_distance}")
+ logger.info(f"layer_norm_epsilon: {hf_model.config.layer_norm_epsilon}")
+
+ max_prompt_length = 256
+ torch.manual_seed(0)
+ tokens = torch.randint(hf_model.config.vocab_size, [1, max_prompt_length])
+
+ tt_prompt = ttnn.from_torch(
+ tokens,
+ layout=ttnn.TILE_LAYOUT,
+ device=encoder_submesh,
+ mesh_mapper=ttnn.ReplicateTensorToMesh(encoder_submesh),
+ )
+
+ # === TT-DiT T5 ====
+ config = T5Config(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.d_model,
+ ff_dim=hf_model.config.d_ff,
+ kv_dim=hf_model.config.d_kv,
+ num_heads=hf_model.config.num_heads,
+ num_hidden_layers=hf_model.config.num_layers,
+ max_prompt_length=max_prompt_length,
+ layer_norm_eps=hf_model.config.layer_norm_epsilon,
+ relative_attention_num_buckets=hf_model.config.relative_attention_num_buckets,
+ relative_attention_max_distance=hf_model.config.relative_attention_max_distance,
+ )
+
+ state_dict = hf_model.state_dict()
+
+ tt_token_embed = Embedding(config.vocab_size, config.embed_dim, device=encoder_submesh)
+ tt_token_embed.load_torch_state_dict({"weight": state_dict["encoder.embed_tokens.weight"]})
+ tt_relative_position_embed = RelativePositionEmbeddings(config, encoder_submesh, ccl_manager, parallel_config)
+ tt_relative_position_embed.load_torch_state_dict(
+ {"weight": state_dict["encoder.block.0.layer.0.SelfAttention.relative_attention_bias.weight"]}
+ )
+
+ layer = 23
+
+ tt_encoder_layer = T5EncoderLayer(config, encoder_submesh, ccl_manager, parallel_config)
+
+ tt_encoder_layer.load_torch_state_dict(
+ substate(hf_model.state_dict(), f"encoder.block.{layer}")
+ ) # first layer weights
+
+ tt_start_time = time.time()
+ tt_embeddings_output = tt_token_embed(tt_prompt)
+ tt_position_bias = tt_relative_position_embed(tt_embeddings_output.shape[1])
+ tt_layer_output = tt_encoder_layer(tt_embeddings_output, tt_position_bias)
+
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+
+ with torch.no_grad():
+ hf_start_time = time.time()
+
+ hf_token_embeddings = hf_model.encoder.embed_tokens(tokens)
+
+ # get position bias from first layer's attention (block 0 always has the bias)
+ hf_position_bias = (
+ hf_model.encoder.block[0] # Always use block[0] for position bias
+ .layer[0]
+ .SelfAttention.compute_bias(
+ hf_token_embeddings.size(1), hf_token_embeddings.size(1), device=hf_token_embeddings.device
+ )
+ )
+
+ # run through encoder layer
+ hf_layer_output = hf_model.encoder.block[layer](
+ hf_token_embeddings, attention_mask=None, position_bias=hf_position_bias
+ )[0]
+
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+
+ # convert mesh tensor to torch tensor for pcc
+ # since weights are replicated, can get the tensor from any single device
+ tt_layer_output = ttnn.to_torch(ttnn.get_device_tensors(tt_layer_output)[0])
+
+ logger.info(f"TT layer execution time: {tt_execution_time:.4f} seconds")
+ logger.info(f"HF layer execution time: {hf_execution_time:.4f} seconds")
+
+ assert hf_layer_output.shape == tt_layer_output.shape
+
+ assert_quality(hf_layer_output, tt_layer_output, pcc=0.947)
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/code/models/tt_dit/tests/encoders/t5/test_t5_full.py b/code/models/tt_dit/tests/encoders/t5/test_t5_full.py
new file mode 100644
index 0000000000000000000000000000000000000000..1df8f0cee699f3304e8b3d72c90a07b3ddf3a073
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/t5/test_t5_full.py
@@ -0,0 +1,154 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import os
+import sys
+import time
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[6]))
+
+import pytest
+import torch
+from loguru import logger
+from transformers.models.t5.configuration_t5 import T5Config as HF_T5Config
+from transformers.models.t5.modeling_t5 import T5EncoderModel
+
+import ttnn
+from models.tt_dit.encoders.t5.model_t5 import T5Config, T5Encoder
+from models.tt_dit.parallel.config import EncoderParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+
+
+@pytest.mark.parametrize(
+ "dit_unit_test",
+ [{"1": True, "0": False}.get(os.environ.get("DIT_UNIT_TEST"), False)],
+)
+@pytest.mark.parametrize(
+ "model_name",
+ [
+ "large",
+ ],
+)
+@pytest.mark.parametrize(
+ "mesh_device,submesh_shape", [[(2, 4), (1, 4)], [(4, 8), (1, 8)]], ids=["t3k", "glx"], indirect=["mesh_device"]
+)
+@pytest.mark.parametrize(
+ "device_params, topology",
+ [[{"l1_small_size": 8192, "fabric_config": ttnn.FabricConfig.FABRIC_1D}, ttnn.Topology.Linear]],
+ indirect=["device_params"],
+)
+def test_t5_encoder(
+ *,
+ mesh_device: ttnn.Device,
+ submesh_shape: ttnn.MeshShape,
+ model_name: str,
+ topology: ttnn.Topology,
+ dit_unit_test: bool,
+) -> None:
+ parent_mesh_shape = tuple(mesh_device.shape)
+ if any(x[0] < x[1] for x in zip(parent_mesh_shape, submesh_shape)):
+ pytest.skip("submesh shape is larger than parent mesh shape, skipping")
+ encoder_submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ print(f"Running on submesh {encoder_submesh.shape} of parent mesh {mesh_device.shape}")
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=encoder_submesh.shape[1], mesh_axis=1),
+ )
+ ccl_manager = CCLManager(
+ mesh_device=encoder_submesh,
+ num_links=1,
+ topology=ttnn.Topology.Linear,
+ )
+
+ model_name_checkpoint = f"stabilityai/stable-diffusion-3.5-{model_name}"
+
+ if dit_unit_test:
+ # Config for t5-xxl with minimal layers
+ hf_config = HF_T5Config(
+ d_model=4096,
+ d_ff=10240,
+ num_heads=64,
+ num_layers=2,
+ num_decoder_layers=2,
+ feed_forward_proj="gated-gelu",
+ output_past=True,
+ )
+ hf_model = T5EncoderModel(hf_config)
+ else:
+ hf_model = T5EncoderModel.from_pretrained(
+ model_name_checkpoint, subfolder="text_encoder_3", local_files_only=False
+ )
+ hf_model.eval()
+
+ logger.info("=== HuggingFace T5 Config ===")
+ logger.info(f"vocab_size: {hf_model.config.vocab_size}")
+ logger.info(f"hidden_size: {hf_model.config.d_model}")
+ logger.info(f"intermediate_size: {hf_model.config.d_ff}")
+ logger.info(f"d_kv: {hf_model.config.d_kv}")
+ logger.info(f"num_attention_heads: {hf_model.config.num_heads}")
+ logger.info(f"num_hidden_layers: {hf_model.config.num_layers}")
+ logger.info(f"relative_attention_num_buckets: {hf_model.config.relative_attention_num_buckets}")
+ logger.info(f"relative_attention_max_distance: {hf_model.config.relative_attention_max_distance}")
+ logger.info(f"layer_norm_epsilon: {hf_model.config.layer_norm_epsilon}")
+
+ max_prompt_length = 256
+ torch.manual_seed(0)
+ tokens = torch.randint(hf_model.config.vocab_size, [1, max_prompt_length])
+
+ tt_prompt = ttnn.from_torch(
+ tokens,
+ layout=ttnn.TILE_LAYOUT,
+ device=encoder_submesh,
+ mesh_mapper=ttnn.ReplicateTensorToMesh(encoder_submesh),
+ )
+
+ # === TT-DiT T5 ====
+ config = T5Config(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.d_model,
+ ff_dim=hf_model.config.d_ff,
+ kv_dim=hf_model.config.d_kv,
+ num_heads=hf_model.config.num_heads,
+ num_hidden_layers=hf_model.config.num_layers,
+ max_prompt_length=max_prompt_length,
+ layer_norm_eps=hf_model.config.layer_norm_epsilon,
+ relative_attention_num_buckets=hf_model.config.relative_attention_num_buckets,
+ relative_attention_max_distance=hf_model.config.relative_attention_max_distance,
+ )
+
+ tt_encoder = T5Encoder(config, encoder_submesh, ccl_manager, parallel_config)
+ tt_encoder.load_torch_state_dict(hf_model.state_dict())
+
+ # time TT model inference only
+ tt_start_time = time.time()
+ tt_output = tt_encoder(tt_prompt)
+
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+
+ # get HF reference outputs
+ with torch.no_grad():
+ hf_start_time = time.time()
+
+ hf_outputs = hf_model(tokens).last_hidden_state
+
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+
+ # convert mesh tensor to torch tensor for pcc
+ # since weights are replicated, can get the tensor from any single device
+ tt_output_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_output[-1])[0])
+
+ logger.info(f"TT encoder execution time: {tt_execution_time:.4f} seconds")
+ logger.info(f"HF encoder execution time: {hf_execution_time:.4f} seconds")
+
+ assert hf_outputs.shape == tt_output_torch.shape
+
+ assert_quality(hf_outputs, tt_output_torch, pcc=0.99)
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/code/models/tt_dit/tests/encoders/umt5/test_umt5.py b/code/models/tt_dit/tests/encoders/umt5/test_umt5.py
new file mode 100644
index 0000000000000000000000000000000000000000..c5778d8921f8c6c3a8114c8f81372c9ef2ea032b
--- /dev/null
+++ b/code/models/tt_dit/tests/encoders/umt5/test_umt5.py
@@ -0,0 +1,304 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import os
+import time
+
+import pytest
+import torch
+from loguru import logger
+from transformers import AutoTokenizer
+from transformers.models.umt5.configuration_umt5 import UMT5Config as HF_UMT5Config
+from transformers.models.umt5.modeling_umt5 import UMT5EncoderModel
+
+import ttnn
+from models.perf.benchmarking_utils import BenchmarkProfiler
+
+from ....encoders.t5.model_t5 import RelativePositionEmbeddings as TT_UMT5RelativePositionEmbeddings
+from ....encoders.umt5.model_umt5 import UMT5Config as TT_UMT5Config
+from ....encoders.umt5.model_umt5 import UMT5Encoder as TT_UMT5Encoder
+from ....layers.embeddings import Embedding
+from ....parallel.config import EncoderParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils import cache
+from ....utils.check import assert_quality
+
+# Shared device configuration for UMT5 tests to reduce code duplication and ensure we are using consistent device configurations.
+
+
+def umt5_device_config(func):
+ """Decorator to apply standard UMT5 device/mesh parametrization to tests."""
+ func = pytest.mark.parametrize(
+ "dit_unit_test",
+ [{"1": True, "0": False}.get(os.environ.get("DIT_UNIT_TEST"), False)],
+ )(func)
+ func = pytest.mark.parametrize(
+ "mesh_device, num_links, max_execution_time",
+ [[(2, 4), 1, 0.13], [(4, 8), 4, 0.1], [(4, 8), 2, 0.1]],
+ ids=["t3k", "wh_glx", "bh_glx"],
+ indirect=["mesh_device", "num_links"],
+ )(func)
+ func = pytest.mark.parametrize(
+ "device_params",
+ [{"fabric_config": ttnn.FabricConfig.FABRIC_1D}],
+ indirect=True,
+ )(func)
+ return func
+
+
+@pytest.fixture
+def num_links(request):
+ return request.param
+
+
+@pytest.fixture
+def parallel_config_and_ccl_manager(mesh_device, num_links):
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=mesh_device.shape[1], mesh_axis=1),
+ )
+ ccl_manager = CCLManager(
+ mesh_device=mesh_device,
+ num_links=num_links,
+ topology=ttnn.Topology.Linear,
+ )
+ return parallel_config, ccl_manager
+
+
+def get_random_weights_model() -> UMT5EncoderModel:
+ hf_config = HF_UMT5Config(
+ d_model=4096,
+ d_ff=10240,
+ num_heads=64,
+ num_layers=24,
+ num_decoder_layers=0,
+ relative_attention_max_distance=128,
+ relative_attention_num_buckets=32,
+ feed_forward_proj="gated-gelu",
+ output_past=True,
+ )
+ return UMT5EncoderModel(hf_config)
+
+
+@umt5_device_config
+def test_umt5_embeddings(
+ *,
+ mesh_device: ttnn.Device,
+ parallel_config_and_ccl_manager: tuple,
+ dit_unit_test: bool,
+ max_execution_time: float,
+) -> None:
+ torch.manual_seed(0)
+
+ parallel_config, ccl_manager = parallel_config_and_ccl_manager
+
+ if dit_unit_test:
+ hf_model = get_random_weights_model()
+ else:
+ model_name_checkpoint = f"Wan-AI/Wan2.2-T2V-A14B-Diffusers"
+ hf_model = UMT5EncoderModel.from_pretrained(
+ model_name_checkpoint, subfolder="text_encoder", local_files_only=False
+ )
+
+ hf_model.eval()
+
+ logger.info("=== HuggingFace UMT5 Config ===")
+ logger.info(f"vocab_size: {hf_model.config.vocab_size}")
+ logger.info(f"hidden_size: {hf_model.config.d_model}")
+ logger.info(f"intermediate_size: {hf_model.config.d_ff}")
+ logger.info(f"d_kv: {hf_model.config.d_kv}")
+ logger.info(f"num_attention_heads: {hf_model.config.num_heads}")
+ logger.info(f"num_hidden_layers: {hf_model.config.num_layers}")
+ logger.info(f"relative_attention_num_buckets: {hf_model.config.relative_attention_num_buckets}")
+ logger.info(f"relative_attention_max_distance: {hf_model.config.relative_attention_max_distance}")
+ logger.info(f"layer_norm_epsilon: {hf_model.config.layer_norm_epsilon}")
+
+ max_prompt_length = 512
+ tokens = torch.randint(hf_model.config.vocab_size, [1, max_prompt_length])
+
+ tt_prompt = ttnn.from_torch(
+ tokens,
+ layout=ttnn.TILE_LAYOUT,
+ device=mesh_device,
+ mesh_mapper=ttnn.ReplicateTensorToMesh(mesh_device),
+ )
+
+ # === TT-DiT UMT5 ====
+ config = TT_UMT5Config(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.d_model,
+ ff_dim=hf_model.config.d_ff,
+ kv_dim=hf_model.config.d_kv,
+ num_heads=hf_model.config.num_heads,
+ num_hidden_layers=hf_model.config.num_layers,
+ max_prompt_length=max_prompt_length,
+ layer_norm_eps=hf_model.config.layer_norm_epsilon,
+ relative_attention_num_buckets=hf_model.config.relative_attention_num_buckets,
+ relative_attention_max_distance=hf_model.config.relative_attention_max_distance,
+ )
+
+ state_dict = hf_model.state_dict()
+
+ tt_token_embed = Embedding(config.vocab_size, config.embed_dim, device=mesh_device)
+ tt_token_embed.load_torch_state_dict({"weight": state_dict["encoder.embed_tokens.weight"]})
+ tt_relative_position_embed = TT_UMT5RelativePositionEmbeddings(config, mesh_device, ccl_manager, parallel_config)
+ tt_relative_position_embed.load_torch_state_dict(
+ {"weight": state_dict["encoder.block.0.layer.0.SelfAttention.relative_attention_bias.weight"]}
+ )
+
+ tt_start_time = time.time()
+ tt_embeddings_output = tt_token_embed(tt_prompt)
+ tt_position_bias = tt_relative_position_embed(tt_embeddings_output.shape[1])
+ tt_end_time = time.time()
+ tt_execution_time = tt_end_time - tt_start_time
+
+ with torch.no_grad():
+ hf_start_time = time.time()
+
+ hf_token_embeddings = hf_model.encoder.embed_tokens(tokens)
+
+ hf_position_bias = (
+ hf_model.encoder.block[0]
+ .layer[0]
+ .SelfAttention.compute_bias(
+ hf_token_embeddings.size(1), hf_token_embeddings.size(1), device=hf_token_embeddings.device
+ )
+ )
+
+ hf_end_time = time.time()
+ hf_execution_time = hf_end_time - hf_start_time
+
+ # convert mesh tensor to torch tensor for pcc
+ # since weights are replicated, can get the tensor from any single device
+ tt_embeddings_output_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_embeddings_output)[0])
+ mesh_shape = list(mesh_device.shape)
+ mesh_shape[1 - parallel_config.tensor_parallel.mesh_axis] = 1
+
+ tt_position_bias_torch = ttnn.to_torch(
+ tt_position_bias,
+ mesh_composer=ttnn.create_mesh_composer(
+ mesh_device, ttnn.MeshComposerConfig([0, 1], ttnn.MeshShape(mesh_shape))
+ ), # [0,1] is the mesh dimensions to concatenate. Set replicated dimensions to 1.
+ )
+
+ logger.info(f"TT embeddings execution time: {tt_execution_time:.4f} seconds")
+ logger.info(f"HF embeddings execution time: {hf_execution_time:.4f} seconds")
+
+ assert_quality(hf_token_embeddings, tt_embeddings_output_torch, pcc=(1.0 - 1e-5), relative_rmse=0.005)
+ assert_quality(hf_position_bias, tt_position_bias_torch, pcc=(1.0 - 1e-5), relative_rmse=0.005)
+
+
+@umt5_device_config
+def test_umt5_encoder(
+ *,
+ mesh_device: ttnn.Device,
+ parallel_config_and_ccl_manager: tuple,
+ dit_unit_test: bool,
+ max_execution_time: float,
+) -> None:
+ torch.manual_seed(0)
+
+ parallel_config, ccl_manager = parallel_config_and_ccl_manager
+
+ model_name_checkpoint = f"Wan-AI/Wan2.2-T2V-A14B-Diffusers"
+
+ if dit_unit_test:
+ hf_model = get_random_weights_model()
+ else:
+ hf_model = UMT5EncoderModel.from_pretrained(
+ model_name_checkpoint, subfolder="text_encoder", local_files_only=False
+ )
+ hf_model.eval()
+
+ max_prompt_length = 512
+
+ prompt = [
+ "A Roman general standing on a battlefield at dawn, torn red cape blowing in the wind, distant soldiers forming ranks, painterly brushwork in the style of Caravaggio, chiaroscuro lighting, epic composition",
+ "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
+ ]
+ total_prompts = len(prompt)
+ num_devices = mesh_device.shape[1 - parallel_config.tensor_parallel.mesh_axis]
+ prompt += [" "] * ((num_devices - (total_prompts % num_devices)) % num_devices)
+ tokenizer = AutoTokenizer.from_pretrained(model_name_checkpoint, subfolder="tokenizer", trust_remote_code=True)
+ text_inputs = tokenizer(
+ prompt,
+ padding="max_length",
+ max_length=max_prompt_length,
+ truncation=True,
+ add_special_tokens=True,
+ return_attention_mask=True,
+ return_tensors="pt",
+ )
+ text_input_ids, mask = text_inputs.input_ids, text_inputs.attention_mask
+
+ dims = [None, None]
+ DP_axis = 1 - parallel_config.tensor_parallel.mesh_axis
+ dims[DP_axis] = 0
+ mesh_mapper = ttnn.ShardTensor2dMesh(mesh_device, mesh_shape=tuple(mesh_device.shape), dims=dims)
+
+ tt_prompt = ttnn.from_torch(
+ text_input_ids,
+ dtype=ttnn.uint32,
+ layout=ttnn.TILE_LAYOUT,
+ device=mesh_device,
+ mesh_mapper=mesh_mapper,
+ )
+
+ tt_mask = ttnn.from_torch(
+ mask,
+ dtype=ttnn.bfloat16,
+ layout=ttnn.TILE_LAYOUT,
+ device=mesh_device,
+ mesh_mapper=mesh_mapper,
+ )
+
+ # === TT-DiT UMT5 ====
+ config = TT_UMT5Config(
+ vocab_size=hf_model.config.vocab_size,
+ embed_dim=hf_model.config.d_model,
+ ff_dim=hf_model.config.d_ff,
+ kv_dim=hf_model.config.d_kv,
+ num_heads=hf_model.config.num_heads,
+ num_hidden_layers=hf_model.config.num_layers,
+ max_prompt_length=max_prompt_length,
+ layer_norm_eps=hf_model.config.layer_norm_epsilon,
+ relative_attention_num_buckets=hf_model.config.relative_attention_num_buckets,
+ relative_attention_max_distance=hf_model.config.relative_attention_max_distance,
+ )
+
+ tt_encoder = TT_UMT5Encoder(config, mesh_device, ccl_manager, parallel_config)
+ cache.load_model(
+ tt_encoder,
+ model_name=model_name_checkpoint,
+ subfolder="text_encoder",
+ parallel_config=parallel_config,
+ mesh_shape=tuple(mesh_device.shape),
+ mesh_device=mesh_device,
+ get_torch_state_dict=lambda: hf_model.state_dict(),
+ )
+
+ # warmup
+ tt_output = tt_encoder(tt_prompt, attention_mask=tt_mask)
+ ttnn.synchronize_device(mesh_device) # wait for all operations to complete
+ benchmark_profiler = BenchmarkProfiler()
+ num_runs = 10
+ for i in range(num_runs):
+ with benchmark_profiler("tt_umt5_encoder", i):
+ tt_output = tt_encoder(tt_prompt, attention_mask=tt_mask)[-1]
+ tt_output = ccl_manager.all_gather(tt_output, dim=0, mesh_axis=DP_axis, use_hyperparams=True)
+ # wait for all operations to complete to get correct dispatch + execution time
+ ttnn.synchronize_device(mesh_device)
+
+ # get HF reference outputs
+ with torch.no_grad():
+ hf_outputs = hf_model(text_input_ids, attention_mask=mask).last_hidden_state
+
+ # # convert mesh tensor to torch tensor for pcc
+ # # since weights are replicated, can get the tensor from any single device
+ tt_output_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_output)[0])
+ tt_execution_time = benchmark_profiler.get_duration_average("tt_umt5_encoder")
+ logger.info(f"TT encoder execution time benchmark: {tt_execution_time:.4f} seconds")
+ assert_quality(hf_outputs[:total_prompts], tt_output_torch[:total_prompts], pcc=0.99, relative_rmse=0.06)
+ # assert (
+ # tt_execution_time < max_execution_time
+ # ), f"TT Encoder execution time {tt_execution_time:.4f} seconds is greater than the max {max_execution_time} seconds"
diff --git a/code/models/tt_dit/tests/models/flux1/scaleout_configs/single_galaxy/mesh_graph_descriptor.textproto b/code/models/tt_dit/tests/models/flux1/scaleout_configs/single_galaxy/mesh_graph_descriptor.textproto
new file mode 100644
index 0000000000000000000000000000000000000000..93ee3b534ab562f8690393dc46523589b6dbba6a
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux1/scaleout_configs/single_galaxy/mesh_graph_descriptor.textproto
@@ -0,0 +1,17 @@
+# Single-rank single-galaxy mesh for the Flux multihost CI experiment.
+# Copied from models/demos/deepseek_v3_b1/scaleout_configs/rev_c/one_galaxy, which is proven to
+# load and map on the bh_sc1 (single BH galaxy) SKU. One connected local 4x2 BLACKHOLE block
+# (8 devices) owned by a single rank.
+
+mesh_descriptors {
+ name: "M0"
+ arch: BLACKHOLE
+ device_topology { dims: [ 4, 2 ] dim_types: [ RING, LINE ] }
+ host_topology { dims: [ 1, 1 ] }
+ channels {
+ count: 2
+ policy: RELAXED
+ }
+}
+
+top_level_instance { mesh { mesh_descriptor: "M0" mesh_id: 0 } }
diff --git a/code/models/tt_dit/tests/models/flux1/test_attention_flux1.py b/code/models/tt_dit/tests/models/flux1/test_attention_flux1.py
new file mode 100644
index 0000000000000000000000000000000000000000..080ec8d30ce7fc01195003ff21501187a2df2ec6
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux1/test_attention_flux1.py
@@ -0,0 +1,161 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import diffusers.models.attention_processor
+import pytest
+import torch
+
+import ttnn
+
+from ....blocks.attention import Attention
+from ....parallel.config import DiTParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils import tensor
+from ....utils.check import assert_quality
+from ....utils.padding import PaddingConfig
+from ....utils.tensor import bf16_tensor
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis"),
+ [
+ pytest.param((1, 1), 0, 1, id="1x1sp0tp1"),
+ pytest.param((1, 2), 0, 1, id="1x2sp0tp1"),
+ pytest.param((1, 2), 1, 0, id="1x2sp1tp0"),
+ pytest.param((2, 1), 0, 1, id="2x1sp0tp1"),
+ pytest.param((2, 1), 1, 0, id="2x1sp1tp0"),
+ pytest.param((2, 2), 0, 1, id="2x2sp0tp1"),
+ pytest.param((2, 2), 1, 0, id="2x2sp1tp0"),
+ pytest.param((2, 4), 0, 1, id="2x4sp0tp1"),
+ pytest.param((2, 4), 1, 0, id="2x4sp1tp0"),
+ pytest.param((4, 4), 0, 1, id="4x4sp0tp1"),
+ pytest.param((4, 8), 0, 1, id="4x8sp0tp1"),
+ pytest.param((4, 8), 1, 0, id="4x8sp1tp0"),
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ ("batch_size", "spatial_seq_len", "prompt_seq_len"),
+ [
+ (1, 4096, 512),
+ # (1, 4096 + 512, 0),
+ ],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [
+ {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "require_exact_physical_num_devices": True,
+ }
+ ],
+ indirect=True,
+)
+def test_attention_flux1(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ sp_axis: int,
+ tp_axis: int,
+ batch_size: int,
+ spatial_seq_len: int,
+ prompt_seq_len: int,
+) -> None:
+ torch.manual_seed(0)
+
+ query_dim = 3072
+ added_kv_proj_dim = 3072
+ head_dim = 128
+ num_heads = 24
+ out_dim = 3072
+ context_pre_only = prompt_seq_len == 0
+ pre_only = prompt_seq_len == 0
+ batch_size = 1
+
+ joint_attention = added_kv_proj_dim != 0
+
+ sp_factor = mesh_device.shape[sp_axis]
+ tp_factor = mesh_device.shape[tp_axis]
+
+ torch_model = diffusers.models.attention_processor.Attention(
+ query_dim=query_dim,
+ added_kv_proj_dim=added_kv_proj_dim if added_kv_proj_dim != 0 else None,
+ dim_head=head_dim,
+ heads=num_heads,
+ out_dim=out_dim,
+ context_pre_only=context_pre_only,
+ pre_only=pre_only,
+ bias=True,
+ qk_norm="rms_norm",
+ eps=1e-6,
+ processor=diffusers.models.attention_processor.FluxAttnProcessor2_0(), # type: ignore[arg-type]
+ )
+ torch_model.eval()
+
+ ccl_manager = CCLManager(
+ mesh_device=mesh_device,
+ num_links=1,
+ topology=ttnn.Topology.Linear,
+ )
+
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=0, mesh_axis=0),
+ tensor_parallel=ParallelFactor(factor=tp_factor, mesh_axis=tp_axis),
+ sequence_parallel=ParallelFactor(factor=sp_factor, mesh_axis=sp_axis),
+ )
+
+ if num_heads % tp_factor != 0:
+ padding_config = PaddingConfig.from_tensor_parallel_factor(num_heads, head_dim, tp_factor)
+ else:
+ padding_config = None
+
+ tt_model = Attention(
+ query_dim=query_dim,
+ head_dim=head_dim,
+ heads=num_heads,
+ out_dim=out_dim,
+ added_kv_proj_dim=added_kv_proj_dim,
+ context_pre_only=context_pre_only,
+ pre_only=pre_only,
+ eps=1e-6,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ padding_config=padding_config,
+ )
+ tt_model.load_torch_state_dict(torch_model.state_dict())
+
+ spatial = torch.randn((batch_size, spatial_seq_len, query_dim))
+ prompt = torch.randn((batch_size, prompt_seq_len, query_dim)) if joint_attention else None
+ rope_cos = torch.randn([spatial_seq_len + prompt_seq_len, 128])
+ rope_sin = torch.randn([spatial_seq_len + prompt_seq_len, 128])
+
+ spatial_padded = tt_model.pad_spatial_sequence(spatial, sp_factor=sp_factor)
+ spatial_rope_cos_padded = tt_model.pad_spatial_sequence(rope_cos[prompt_seq_len:], sp_factor=sp_factor)
+ spatial_rope_sin_padded = tt_model.pad_spatial_sequence(rope_sin[prompt_seq_len:], sp_factor=sp_factor)
+
+ tt_spatial = bf16_tensor(spatial_padded, device=mesh_device, mesh_axis=sp_axis, shard_dim=-2)
+ tt_prompt = bf16_tensor(prompt, device=mesh_device) if prompt is not None else None
+ tt_spatial_rope_cos = bf16_tensor(spatial_rope_cos_padded, device=mesh_device, mesh_axis=sp_axis, shard_dim=-2)
+ tt_spatial_rope_sin = bf16_tensor(spatial_rope_sin_padded, device=mesh_device, mesh_axis=sp_axis, shard_dim=-2)
+ tt_prompt_rope_cos = bf16_tensor(rope_cos[:prompt_seq_len], device=mesh_device) if joint_attention else None
+ tt_prompt_rope_sin = bf16_tensor(rope_sin[:prompt_seq_len], device=mesh_device) if joint_attention else None
+
+ with torch.no_grad():
+ torch_output = torch_model.forward(spatial, prompt, image_rotary_emb=(rope_cos, rope_sin))
+ torch_spatial, torch_prompt = torch_output if joint_attention else (torch_output, None)
+
+ tt_spatial_out, tt_prompt_out = tt_model.forward(
+ spatial=tt_spatial,
+ prompt=tt_prompt,
+ spatial_rope=(tt_spatial_rope_cos, tt_spatial_rope_sin),
+ prompt_rope=(tt_prompt_rope_cos, tt_prompt_rope_sin) if joint_attention else None,
+ spatial_sequence_length=spatial_seq_len,
+ )
+
+ tt_spatial_torch = tensor.to_torch(tt_spatial_out, mesh_axes=[None, sp_axis, tp_axis])
+ assert_quality(torch_spatial, tt_spatial_torch, pcc=0.995, relative_rmse=0.13)
+
+ if torch_prompt is not None:
+ tt_prompt_torch = tensor.to_torch(tt_prompt_out, mesh_axes=[None, None, tp_axis])
+ assert_quality(torch_prompt, tt_prompt_torch, pcc=0.995, relative_rmse=0.13)
diff --git a/code/models/tt_dit/tests/models/flux1/test_performance_flux1.py b/code/models/tt_dit/tests/models/flux1/test_performance_flux1.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e646e508ea11da1aa5cb6fafb868ce5c730301b
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux1/test_performance_flux1.py
@@ -0,0 +1,525 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import statistics
+
+import pytest
+from loguru import logger
+
+import ttnn
+from models.common.utility_functions import is_blackhole
+from models.perf.benchmarking_utils import BenchmarkData, BenchmarkProfiler
+
+from ....parallel.config import DiTParallelConfig, EncoderParallelConfig, VAEParallelConfig
+from ....pipelines.events import profiler_event_callback
+from ....pipelines.flux1.pipeline_flux1 import Flux1Pipeline, Flux1PipelineConfig
+from ...dataset_eval.clip_encoder import CLIPEncoder
+
+# Test prompts - diverse set for comprehensive performance testing
+TEST_PROMPTS = [
+ """A neon-lit alley in a sprawling cyberpunk metropolis at night, rain-slick streets reflecting glowing holograms, dense atmosphere, flying cars in the sky, people in high-tech streetwear — ultra-detailed, cinematic lighting, 4K""",
+ """A colossal whale floating through a desert sky like a blimp, casting a long shadow over sand dunes, people in ancient robes watching in awe, golden hour lighting, dreamlike color palette — surrealism, concept art, Greg Rutkowski style""",
+ """A Roman general standing on a battlefield at dawn, torn red cape blowing in the wind, distant soldiers forming ranks, painterly brushwork in the style of Caravaggio, chiaroscuro lighting, epic composition""",
+ """A tiny, fluffy dragon curled up in a teacup, warm cozy lighting, big expressive eyes, intricate scale patterns, surrounded by books and potions — high detail, Studio Ghibli meets Pixar""",
+ """An epic, high-definition cinematic shot of a rustic snowy cabin glowing warmly at dusk, nestled in a serene winter landscape. Surrounded by gentle snow-covered pines and delicate falling snowflakes — captured in a rich, atmospheric, wide-angle scene with deep cinematic depth and warmth.""",
+]
+
+PERF_TEST_IMAGE_PARAMS = [
+ (1024, 1024, 3.5, 28),
+]
+
+PERF_TEST_MESH_PARAMS = [
+ pytest.param((1, 2), (1, 0), (2, 1), (2, 1), (2, 1), ttnn.Topology.Linear, 2, id="1x2sp0tp1"),
+ pytest.param((2, 2), (2, 0), (2, 1), (2, 1), (2, 1), ttnn.Topology.Linear, 2, id="2x2sp0tp1"),
+ pytest.param((2, 4), (2, 0), (4, 1), (4, 1), (4, 1), ttnn.Topology.Linear, 1, id="wh_2x4sp0tp1"),
+ pytest.param((2, 4), (2, 0), (4, 1), (4, 1), (4, 1), ttnn.Topology.Linear, 2, id="bh_2x4sp0tp1"),
+ pytest.param((4, 8), (4, 0), (8, 1), (4, 0), (4, 0), ttnn.Topology.Linear, 4, id="wh_4x8sp0tp1"),
+ pytest.param((4, 8), (4, 0), (8, 1), (4, 0), (4, 0), ttnn.Topology.Linear, 2, id="bh_4x8sp0tp1"),
+]
+
+PERF_TEST_DEVICE_PARAMS = [
+ {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "l1_small_size": 32768,
+ "trace_region_size": 51000000,
+ "require_exact_physical_num_devices": True,
+ }
+]
+
+PERF_TEST_MODEL_VARIANTS = [
+ pytest.param("schnell", id="flux_schnell"),
+ pytest.param("dev", id="flux_dev"),
+]
+
+
+@pytest.mark.parametrize(
+ "model_variant",
+ PERF_TEST_MODEL_VARIANTS,
+)
+@pytest.mark.parametrize(
+ "image_w, image_h, guidance_scale, num_inference_steps",
+ PERF_TEST_IMAGE_PARAMS,
+)
+@pytest.mark.parametrize(
+ "mesh_device, sp, tp, encoder_tp, vae_tp, topology, num_links",
+ PERF_TEST_MESH_PARAMS,
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ PERF_TEST_DEVICE_PARAMS,
+ indirect=True,
+)
+def test_flux1_pipeline_performance_speed(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ image_w,
+ image_h,
+ guidance_scale,
+ num_inference_steps,
+ sp,
+ tp,
+ encoder_tp,
+ vae_tp,
+ topology,
+ num_links,
+ model_variant,
+ model_location_generator,
+ is_ci_env,
+ galaxy_type,
+) -> None:
+ """Performance test for Flux.1 pipeline with detailed timing analysis."""
+
+ benchmark_profiler = BenchmarkProfiler()
+
+ # Skip 4U.
+ if galaxy_type == "4U":
+ # NOTE: Pipelines fail if a performance test is skipped without providing a benchmark output.
+ if is_ci_env:
+ with benchmark_profiler("run", iteration=0):
+ pass
+
+ benchmark_data = BenchmarkData()
+ benchmark_data.save_partial_run_json(
+ benchmark_profiler,
+ run_type="empty_run",
+ ml_model_name="empty_run",
+ )
+ pytest.skip("4U is not supported for this test")
+
+ logger.info(f" Image size: {image_w}x{image_h}")
+ logger.info(f" Guidance scale: {guidance_scale}")
+ logger.info(f" Inference steps: {num_inference_steps}")
+
+ pipeline = Flux1Pipeline(
+ device=mesh_device,
+ config=Flux1PipelineConfig.default(
+ mesh_shape=mesh_device.shape,
+ dit_parallel_config=DiTParallelConfig.from_tuples(cfg=(1, 0), sp=sp, tp=tp),
+ encoder_parallel_config=EncoderParallelConfig.from_tuple(encoder_tp),
+ vae_parallel_config=VAEParallelConfig.from_tuple(vae_tp),
+ topology=topology,
+ num_links=num_links,
+ width=image_w,
+ height=image_h,
+ checkpoint_name=model_location_generator(f"black-forest-labs/FLUX.1-{model_variant}"),
+ ),
+ )
+
+ # Warmup runs
+ for i in range(2):
+ logger.info("Running warmup iteration...")
+ with benchmark_profiler("run", iteration=0):
+ images = pipeline.run_single_prompt(
+ prompt=TEST_PROMPTS[0],
+ num_inference_steps=num_inference_steps,
+ )
+ images[0].save(f"flux1_{model_variant}_{image_w}_{image_h}_warmup_{i}.png")
+ logger.info(f"Warmup completed in {benchmark_profiler.get_duration('run', 0):.2f}s")
+
+ # Performance measurement runs
+ logger.info("Running performance measurement iterations...")
+ num_perf_runs = 4 # Use 4 different prompts for performance testing
+
+ # Optional Tracy profiling (if available)
+ profiler = None
+ try:
+ from tracy import Profiler
+
+ profiler = Profiler()
+ profiler.enable()
+ logger.info("Tracy profiling enabled")
+ except ImportError:
+ logger.info("Tracy profiler not available, continuing without profiling")
+
+ try:
+ for i in range(num_perf_runs):
+ logger.info(f"Performance run {i+1}/{num_perf_runs}...")
+
+ # Run pipeline with different prompt
+ prompt_idx = (i + 1) % len(TEST_PROMPTS)
+ with benchmark_profiler("run", iteration=i):
+ images = pipeline.run_single_prompt(
+ prompt=TEST_PROMPTS[prompt_idx],
+ num_inference_steps=num_inference_steps,
+ on_event=profiler_event_callback(benchmark_profiler, i),
+ )
+ images[0].save(f"flux1_{model_variant}_{image_w}_{image_h}_perf_run{i}.png")
+
+ # Collect timing data
+ logger.info(f" Run {i+1} completed in {benchmark_profiler.get_duration('run', i):.2f}s")
+
+ finally:
+ if profiler:
+ profiler.disable()
+ logger.info("Tracy profiling disabled")
+
+ # Calculate statistics
+ clip_times = [benchmark_profiler.get_duration("clip_encoding", i) for i in range(num_perf_runs)]
+ t5_times = [benchmark_profiler.get_duration("t5_encoding", i) for i in range(num_perf_runs)]
+ total_encoding_times = [benchmark_profiler.get_duration("encoder", i) for i in range(num_perf_runs)]
+ vae_times = [benchmark_profiler.get_duration("vae", i) for i in range(num_perf_runs)]
+ total_times = [benchmark_profiler.get_duration("run", i) for i in range(num_perf_runs)]
+
+ # Calculate per-step denoising times
+ all_denoising_steps = []
+ for i in range(num_perf_runs):
+ for j in range(num_inference_steps):
+ assert benchmark_profiler.contains_step(
+ f"denoising_step_{j}", i
+ ), f"All runs should have {num_inference_steps} denoising steps"
+ all_denoising_steps.append(benchmark_profiler.get_duration(f"denoising_step_{j}", i))
+
+ # Report results
+ sp_factor = sp[0] # pipeline.dit_parallel_config.sequence_parallel.factor
+ tp_factor = tp[0] # pipeline.dit_parallel_config.tensor_parallel.factor
+ encoder_tp_factor = encoder_tp[0] # pipeline.encoder_parallel_config.tensor_parallel.factor
+ vae_tp_factor = vae_tp[0] # pipeline.vae_parallel_config.tensor_parallel.factor
+
+ print("\n" + "=" * 80)
+ print(f"FLUX.1 {model_variant.upper()} PIPELINE PERFORMANCE RESULTS")
+ print("=" * 80)
+ print(f"Model: FLUX.1-{model_variant}")
+ print(f"Image Size: {image_w}x{image_h}")
+ print(f"Guidance Scale: {guidance_scale}")
+ print(f"Inference Steps: {num_inference_steps}")
+ print(f"Configuration: sp={sp_factor}, tp={tp_factor}, encoder_tp={encoder_tp_factor}, vae_tp={vae_tp_factor}")
+ print(f"Mesh Shape: {mesh_device.shape}")
+ print(f"Topology: {topology}")
+ print("-" * 80)
+
+ def print_stats(name, times):
+ if not times:
+ print(f"{name:25} | No data available")
+ return
+ mean_time = statistics.mean(times)
+ std_time = statistics.stdev(times) if len(times) > 1 else 0
+ min_time = min(times)
+ max_time = max(times)
+ print(
+ f"{name:25} | Mean: {mean_time:8.4f}s | Std: {std_time:8.4f}s | Min: {min_time:8.4f}s | Max: {max_time:8.4f}s"
+ )
+
+ print_stats("CLIP Encoding", clip_times)
+ print_stats("T5 Encoding", t5_times)
+ print_stats("Total Encoding", total_encoding_times)
+ print_stats("Denoising (per step)", all_denoising_steps)
+ print_stats("VAE Decoding", vae_times)
+ print_stats("Total Pipeline", total_times)
+
+ print("-" * 80)
+
+ # Additional metrics
+ if total_times and all_denoising_steps:
+ avg_total_time = statistics.mean(total_times)
+ avg_step_time = statistics.mean(all_denoising_steps)
+ total_denoising_time = avg_step_time * num_inference_steps
+
+ print(f"Average total denoising time: {total_denoising_time:.4f}s")
+ print(f"Denoising throughput: {num_inference_steps / total_denoising_time:.2f} steps/second")
+ print(f"Overall throughput: {1 / avg_total_time:.4f} images/second")
+
+ # Breakdown percentages
+ avg_encoding_time = statistics.mean(total_encoding_times)
+ avg_vae_time = statistics.mean(vae_times)
+
+ print(f"\nTime breakdown:")
+ print(f" Encoding: {avg_encoding_time/avg_total_time*100:.1f}%")
+ print(f" Denoising: {total_denoising_time/avg_total_time*100:.1f}%")
+ print(f" VAE: {avg_vae_time/avg_total_time*100:.1f}%")
+
+ print("=" * 80)
+
+ # Validate performance
+ measurements = {
+ "clip_encoding_time": statistics.mean(clip_times),
+ "t5_encoding_time": statistics.mean(t5_times),
+ "total_encoding_time": statistics.mean(total_encoding_times),
+ "denoising_steps_time": total_denoising_time,
+ "vae_decoding_time": statistics.mean(vae_times),
+ "total_time": statistics.mean(total_times),
+ }
+ if tuple(mesh_device.shape) == (2, 4) and is_blackhole():
+ expected_metrics = {
+ "clip_encoding_time": 0.05,
+ "t5_encoding_time": 0.25,
+ "total_encoding_time": 0.3,
+ "denoising_steps_time": 0.38 * num_inference_steps,
+ "vae_decoding_time": 1.2,
+ "total_time": 12.5,
+ }
+ elif tuple(mesh_device.shape) == (2, 4): # WH.
+ expected_metrics = {
+ "clip_encoding_time": 0.1,
+ "t5_encoding_time": 0.25,
+ "total_encoding_time": 0.3,
+ "denoising_steps_time": 0.75 * num_inference_steps,
+ "vae_decoding_time": 1.6,
+ "total_time": 23,
+ }
+ elif tuple(mesh_device.shape) == (4, 8):
+ expected_metrics = {
+ "clip_encoding_time": 0.1,
+ "t5_encoding_time": 0.21,
+ "total_encoding_time": 0.3,
+ "denoising_steps_time": 0.3 * num_inference_steps,
+ "vae_decoding_time": 1.4,
+ "total_time": 9.0,
+ }
+ elif tuple(mesh_device.shape) == (2, 2):
+ assert is_blackhole(), "2x2 is only supported for blackhole"
+ expected_metrics = {
+ "clip_encoding_time": 0.05,
+ "t5_encoding_time": 0.25,
+ "total_encoding_time": 0.3,
+ "denoising_steps_time": 0.55 * num_inference_steps,
+ "vae_decoding_time": 1.8,
+ "total_time": 17.5,
+ }
+ elif tuple(mesh_device.shape) == (1, 2):
+ assert is_blackhole(), "1x2 is only supported for blackhole"
+ expected_metrics = {
+ "clip_encoding_time": 0.1,
+ "t5_encoding_time": 0.25,
+ "total_encoding_time": 0.3,
+ "denoising_steps_time": 1.1 * num_inference_steps,
+ "vae_decoding_time": 1.6,
+ "total_time": 29.0,
+ }
+ else:
+ assert False, f"Unknown mesh device for performance comparison: {mesh_device}"
+
+ if is_ci_env:
+ # In CI, dump a performance report
+ benchmark_data = BenchmarkData()
+ for iteration in range(num_perf_runs):
+ for step_name, target in zip(
+ ["encoder", "denoising", "vae", "run"],
+ [
+ None,
+ expected_metrics["denoising_steps_time"],
+ expected_metrics["vae_decoding_time"],
+ expected_metrics["total_time"],
+ ],
+ ):
+ benchmark_data.add_measurement(
+ profiler=benchmark_profiler,
+ iteration=iteration,
+ step_name=step_name,
+ name=step_name,
+ value=benchmark_profiler.get_duration(step_name, iteration),
+ target=target,
+ )
+ device_name_map = {
+ (1, 2): "BH_P300",
+ (2, 2): "BH_QB",
+ (2, 4): "BH_LB" if is_blackhole() else "WH_T3K",
+ (4, 8): "BH_GLX" if is_blackhole() else "WH_GLX",
+ }
+ benchmark_data.save_partial_run_json(
+ benchmark_profiler,
+ run_type=device_name_map[tuple(mesh_device.shape)],
+ ml_model_name=f"Flux1{model_variant.capitalize()}",
+ batch_size=1,
+ config_params={
+ "width": image_w,
+ "height": image_h,
+ "num_frames": 1,
+ "num_steps": num_inference_steps,
+ "sp_factor": sp_factor,
+ "tp_factor": tp_factor,
+ "topology": str(topology),
+ "num_links": num_links,
+ "fsdp": False,
+ },
+ )
+
+ pass_perf_check = True
+ assert_msgs = []
+ for k in expected_metrics.keys():
+ if measurements[k] > expected_metrics[k]:
+ assert_msgs.append(
+ f"Warning: {k} is outside of the tolerance range. Expected: {expected_metrics[k]}, Actual: {measurements[k]}"
+ )
+ pass_perf_check = False
+
+ assert pass_perf_check, "\n".join(assert_msgs)
+
+ # Synchronize all devices
+ pipeline.synchronize_devices()
+
+ logger.info("Performance test completed successfully!")
+
+
+@pytest.mark.parametrize(
+ "model_variant",
+ PERF_TEST_MODEL_VARIANTS,
+)
+@pytest.mark.parametrize(
+ "image_w, image_h, guidance_scale, num_inference_steps",
+ PERF_TEST_IMAGE_PARAMS,
+)
+@pytest.mark.parametrize(
+ "mesh_device, sp, tp, encoder_tp, vae_tp, topology, num_links",
+ PERF_TEST_MESH_PARAMS,
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ PERF_TEST_DEVICE_PARAMS,
+ indirect=True,
+)
+def test_flux1_pipeline_performance_accuracy(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ image_w,
+ image_h,
+ guidance_scale,
+ num_inference_steps,
+ sp,
+ tp,
+ encoder_tp,
+ vae_tp,
+ topology,
+ num_links,
+ model_variant,
+ model_location_generator,
+ is_ci_env,
+ galaxy_type,
+) -> None:
+ pipeline = Flux1Pipeline(
+ device=mesh_device,
+ config=Flux1PipelineConfig.default(
+ mesh_shape=mesh_device.shape,
+ dit_parallel_config=DiTParallelConfig.from_tuples(cfg=(1, 0), sp=sp, tp=tp),
+ encoder_parallel_config=EncoderParallelConfig.from_tuple(encoder_tp),
+ vae_parallel_config=VAEParallelConfig.from_tuple(vae_tp),
+ topology=topology,
+ num_links=num_links,
+ width=image_w,
+ height=image_h,
+ checkpoint_name=model_location_generator(f"black-forest-labs/FLUX.1-{model_variant}"),
+ ),
+ )
+
+ # Warmup runs
+ for i in range(2):
+ logger.info("Running warmup iteration...")
+ pipeline.run_single_prompt(
+ prompt=TEST_PROMPTS[0],
+ num_inference_steps=num_inference_steps,
+ )
+
+ output_images = []
+ for i in range(len(TEST_PROMPTS)):
+ images = pipeline.run_single_prompt(
+ prompt=TEST_PROMPTS[i],
+ num_inference_steps=num_inference_steps,
+ )
+ images[0].save(f"flux1_{model_variant}_{image_w}_{image_h}_perf_run{i}.png")
+ output_images.append(images[0])
+
+ clip = CLIPEncoder()
+ clip_scores = []
+
+ for prompt, image in zip(TEST_PROMPTS, output_images):
+ clip_scores.append(100 * clip.get_clip_score(prompt, image).item())
+
+ for prompt, score in zip(TEST_PROMPTS, clip_scores):
+ logger.info(f'Prompt "{prompt[:50]}". Score {score}')
+ # TODO: At some point this was above 30 for all prompts. I wasn't able to find out what caused
+ # the regression, so doing the hacky thing and reducing the value for now.
+ # Possible culprit (https://github.com/tenstorrent/tt-metal/actions/runs/27652185143) this
+ # regression occurred at the same time as this went down, but unclear if the two are related.
+ assert score > 28.0
+
+
+@pytest.mark.parametrize(
+ "model_variant",
+ PERF_TEST_MODEL_VARIANTS,
+)
+@pytest.mark.parametrize(
+ "image_w, image_h, guidance_scale, num_inference_steps",
+ PERF_TEST_IMAGE_PARAMS,
+)
+@pytest.mark.parametrize(
+ "mesh_device, sp, tp, encoder_tp, vae_tp, topology, num_links",
+ PERF_TEST_MESH_PARAMS,
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ PERF_TEST_DEVICE_PARAMS,
+ indirect=True,
+)
+def test_flux1_pipeline_performance_determinism(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ image_w,
+ image_h,
+ guidance_scale,
+ num_inference_steps,
+ sp,
+ tp,
+ encoder_tp,
+ vae_tp,
+ topology,
+ num_links,
+ model_variant,
+ model_location_generator,
+ is_ci_env,
+ galaxy_type,
+) -> None:
+ pipeline = Flux1Pipeline(
+ device=mesh_device,
+ config=Flux1PipelineConfig.default(
+ mesh_shape=mesh_device.shape,
+ dit_parallel_config=DiTParallelConfig.from_tuples(cfg=(1, 0), sp=sp, tp=tp),
+ encoder_parallel_config=EncoderParallelConfig.from_tuple(encoder_tp),
+ vae_parallel_config=VAEParallelConfig.from_tuple(vae_tp),
+ topology=topology,
+ num_links=num_links,
+ width=image_w,
+ height=image_h,
+ checkpoint_name=model_location_generator(f"black-forest-labs/FLUX.1-{model_variant}"),
+ ),
+ )
+
+ import numpy as np
+
+ output_images = []
+ num_iters = 3
+ for i in range(num_iters):
+ images = pipeline.run_single_prompt(
+ prompt=TEST_PROMPTS[0],
+ num_inference_steps=num_inference_steps,
+ )
+ images[0].save(f"flux1_{model_variant}_{image_w}_{image_h}_perf_run{i}.png")
+ output_images.append(np.array(images[0]))
+
+ reference = output_images[0]
+ for i, img_array in enumerate(output_images[1:], start=1):
+ assert np.array_equal(
+ reference, img_array
+ ), f"Image from iteration {i} differs from iteration 0 (pixel-level mismatch)"
diff --git a/code/models/tt_dit/tests/models/flux1/test_pipeline_flux1.py b/code/models/tt_dit/tests/models/flux1/test_pipeline_flux1.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b67e7654c247638fc059ebafb0c558b7136062b
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux1/test_pipeline_flux1.py
@@ -0,0 +1,227 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+import itertools
+import os
+
+import pytest
+from loguru import logger
+
+import ttnn
+from models.perf.benchmarking_utils import BenchmarkProfiler
+
+from ....models.transformers.transformer_flux1 import Flux1Checkpoint
+from ....parallel.config import DiTParallelConfig, EncoderParallelConfig, VAEParallelConfig
+from ....pipelines.events import profiler_event_callback
+from ....pipelines.flux1.pipeline_flux1 import Flux1Pipeline, Flux1PipelineConfig
+
+
+@pytest.mark.parametrize(
+ "no_prompt",
+ [{"1": True, "0": False}.get(os.environ.get("NO_PROMPT"), False)],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [
+ {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "l1_small_size": 32768,
+ "trace_region_size": 50000000,
+ "require_exact_physical_num_devices": True,
+ }
+ ],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ ("model_variant", "width", "height", "num_inference_steps"),
+ [
+ ("schnell", 1024, 1024, 4),
+ ("dev", 1024, 1024, 28),
+ ],
+ ids=["flux_schnell", "flux_dev"],
+)
+@pytest.mark.parametrize(
+ ("mesh_device", "sp", "tp", "encoder_tp", "vae_tp", "topology", "num_links", "mesh_test_id"),
+ [
+ pytest.param((1, 2), (1, 0), (2, 1), (2, 1), (2, 1), ttnn.Topology.Linear, 2, "1x2sp0tp1", id="1x2sp0tp1"),
+ pytest.param((2, 2), (2, 0), (2, 1), (2, 1), (2, 1), ttnn.Topology.Linear, 2, "2x2sp0tp1", id="2x2sp0tp1"),
+ pytest.param((2, 4), (2, 0), (4, 1), (4, 1), (4, 1), ttnn.Topology.Linear, 1, "2x4sp0tp1", id="2x4sp0tp1"),
+ pytest.param((4, 8), (4, 0), (8, 1), (4, 0), (4, 0), ttnn.Topology.Linear, 4, "4x8sp0tp1", id="4x8sp0tp1"),
+ pytest.param((4, 8), (8, 1), (4, 0), (4, 0), (4, 0), ttnn.Topology.Linear, 4, "4x8sp1tp0", id="4x8sp1tp0"),
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ ("enable_t5_text_encoder", "use_torch_t5_text_encoder", "use_torch_clip_text_encoder"),
+ [
+ # pytest.param(True, True, True, id="encoder_cpu"),
+ pytest.param(True, False, False, id="encoder_device"),
+ ],
+)
+@pytest.mark.parametrize(
+ "traced",
+ [
+ pytest.param(True, id="traced"),
+ pytest.param(False, id="not_traced"),
+ ],
+)
+@pytest.mark.parametrize(
+ "use_cache",
+ [
+ pytest.param(True, id="yes_use_cache"),
+ pytest.param(False, id="no_use_cache"),
+ ],
+)
+def test_flux1_pipeline(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ model_variant: str,
+ width: int,
+ height: int,
+ num_inference_steps: int,
+ sp: tuple[int, int],
+ tp: tuple[int, int],
+ encoder_tp: tuple[int, int],
+ vae_tp: tuple[int, int],
+ topology: ttnn.Topology,
+ num_links: int,
+ no_prompt: bool,
+ enable_t5_text_encoder: bool,
+ use_torch_t5_text_encoder: bool,
+ use_torch_clip_text_encoder: bool,
+ model_location_generator,
+ traced: bool,
+ mesh_test_id: str,
+ use_cache: bool,
+ is_ci_env: bool,
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ # Setup CI environment
+ if is_ci_env and traced:
+ pytest.skip("Skipping traced test in CI environment. Use Performance test for detailed timing analysis.")
+
+ parallel_config = DiTParallelConfig.from_tuples(cfg=(1, 0), sp=sp, tp=tp)
+ encoder_parallel_config = EncoderParallelConfig.from_tuple(encoder_tp)
+ vae_parallel_config = VAEParallelConfig.from_tuple(vae_tp)
+
+ logger.info(f"Mesh device shape: {mesh_device.shape}")
+ logger.info(f"Parallel config: {parallel_config}")
+ logger.info(f"Encoder parallel config: {encoder_parallel_config}")
+ logger.info(f"VAE parallel config: {vae_parallel_config}")
+ logger.info(f"T5 enabled: {enable_t5_text_encoder}")
+ logger.info(f"HF_HUB_OFFLINE: {os.environ.get('HF_HUB_OFFLINE', '')}")
+
+ pipeline = Flux1Pipeline(
+ device=mesh_device,
+ config=Flux1PipelineConfig.default(
+ mesh_shape=mesh_device.shape,
+ dit_parallel_config=parallel_config,
+ encoder_parallel_config=encoder_parallel_config,
+ vae_parallel_config=vae_parallel_config,
+ enable_t5_text_encoder=enable_t5_text_encoder,
+ use_torch_t5_text_encoder=use_torch_t5_text_encoder,
+ use_torch_clip_text_encoder=use_torch_clip_text_encoder,
+ num_links=num_links,
+ topology=topology,
+ width=width,
+ height=height,
+ checkpoint_name=model_location_generator(f"black-forest-labs/FLUX.1-{model_variant}"),
+ ),
+ )
+
+ prompts = [
+ "A luxury sports car.",
+ # "Neon-lit cyberpunk alley, rain-soaked, cinematic wide shot",
+ # "Golden retriever astronaut drifting in sunlit space",
+ # "Minimalist Scandinavian kitchen, morning light, ultra clean",
+ # "Ancient desert temple at dawn, soft fog, wide angle",
+ # "Steampunk airship over Victorian city, dramatic clouds",
+ # "Macro dewdrops on fern, shallow depth of field",
+ # "Luxury wristwatch on marble, studio lighting, hyper-detail",
+ # "Stormy coastline lighthouse, crashing waves, long exposure",
+ # "Futuristic Tokyo street market, vibrant signage, motion blur",
+ ]
+
+ filename_prefix = f"flux_{model_variant}_{width}_{height}_{mesh_test_id}"
+ if enable_t5_text_encoder:
+ if use_torch_t5_text_encoder:
+ filename_prefix += "_t5cpu"
+ else:
+ filename_prefix += "_t5off"
+ if use_torch_clip_text_encoder:
+ filename_prefix += "_clipcpu"
+ if not traced:
+ filename_prefix += "_untraced"
+
+ def run(*, prompt: str, number: int, seed: int) -> None:
+ benchmark_profiler = BenchmarkProfiler()
+ with benchmark_profiler("run", iteration=0):
+ images = pipeline(
+ prompts=[prompt],
+ num_inference_steps=num_inference_steps,
+ seed=seed,
+ traced=traced,
+ vae_traced=False,
+ encoder_traced=False,
+ on_event=profiler_event_callback(benchmark_profiler, 0),
+ )
+
+ output_filename = f"{filename_prefix}_{number}.png"
+ images[0].save(output_filename)
+ logger.info(f"Image saved as {output_filename}")
+
+ logger.info(f"CLIP encoding time: {benchmark_profiler.get_duration('clip_encoding', 0):.2f}s")
+ logger.info(f"T5 encoding time: {benchmark_profiler.get_duration('t5_encoding', 0):.2f}s")
+ logger.info(f"Total encoding time: {benchmark_profiler.get_duration('encoder', 0):.2f}s")
+ logger.info(f"VAE decoding time: {benchmark_profiler.get_duration('vae', 0):.2f}s")
+ logger.info(f"Total pipeline time: {benchmark_profiler.get_duration('total', 0):.2f}s")
+ avg_step_time = benchmark_profiler.get_duration("denoising", 0) / num_inference_steps
+ logger.info(f"Average denoising step time: {avg_step_time:.2f}s")
+
+ if no_prompt:
+ for i, prompt in enumerate(prompts):
+ run(prompt=prompt, number=i, seed=0)
+ else:
+ prompt = prompts[0]
+ for i in itertools.count():
+ new_prompt = input("Enter the input prompt, or q to exit: ")
+ if new_prompt:
+ prompt = new_prompt
+ if prompt[0] == "q":
+ break
+ run(prompt=prompt, number=i, seed=i)
+
+
+def _log_hf_checkpoint_location(checkpoint_name) -> None:
+ """Log where HuggingFace will look for / resolve the checkpoint files."""
+ import huggingface_hub.constants as hf_const
+ from huggingface_hub import try_to_load_from_cache
+
+ logger.info(f"Resolved checkpoint_name (passed to from_pretrained): {checkpoint_name}")
+ logger.info(f"HF_HOME = {hf_const.HF_HOME}")
+ logger.info(f"HF_HUB_CACHE = {hf_const.HF_HUB_CACHE}")
+
+ # If checkpoint_name is a local path, files load from there directly.
+ if os.path.isdir(checkpoint_name):
+ logger.info(f"checkpoint_name is a local directory; loading from: {checkpoint_name}")
+ return
+
+ # Otherwise it's an HF repo id resolved out of the hub cache. Show the cached file paths.
+ for rel in ("transformer/config.json", "model_index.json"):
+ cached = try_to_load_from_cache(checkpoint_name, rel)
+ logger.info(f" {rel} -> {cached}")
+
+
+def test_flux1_dev_checkpoint_load(model_location_generator) -> None:
+ """Loads Flux1Checkpoint and verifies key metadata/state are available."""
+ checkpoint_name = model_location_generator("black-forest-labs/FLUX.1-dev")
+ _log_hf_checkpoint_location(checkpoint_name)
+ Flux1Checkpoint(checkpoint_name)
+
+
+def test_flux1_schnell_checkpoint_load(model_location_generator) -> None:
+ """Loads Flux1Checkpoint and verifies key metadata/state are available."""
+ checkpoint_name = model_location_generator("black-forest-labs/FLUX.1-schnell")
+ _log_hf_checkpoint_location(checkpoint_name)
+ Flux1Checkpoint(checkpoint_name)
diff --git a/code/models/tt_dit/tests/models/flux1/test_transformer_block_flux1.py b/code/models/tt_dit/tests/models/flux1/test_transformer_block_flux1.py
new file mode 100644
index 0000000000000000000000000000000000000000..8541b759132ec7cacb3faaeda8d88dce5f072eb0
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux1/test_transformer_block_flux1.py
@@ -0,0 +1,171 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+from time import time
+
+import diffusers.models.transformers.transformer_flux
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+
+from ....blocks.attention import Attention
+from ....blocks.transformer_block import TransformerBlock
+from ....parallel.config import DiTParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils import tensor
+from ....utils.check import assert_quality
+from ....utils.padding import PaddingConfig
+from ....utils.tensor import bf16_tensor, bf16_tensor_2dshard
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "submesh_shape", "sp_axis", "tp_axis", "num_links", "mesh_id"),
+ [
+ pytest.param((1, 2), (1, 2), 0, 1, 2, "1x2sp0tp1", id="1x2sp0tp1"),
+ pytest.param((2, 2), (2, 2), 0, 1, 2, "2x2sp0tp1", id="2x2sp0tp1"),
+ pytest.param((2, 4), (1, 4), 0, 1, 1, "1x4sp0tp1", id="1x4sp0tp1"),
+ pytest.param((2, 4), (2, 4), 0, 1, 1, "2x4sp0tp1", id="2x4sp0tp1"),
+ pytest.param((2, 4), (2, 4), 1, 0, 1, "2x4sp1tp0", id="2x4sp1tp0"),
+ pytest.param((4, 8), (4, 4), 0, 1, 4, "4x4sp0tp1", id="4x4sp0tp1"),
+ pytest.param((4, 8), (4, 8), 0, 1, 4, "4x8sp0tp1", id="4x8sp0tp1"),
+ pytest.param((4, 8), (4, 8), 1, 0, 4, "4x8sp1tp0", id="4x8sp1tp0"),
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ ("batch_size", "spatial_seq_len", "prompt_seq_len"),
+ [
+ (1, 4096, 512),
+ ],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [
+ {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "require_exact_physical_num_devices": True,
+ }
+ ],
+ indirect=True,
+)
+def test_transformer_block_flux1(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ submesh_shape: tuple[int, int],
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ batch_size: int,
+ spatial_seq_len: int,
+ prompt_seq_len: int,
+ mesh_id: str,
+ is_ci_env: bool,
+) -> None:
+ torch.manual_seed(0)
+
+ submesh_device = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ sp_factor = tuple(submesh_device.shape)[sp_axis]
+ tp_factor = tuple(submesh_device.shape)[tp_axis]
+
+ inner_dim = 3072
+ head_dim = 128
+ num_heads = 24
+
+ torch_model = diffusers.models.transformers.transformer_flux.FluxTransformerBlock(
+ dim=inner_dim, num_attention_heads=num_heads, attention_head_dim=head_dim
+ )
+ torch_model.eval()
+
+ ccl_manager = CCLManager(
+ mesh_device=submesh_device,
+ num_links=num_links,
+ topology=ttnn.Topology.Linear,
+ )
+
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=0, mesh_axis=0),
+ tensor_parallel=ParallelFactor(factor=tp_factor, mesh_axis=tp_axis),
+ sequence_parallel=ParallelFactor(factor=sp_factor, mesh_axis=sp_axis),
+ )
+
+ if num_heads % tp_factor != 0:
+ padding_config = PaddingConfig.from_tensor_parallel_factor(num_heads, head_dim, tp_factor)
+ else:
+ padding_config = None
+
+ tt_model = TransformerBlock(
+ dim=inner_dim,
+ num_heads=num_heads,
+ head_dim=head_dim,
+ context_pre_only=False,
+ mesh_device=submesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ padding_config=padding_config,
+ )
+ tt_model.load_torch_state_dict(torch_model.state_dict())
+
+ spatial = torch.randn([batch_size, spatial_seq_len, inner_dim])
+ prompt = torch.randn([batch_size, prompt_seq_len, inner_dim])
+ time_embed = torch.randn([batch_size, inner_dim])
+ rope_cos = torch.randn([spatial_seq_len + prompt_seq_len, head_dim])
+ rope_sin = torch.randn([spatial_seq_len + prompt_seq_len, head_dim])
+
+ spatial_padded = Attention.pad_spatial_sequence(spatial, sp_factor=sp_factor)
+ spatial_rope_cos_padded = Attention.pad_spatial_sequence(rope_cos[prompt_seq_len:], sp_factor=sp_factor)
+ spatial_rope_sin_padded = Attention.pad_spatial_sequence(rope_sin[prompt_seq_len:], sp_factor=sp_factor)
+
+ tt_spatial = bf16_tensor_2dshard(spatial_padded, device=submesh_device, shard_mapping={sp_axis: 1, tp_axis: 2})
+ tt_prompt = bf16_tensor(prompt, device=submesh_device, mesh_axis=tp_axis, shard_dim=2)
+ tt_time_embed = bf16_tensor(time_embed.unsqueeze(1), device=submesh_device)
+ tt_spatial_rope_cos = bf16_tensor(spatial_rope_cos_padded, device=submesh_device, mesh_axis=sp_axis, shard_dim=0)
+ tt_spatial_rope_sin = bf16_tensor(spatial_rope_sin_padded, device=submesh_device, mesh_axis=sp_axis, shard_dim=0)
+ tt_prompt_rope_cos = bf16_tensor(rope_cos[:prompt_seq_len], device=submesh_device)
+ tt_prompt_rope_sin = bf16_tensor(rope_sin[:prompt_seq_len], device=submesh_device)
+
+ with torch.no_grad():
+ torch_prompt, torch_spatial = torch_model.forward(
+ spatial, prompt, temb=time_embed, image_rotary_emb=(rope_cos, rope_sin)
+ )
+
+ if not is_ci_env:
+ try:
+ from tracy import signpost
+
+ signpost("caching")
+ tt_spatial_out, tt_prompt_out = tt_model.forward(
+ tt_spatial,
+ tt_prompt,
+ tt_time_embed,
+ spatial_sequence_length=spatial_seq_len,
+ spatial_rope=(tt_spatial_rope_cos, tt_spatial_rope_sin),
+ prompt_rope=(tt_prompt_rope_cos, tt_prompt_rope_sin),
+ )
+
+ signpost("performance")
+ except ImportError:
+ logger.info("Tracy profiler not available, continuing without profiling")
+
+ itr = 1
+ start = time()
+ for _ in range(itr):
+ tt_spatial_out, tt_prompt_out = tt_model.forward(
+ tt_spatial,
+ tt_prompt,
+ tt_time_embed,
+ spatial_sequence_length=spatial_seq_len,
+ spatial_rope=(tt_spatial_rope_cos, tt_spatial_rope_sin),
+ prompt_rope=(tt_prompt_rope_cos, tt_prompt_rope_sin),
+ )
+
+ ttnn.synchronize_device(submesh_device)
+ logger.info(f"Time taken for {mesh_id}: {(time() - start) * 1000 / itr} ms")
+
+ tt_spatial_torch = tensor.to_torch(tt_spatial_out, mesh_axes=[None, sp_axis, tp_axis])
+ assert_quality(torch_spatial, tt_spatial_torch, pcc=0.99998, relative_rmse=0.006)
+
+ tt_prompt_torch = tensor.to_torch(tt_prompt_out, mesh_axes=[None, None, tp_axis])
+ assert_quality(torch_prompt, tt_prompt_torch, pcc=0.99998, relative_rmse=0.006)
diff --git a/code/models/tt_dit/tests/models/flux1/test_transformer_flux1.py b/code/models/tt_dit/tests/models/flux1/test_transformer_flux1.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f3f9f81762845851cb3b7fff4f7f371e389c2f4
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux1/test_transformer_flux1.py
@@ -0,0 +1,326 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# import diffusers.models.transformers.transformer_flux as reference
+from time import time
+
+import diffusers as reference
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+
+from ....models.transformers.transformer_flux1 import Flux1Checkpoint, Flux1SingleTransformerBlock
+from ....parallel.config import DiTParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils.check import assert_quality
+from ....utils.padding import PaddingConfig
+from ....utils.tensor import bf16_tensor, bf16_tensor_2dshard
+
+TEST_MESH_PARAMS = [
+ # BH Only
+ pytest.param((1, 2), (1, 2), 0, 1, 2, "1x2sp0tp1", id="1x2sp0tp1"),
+ pytest.param((2, 2), (2, 2), 0, 1, 2, "2x2sp0tp1", id="2x2sp0tp1"),
+ # WH Only
+ pytest.param((2, 4), (2, 4), 0, 1, 1, "2x4sp0tp1", id="2x4sp0tp1"),
+ pytest.param((2, 4), (2, 4), 1, 0, 1, "2x4sp1tp0", id="2x4sp1tp0"),
+ # BH and WH
+ pytest.param((4, 8), (4, 4), 0, 1, 4, "4x4sp0tp1", id="4x4sp0tp1"),
+ pytest.param((4, 8), (4, 8), 0, 1, 4, "4x8sp0tp1", id="4x8sp0tp1"),
+ pytest.param((4, 8), (4, 8), 1, 0, 4, "4x8sp1tp0", id="4x8sp1tp0"),
+]
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "submesh_shape", "sp_axis", "tp_axis", "num_links", "id"),
+ TEST_MESH_PARAMS,
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ ("batch_size", "spatial_seq_len", "prompt_seq_len"),
+ [
+ (1, 4096, 512),
+ ],
+)
+@pytest.mark.parametrize(
+ "model_variant",
+ ["schnell", "dev"],
+ ids=["flux_schnell", "flux_dev"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [
+ {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "require_exact_physical_num_devices": True,
+ }
+ ],
+ indirect=True,
+)
+def test_transformer_single_block(
+ mesh_device: ttnn.MeshDevice,
+ submesh_shape: tuple[int, int],
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ batch_size: int,
+ prompt_seq_len: int,
+ spatial_seq_len: int,
+ model_variant: str,
+ id: str,
+ model_location_generator,
+ is_ci_env: bool,
+) -> None:
+ submesh_device = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ sp_factor = tuple(submesh_device.shape)[sp_axis]
+ tp_factor = tuple(submesh_device.shape)[tp_axis]
+ model_name = model_location_generator(f"black-forest-labs/FLUX.1-{model_variant}", model_subdir="transformer")
+ parent_torch_model = reference.FluxTransformer2DModel.from_pretrained(
+ model_name, subfolder="transformer", torch_dtype=torch.bfloat16
+ )
+ torch_model = parent_torch_model.single_transformer_blocks[0]
+ assert isinstance(torch_model, reference.models.transformers.transformer_flux.FluxSingleTransformerBlock)
+ torch_model.eval()
+
+ inner_dim = torch_model.attn.inner_dim
+ num_heads = torch_model.attn.heads
+ head_dim = inner_dim // num_heads
+
+ ccl_manager = CCLManager(
+ mesh_device=submesh_device,
+ num_links=num_links,
+ topology=ttnn.Topology.Linear,
+ )
+
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=0, mesh_axis=0),
+ tensor_parallel=ParallelFactor(factor=tp_factor, mesh_axis=tp_axis),
+ sequence_parallel=ParallelFactor(factor=sp_factor, mesh_axis=sp_axis),
+ )
+
+ if num_heads % tp_factor != 0:
+ padding_config = PaddingConfig.from_tensor_parallel_factor(num_heads, head_dim, tp_factor)
+ else:
+ padding_config = None
+
+ tt_model = Flux1SingleTransformerBlock(
+ dim=inner_dim,
+ num_heads=num_heads,
+ head_dim=head_dim,
+ mesh_device=submesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ padding_config=padding_config,
+ )
+ tt_model.load_torch_state_dict(torch_model.state_dict())
+
+ torch.manual_seed(0)
+ combined = torch.randn([batch_size, prompt_seq_len + spatial_seq_len, inner_dim]).to(torch.bfloat16)
+ time_embed = torch.randn([batch_size, inner_dim]).to(torch.bfloat16)
+ rope_cos = torch.randn([prompt_seq_len + spatial_seq_len, head_dim]).to(torch.bfloat16)
+ rope_sin = torch.randn([prompt_seq_len + spatial_seq_len, head_dim]).to(torch.bfloat16)
+
+ tt_spatial = bf16_tensor_2dshard(
+ combined[:, prompt_seq_len:], device=submesh_device, shard_mapping={sp_axis: 1, tp_axis: 2}
+ )
+ tt_prompt = bf16_tensor(combined[:, :prompt_seq_len], device=submesh_device, mesh_axis=tp_axis, shard_dim=2)
+ tt_time_embed = bf16_tensor(time_embed.unsqueeze(1), device=submesh_device)
+ tt_spatial_rope_cos = bf16_tensor(rope_cos[prompt_seq_len:], device=submesh_device, mesh_axis=sp_axis, shard_dim=0)
+ tt_spatial_rope_sin = bf16_tensor(rope_sin[prompt_seq_len:], device=submesh_device, mesh_axis=sp_axis, shard_dim=0)
+ tt_prompt_rope_cos = bf16_tensor(rope_cos[:prompt_seq_len], device=submesh_device)
+ tt_prompt_rope_sin = bf16_tensor(rope_sin[:prompt_seq_len], device=submesh_device)
+
+ with torch.no_grad():
+ torch_combined = torch_model.forward(
+ hidden_states=combined[:, prompt_seq_len:],
+ encoder_hidden_states=combined[:, :prompt_seq_len],
+ temb=time_embed,
+ image_rotary_emb=(rope_cos, rope_sin),
+ )
+
+ if not is_ci_env:
+ try:
+ from tracy import signpost
+
+ signpost("caching")
+ tt_spatial_out, tt_prompt_out = tt_model.forward(
+ spatial=tt_spatial,
+ prompt=tt_prompt,
+ time_embed=tt_time_embed,
+ spatial_rope=(tt_spatial_rope_cos, tt_spatial_rope_sin),
+ prompt_rope=(tt_prompt_rope_cos, tt_prompt_rope_sin),
+ spatial_sequence_length=spatial_seq_len,
+ )
+
+ signpost("performance")
+ except ImportError:
+ logger.info("Tracy profiler not available, continuing without profiling")
+
+ itr = 1
+ start = time()
+ for _ in range(itr):
+ tt_spatial_out, tt_prompt_out = tt_model.forward(
+ spatial=tt_spatial,
+ prompt=tt_prompt,
+ time_embed=tt_time_embed,
+ spatial_rope=(tt_spatial_rope_cos, tt_spatial_rope_sin),
+ prompt_rope=(tt_prompt_rope_cos, tt_prompt_rope_sin),
+ spatial_sequence_length=spatial_seq_len,
+ )
+ # ttnn.synchronize_device(submesh_device)
+ ttnn.synchronize_device(submesh_device)
+ logger.info(f"Time taken for {id}: {(time() - start)*1000/itr} ms")
+
+ shard_dims = [None, None]
+ shard_dims[sp_axis], shard_dims[tp_axis] = 1, 2
+ tt_spatial_torch = ttnn.to_torch(
+ tt_spatial_out,
+ mesh_composer=ttnn.create_mesh_composer(submesh_device, ttnn.MeshComposerConfig(shard_dims)),
+ )
+
+ shard_dims = [None, None]
+ shard_dims[sp_axis], shard_dims[tp_axis] = 0, 2
+ tt_prompt_torch = ttnn.to_torch(
+ tt_prompt_out,
+ mesh_composer=ttnn.create_mesh_composer(submesh_device, ttnn.MeshComposerConfig(shard_dims)),
+ )[:batch_size]
+
+ tt_combined_torch = torch.concat([tt_prompt_torch, tt_spatial_torch], dim=1)
+ torch_combined = torch.concat(torch_combined, dim=1)
+
+ assert_quality(torch_combined, tt_combined_torch, pcc=0.99959, relative_rmse=6.3)
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "submesh_shape", "sp_axis", "tp_axis", "num_links", "id"),
+ TEST_MESH_PARAMS,
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ ("batch_size", "spatial_seq_len", "prompt_seq_len"),
+ [
+ (1, 4096, 512),
+ ],
+)
+@pytest.mark.parametrize(
+ "model_variant",
+ ["schnell", "dev"],
+ ids=["flux_schnell", "flux_dev"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [
+ {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "trace_region_size": 31000000,
+ "require_exact_physical_num_devices": True,
+ },
+ ],
+ indirect=True,
+)
+def test_transformer(
+ mesh_device: ttnn.MeshDevice,
+ submesh_shape: tuple[int, int],
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ batch_size: int,
+ spatial_seq_len: int,
+ prompt_seq_len: int,
+ model_variant: str,
+ id: str,
+ model_location_generator,
+) -> None:
+ submesh_device = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+
+ sp_factor = tuple(submesh_device.shape)[sp_axis]
+ tp_factor = tuple(submesh_device.shape)[tp_axis]
+
+ model_name = model_location_generator(f"black-forest-labs/FLUX.1-{model_variant}", model_subdir="transformer")
+ torch_model = reference.FluxTransformer2DModel.from_pretrained(
+ model_name, subfolder="transformer", torch_dtype=torch.bfloat16
+ )
+ assert isinstance(torch_model, reference.FluxTransformer2DModel)
+ torch_model.eval()
+
+ in_channels = torch_model.config.in_channels
+ joint_attention_dim = torch_model.config.joint_attention_dim
+ pooled_projection_dim = torch_model.config.pooled_projection_dim
+ with_guidance_embeds = torch_model.config.guidance_embeds
+
+ ccl_manager = CCLManager(
+ mesh_device=submesh_device,
+ num_links=num_links,
+ topology=ttnn.Topology.Linear,
+ )
+
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=0, mesh_axis=0),
+ tensor_parallel=ParallelFactor(factor=tp_factor, mesh_axis=tp_axis),
+ sequence_parallel=ParallelFactor(factor=sp_factor, mesh_axis=sp_axis),
+ )
+
+ checkpoint = Flux1Checkpoint(model_name)
+ tt_model = checkpoint.build(ccl_manager=ccl_manager, parallel_config=parallel_config)
+
+ torch.manual_seed(0)
+ spatial = torch.randn([batch_size, spatial_seq_len, in_channels]).to(torch.bfloat16)
+ prompt = torch.randn([batch_size, prompt_seq_len, joint_attention_dim]).to(torch.bfloat16)
+ pooled = torch.randn([batch_size, pooled_projection_dim]).to(torch.bfloat16)
+ timestep = torch.full([batch_size], fill_value=500).to(torch.bfloat16)
+ guidance = torch.full([batch_size], fill_value=3).to(torch.bfloat16) if with_guidance_embeds else None
+
+ # prepare for ROPE
+ text_ids = torch.zeros([prompt_seq_len, 3]).to(torch.bfloat16)
+ image_ids = torch.randint(1024 * 1024, [spatial_seq_len, 3]).to(torch.bfloat16)
+ ids = torch.cat((text_ids, image_ids), dim=0).to(torch.bfloat16)
+ rope_cos, rope_sin = torch_model.pos_embed.forward(ids)
+
+ tt_spatial = bf16_tensor(spatial, device=submesh_device, mesh_axis=sp_axis, shard_dim=1)
+ tt_prompt = bf16_tensor(prompt, device=submesh_device)
+ tt_pooled = bf16_tensor(pooled, device=submesh_device)
+ tt_timestep = ttnn.from_torch(
+ timestep.unsqueeze(-1), dtype=ttnn.float32, layout=ttnn.TILE_LAYOUT, device=submesh_device
+ )
+ tt_guidance = bf16_tensor(guidance.unsqueeze(-1), device=submesh_device) if guidance is not None else None
+
+ tt_spatial_rope_cos = bf16_tensor(rope_cos[prompt_seq_len:], device=submesh_device, mesh_axis=sp_axis, shard_dim=0)
+ tt_spatial_rope_sin = bf16_tensor(rope_sin[prompt_seq_len:], device=submesh_device, mesh_axis=sp_axis, shard_dim=0)
+ tt_prompt_rope_cos = bf16_tensor(rope_cos[:prompt_seq_len], device=submesh_device)
+ tt_prompt_rope_sin = bf16_tensor(rope_sin[:prompt_seq_len], device=submesh_device)
+
+ logger.info("running torch model...")
+ with torch.no_grad():
+ torch_output = torch_model.forward(
+ hidden_states=spatial,
+ encoder_hidden_states=prompt,
+ pooled_projections=pooled,
+ timestep=timestep / 1000,
+ guidance=guidance,
+ img_ids=image_ids,
+ txt_ids=text_ids,
+ ).sample
+
+ logger.info("running TT model...")
+ tt_output = tt_model.forward(
+ spatial=tt_spatial,
+ prompt=tt_prompt,
+ pooled=tt_pooled,
+ timestep=tt_timestep,
+ guidance=tt_guidance,
+ spatial_rope=(tt_spatial_rope_cos, tt_spatial_rope_sin),
+ prompt_rope=(tt_prompt_rope_cos, tt_prompt_rope_sin),
+ spatial_sequence_length=spatial_seq_len,
+ prompt_sequence_length=prompt_seq_len,
+ )
+
+ shard_dims = [None, None]
+ shard_dims[sp_axis], shard_dims[tp_axis] = 1, 0
+ tt_output_torch = ttnn.to_torch(
+ tt_output,
+ mesh_composer=ttnn.create_mesh_composer(submesh_device, ttnn.MeshComposerConfig(shard_dims)),
+ )[:batch_size]
+
+ assert_quality(torch_output, tt_output_torch, pcc=0.997, relative_rmse=8.1)
diff --git a/code/models/tt_dit/tests/models/flux1/test_vae.py b/code/models/tt_dit/tests/models/flux1/test_vae.py
new file mode 100644
index 0000000000000000000000000000000000000000..890eef8ed8b984ec991fbb3619b3ca7f43a40bf4
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux1/test_vae.py
@@ -0,0 +1,137 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+import diffusers.models.autoencoders.autoencoder_kl_qwenimage as reference
+import pytest
+import torch
+
+import ttnn
+
+from ...models.vae.vae import VaeAttention, VaeContext, VaeNormDescRms, VaeRmsNorm
+from ...parallel.manager import CCLManager
+from ...utils import tensor
+from ...utils.check import assert_quality
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "tp_axis"),
+ [
+ pytest.param((1, 1), None, id="1x1"),
+ pytest.param((1, 2), None, id="1x2"),
+ # pytest.param((1, 2), 1, id="1x2tp"), # hangs, resolved by https://github.com/tenstorrent/tt-metal/pull/33254
+ pytest.param((1, 4), 1, id="1x4tp"),
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [{"fabric_config": ttnn.FabricConfig.FABRIC_1D, "l1_small_size": 65536, "trace_region_size": 20000000}],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ ("batch_size", "height", "width", "channels"),
+ [
+ (2, 128, 128, 384),
+ ],
+)
+def test_vae_rms_norm(
+ *,
+ mesh_device: ttnn.Device,
+ batch_size: int,
+ height: int,
+ width: int,
+ channels: int,
+ tp_axis: int | None,
+) -> None:
+ torch.manual_seed(0)
+
+ torch_model = reference.QwenImageRMS_norm(channels)
+ torch_model.eval()
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+
+ ctx = VaeContext(tp_axis=tp_axis, device=mesh_device, ccl_manager=ccl_manager)
+ tt_model = VaeRmsNorm(channels, eps=1e-12, ctx=ctx)
+
+ tt_model.load_torch_state_dict(torch_model.state_dict())
+
+ inp = torch.randn(batch_size, channels, height, width)
+
+ tt_inp = tensor.from_torch(inp.permute(0, 2, 3, 1), mesh_axes=[..., tp_axis], device=mesh_device)
+
+ with torch.no_grad():
+ torch_output = torch_model.forward(inp)
+
+ tt_out = tt_model.forward(tt_inp)
+
+ tt_out_torch = tensor.to_torch(tt_out, mesh_axes=[..., tp_axis]).permute(0, 3, 1, 2)
+ assert_quality(torch_output, tt_out_torch, pcc=0.99999, relative_rmse=0.008)
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "tp_axis"),
+ [
+ pytest.param((1, 1), None, id="1x1"),
+ pytest.param((1, 2), None, id="1x2"),
+ # pytest.param((1, 2), 1, id="1x2tp"), # hangs, resolved by https://github.com/tenstorrent/tt-metal/pull/33254
+ pytest.param((1, 4), 1, id="1x4tp"),
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [{"fabric_config": ttnn.FabricConfig.FABRIC_1D, "l1_small_size": 65536, "trace_region_size": 20000000}],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ ("batch_size", "height", "width", "dim"),
+ [
+ (2, 128, 128, 384),
+ ],
+)
+def test_vae_qwenimage_attention(
+ *,
+ mesh_device: ttnn.Device,
+ batch_size: int,
+ height: int,
+ width: int,
+ dim: int,
+ tp_axis: int | None,
+) -> None:
+ torch.manual_seed(0)
+
+ torch_model = reference.QwenImageAttentionBlock(dim)
+ torch_model.eval()
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+
+ ctx = VaeContext(tp_axis=tp_axis, device=mesh_device, ccl_manager=ccl_manager)
+ tt_model = VaeAttention(num_channels=dim, norm=VaeNormDescRms(eps=1e-12), ctx=ctx)
+
+ state = torch_model.state_dict()
+ tt_model.load_torch_state_dict(
+ {
+ "to_out.0.weight": state["proj.weight"].squeeze(2, 3),
+ "to_out.0.bias": state["proj.bias"],
+ "to_q.weight": state["to_qkv.weight"].squeeze(2, 3).chunk(3)[0],
+ "to_k.weight": state["to_qkv.weight"].squeeze(2, 3).chunk(3)[1],
+ "to_v.weight": state["to_qkv.weight"].squeeze(2, 3).chunk(3)[2],
+ "to_q.bias": state["to_qkv.bias"].chunk(3)[0],
+ "to_k.bias": state["to_qkv.bias"].chunk(3)[1],
+ "to_v.bias": state["to_qkv.bias"].chunk(3)[2],
+ "norm.gamma": state["norm.gamma"],
+ }
+ )
+
+ inp = torch.randn(batch_size, dim, height, width)
+
+ tt_inp = tensor.from_torch(inp.permute(0, 2, 3, 1), mesh_axes=[..., tp_axis], device=mesh_device)
+
+ with torch.no_grad():
+ torch_output = torch_model.forward(inp.unsqueeze(2)).squeeze(2)
+
+ tt_out = tt_model.forward(tt_inp)
+
+ tt_out_torch = tensor.to_torch(tt_out, mesh_axes=[..., tp_axis]).permute(0, 3, 1, 2)
+ assert_quality(torch_output, tt_out_torch, pcc=0.99996, relative_rmse=0.009)
diff --git a/code/models/tt_dit/tests/models/flux2/device_params.py b/code/models/tt_dit/tests/models/flux2/device_params.py
new file mode 100644
index 0000000000000000000000000000000000000000..b41c716aadb0247a074d94510bd6d443d4ac830e
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux2/device_params.py
@@ -0,0 +1,54 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Shared ``device_params`` dicts for the Flux2 test suite.
+
+Every Flux2 test parametrizes the repo-global ``device_params`` fixture (indirect) with one of
+these. They build on the fabric/topology base dicts in ``models/tt_dit/utils/test.py`` and layer on
+the three knobs Flux2 needs: an L1_SMALL region for the VAE conv2d, a trace region for the
+transformer denoising loop, and ``require_exact_physical_num_devices`` so one CI command self-selects
+the right mesh per SKU. Centralizing them here keeps the L1_SMALL value and the req-exact overlay
+defined once instead of copy-pasted across the five test modules.
+"""
+
+from ....utils.test import line_params_8k, line_params_req_exact_devices, ring_params_8k
+
+# Applied to a row so it self-skips unless the mesh it asks for is exactly what the machine has.
+REQ_EXACT = {"require_exact_physical_num_devices": True}
+# The Flux2 VAE uses conv2d, which needs L1_SMALL buffers on every mesh.
+L1_SMALL_SIZE = 65536
+# The transformer traces the denoising loop.
+TRACE_REGION_SIZE = 31_000_000
+
+# Base fabric/topology params + L1_SMALL for the VAE conv2d. line_params_flux2 already carries
+# require_exact (via line_params_req_exact_devices).
+line_params_flux2 = {**line_params_req_exact_devices, "l1_small_size": L1_SMALL_SIZE}
+line_params_8k_flux2 = {**line_params_8k, "l1_small_size": L1_SMALL_SIZE}
+ring_params_8k_flux2 = {**ring_params_8k, "l1_small_size": L1_SMALL_SIZE}
+ring_params_8k_flux2_req_exact = {**ring_params_8k_flux2, **REQ_EXACT}
+
+# Transformer: base + trace region.
+line_params_flux2_transformer = {**line_params_flux2, "trace_region_size": TRACE_REGION_SIZE}
+
+# Performance: base + req-exact so each row self-selects its mesh.
+line_params_flux2_perf = {**line_params_flux2, **REQ_EXACT}
+line_params_8k_flux2_perf = {**line_params_8k_flux2, **REQ_EXACT}
+ring_params_8k_flux2_perf = {**ring_params_8k_flux2, **REQ_EXACT}
+
+# VAE single-device row: L1_SMALL only, no fabric and no require_exact.
+# Don't set require_exact so it runs on every machine rather than only on a literal one-chip host, which doesn't exist.
+
+# No fabric because FABRIC_1D on a 1-device submesh of a multi-chip box doesn't work: fabric gets initialized
+# on 1 device, but router sync still waits on a router
+# on another chip (that isn't configured), so the handshake stays at STARTED and times
+# out in fabric_firmware_initializer.cpp.
+
+# Even if the bug above were fixed, CCLManager.all_gather returns early when shape[mesh_axis] == 1, _all_gather_hw
+# when both factors are 1, vae_all_gather when the cluster axis is 1 -- so no CCL op is ever
+# issued and fabric is redundant.
+single_device_params_flux2 = {"l1_small_size": L1_SMALL_SIZE}
+
+# Prompt encoder: fabric + req-exact, no L1_SMALL (it has no conv2d). Same shape as
+# line_params_req_exact_devices; aliased here so all Flux2 device params have one home.
+prompt_encoder_params_flux2 = line_params_req_exact_devices
diff --git a/code/models/tt_dit/tests/models/flux2/run_flux2_tests.sh b/code/models/tt_dit/tests/models/flux2/run_flux2_tests.sh
new file mode 100644
index 0000000000000000000000000000000000000000..46d795b209f1c8e15687fbd1783a3b3965763264
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux2/run_flux2_tests.sh
@@ -0,0 +1,271 @@
+#!/usr/bin/env bash
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Run the FLUX.2 unit + e2e test suites on a single host.
+#
+# The FLUX.2 CI legs in tests/pipeline_reorg/*.yaml cover only 1024x1024 -- one
+# quietbox config and one galaxy config -- so the legs below are derived from the
+# test files themselves to sweep wider. Two things worth knowing:
+#
+# * Every flux2 test sets require_exact_physical_num_devices, so a (2,2) or (1,8)
+# parametrization self-skips on a 32-chip galaxy. Legs still pin a mesh id where
+# a file offers more than one row matching the host. Use --mesh/-k to widen.
+# * black-forest-labs/FLUX.2-dev is a manually gated HF repo (CI is blocked on
+# it, see #54499) and is not staged under /mnt/models. You need an HF token
+# on its allowlist; see PREREQUISITES below.
+#
+# All resolutions run by default (1024 -> 4096 for perf); cap with --max-res.
+#
+# Usage:
+# ./run_flux2_tests.sh # everything, arch auto-detected
+# ./run_flux2_tests.sh --suite unit
+# ./run_flux2_tests.sh --max-res 2048 # skip the 4096 perf leg
+# ./run_flux2_tests.sh --list # collect only, run nothing
+# ./run_flux2_tests.sh -- -x # trailing args go to pytest
+
+set -uo pipefail
+
+SUITE=all
+ARCH=auto
+MESH=auto
+EXTRA_K=""
+MAX_RES=4096
+WITH_PROFILE=0
+LIST_ONLY=0
+DRY_RUN=0
+FAIL_FAST=0
+LOG_DIR=""
+PYTEST_EXTRA=()
+
+usage() {
+ sed -n '6,28p' "$0" | sed 's/^# \?//'
+ cat <<'EOF'
+
+Options:
+ --suite {unit,e2e,all} Which suite(s) to run (default: all)
+ --arch {bh,wh,auto} Pick the arch-specific parametrizations (default: auto)
+ --mesh ID Override the mesh-id filter for every leg (advanced;
+ the per-test ids differ, so this is usually wrong --
+ prefer -k)
+ -k EXPR Extra pytest -k expression, ANDed into every leg
+ --max-res N Skip perf/profile resolutions above N
+ (N in 1024 2048 4096; default 4096)
+ --with-profile Also run test_transformer_profile (72 heavy cases,
+ a profiling sweep, not a correctness test)
+ --list pytest --collect-only -q for each leg
+ --dry-run Print the commands without running them
+ --fail-fast Stop after the first failing leg
+ --log-dir DIR Where to write per-leg logs and generated images
+ (default: $TT_METAL_HOME/generated/flux2_tests/)
+ -h, --help This message
+
+PREREQUISITES
+ * Activate the venv: source $TT_METAL_HOME/python_env/bin/activate
+ * FLUX.2-dev is gated. Request access at
+ https://huggingface.co/black-forest-labs/FLUX.2-dev then either
+ `huggingface-cli login` or export HF_TOKEN=. Without it every leg
+ fails with a 403 GatedRepoError.
+ * First run downloads the full FLUX.2-dev checkpoint (tens of GB); it is not
+ pre-staged under /mnt/models (only FLUX.1-dev/schnell are).
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --suite) SUITE="$2"; shift 2 ;;
+ --arch) ARCH="$2"; shift 2 ;;
+ --mesh) MESH="$2"; shift 2 ;;
+ -k) EXTRA_K="$2"; shift 2 ;;
+ --max-res) MAX_RES="$2"; shift 2 ;;
+ --with-profile) WITH_PROFILE=1; shift ;;
+ --list) LIST_ONLY=1; shift ;;
+ --dry-run) DRY_RUN=1; shift ;;
+ --fail-fast) FAIL_FAST=1; shift ;;
+ --log-dir) LOG_DIR="$2"; shift 2 ;;
+ -h|--help) usage; exit 0 ;;
+ --) shift; PYTEST_EXTRA=("$@"); break ;;
+ *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
+ esac
+done
+
+case "$SUITE" in unit|e2e|all) ;; *) echo "--suite must be unit|e2e|all" >&2; exit 2 ;; esac
+case "$ARCH" in bh|wh|auto) ;; *) echo "--arch must be bh|wh|auto" >&2; exit 2 ;; esac
+case "$MAX_RES" in 1024|2048|4096) ;; *) echo "--max-res must be 1024|2048|4096" >&2; exit 2 ;; esac
+
+# ---------------------------------------------------------------------------
+# Repo / arch discovery
+# ---------------------------------------------------------------------------
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+: "${TT_METAL_HOME:=$(cd "${SCRIPT_DIR}/../../../../.." && pwd)}"
+export TT_METAL_HOME
+export PYTHONPATH="${PYTHONPATH:-${TT_METAL_HOME}}"
+
+if [[ "$ARCH" == auto && -n "${ARCH_NAME:-}" ]]; then
+ case "$ARCH_NAME" in *blackhole*) ARCH=bh ;; *wormhole*) ARCH=wh ;; esac
+fi
+if [[ "$ARCH" == auto ]]; then
+ smi="$(tt-smi -ls 2>/dev/null)"
+ if grep -qi blackhole <<<"$smi"; then ARCH=bh
+ elif grep -qi wormhole <<<"$smi"; then ARCH=wh
+ else echo "could not detect arch, pass --arch bh|wh" >&2; exit 2
+ fi
+fi
+
+# Per-test mesh parametrization ids. The ids differ between files, hence one
+# variable per test rather than a single global mesh filter.
+VAE_ID="4x8" # test_vae_flux2.py also has 1x1, 1x8
+ENC_ID="4x8" # test_prompt_encoder.py only 4x8 exists
+PIPE_ID="4x8" # test_pipeline_flux2.py both 4x8 rows; the 4-link one self-skips on BH
+if [[ "$ARCH" == bh ]]; then
+ XF_ID="bh_4x8_ring" # test_transformer_flux2.py::test_transformer
+ PERF_IDS="bh_glx_linear or bh_glx_ring_sp0tp1_nofsdp or bh_glx_ring_sp1tp0 or bh_glx_ring_sp0tp1_fsdp"
+ PROFILE_IDS="bh_4x8_ring_nofsdp or bh_4x8_ring_sp1tp0_nofsdp or bh_4x8_ring_fsdp"
+else
+ XF_ID="wh_2x4_linear"
+ PERF_IDS="" # test_performance_flux2.py parametrizes BH meshes only
+ PROFILE_IDS="" # test_transformer_profile is BH-only too
+fi
+if [[ "$MESH" != auto ]]; then
+ VAE_ID="$MESH"; ENC_ID="$MESH"; XF_ID="$MESH"; PIPE_ID="$MESH"
+ PERF_IDS="$MESH"; PROFILE_IDS="$MESH"
+fi
+
+# Resolutions, capped by --max-res.
+ALL_RES=(1024 2048 4096)
+RES=()
+for r in "${ALL_RES[@]}"; do (( r <= MAX_RES )) && RES+=("$r"); done
+
+TS="$(date +%Y%m%d-%H%M%S)"
+: "${LOG_DIR:=${TT_METAL_HOME}/generated/flux2_tests/${TS}}"
+mkdir -p "$LOG_DIR"
+
+# ---------------------------------------------------------------------------
+# Environment (same conventions as the Flux.1 CI legs)
+# ---------------------------------------------------------------------------
+unset TT_METAL_WATCHER # blocked by #50886
+unset HF_HUB_OFFLINE # FLUX.2-dev is not pre-staged; the hub is the source
+export NO_PROMPT=1 # test_pipeline_flux2 blocks on input() without this
+export TT_DIT_CACHE_DIR="${TT_DIT_CACHE_DIR:-/tmp/TT_DIT_CACHE}"
+if [[ -z "${HF_TOKEN:-}" && ! -f "${HF_HOME:-$HOME/.cache/huggingface}/token" ]]; then
+ echo "WARNING: no HF credentials found (HF_TOKEN unset, no cached token)." >&2
+ echo " black-forest-labs/FLUX.2-dev is gated; expect 403 GatedRepoError." >&2
+fi
+
+FLUX2_DIR="models/tt_dit/tests/models/flux2"
+
+# ---------------------------------------------------------------------------
+# Leg runner
+# ---------------------------------------------------------------------------
+declare -a RESULTS=()
+FAILED=0
+
+# run [k_expr] [extra pytest args...]
+run() {
+ local name="$1" timeout="$2" target="$3" kexpr="${4:-}"
+ shift 4 2>/dev/null || shift $#
+
+ if [[ -n "$EXTRA_K" ]]; then
+ if [[ -n "$kexpr" ]]; then kexpr="($kexpr) and ($EXTRA_K)"; else kexpr="$EXTRA_K"; fi
+ fi
+
+ local -a cmd=(pytest --timeout "$timeout" "$target")
+ [[ -n "$kexpr" ]] && cmd+=(-k "$kexpr")
+ cmd+=("$@")
+ (( LIST_ONLY )) && cmd+=(--collect-only -q)
+ cmd+=("${PYTEST_EXTRA[@]+"${PYTEST_EXTRA[@]}"}")
+
+ local log="${LOG_DIR}/${name}.log"
+ echo
+ echo "=============================================================================="
+ echo "[$name] ${cmd[*]}"
+ echo " log: $log"
+ echo "=============================================================================="
+
+ if (( DRY_RUN )); then
+ RESULTS+=("DRYRUN $name")
+ return 0
+ fi
+
+ local start=$SECONDS
+ ( cd "$TT_METAL_HOME" && "${cmd[@]}" ) 2>&1 | tee "$log"
+ local rc=${PIPESTATUS[0]}
+ local dur=$(( SECONDS - start ))
+
+ # test_pipeline_flux2 saves flux2_*.png into the cwd; keep them with the logs.
+ shopt -s nullglob
+ for png in "$TT_METAL_HOME"/flux2_*.png; do mv -f "$png" "$LOG_DIR/"; done
+ shopt -u nullglob
+
+ if (( rc == 0 )); then
+ RESULTS+=("$(printf 'PASS %-34s %5ds' "$name" "$dur")")
+ else
+ RESULTS+=("$(printf 'FAIL(%d) %-34s %5ds %s' "$rc" "$name" "$dur" "$log")")
+ FAILED=1
+ if (( FAIL_FAST )); then summary; exit 1; fi
+ fi
+}
+
+skip_leg() {
+ RESULTS+=("$(printf 'SKIP %-34s %s' "$1" "$2")")
+ echo
+ echo "[skip] $1 -- $2"
+}
+
+summary() {
+ echo
+ echo "=============================================================================="
+ echo "FLUX.2 test summary (suite=$SUITE arch=$ARCH max-res=$MAX_RES)"
+ echo "logs/images: $LOG_DIR"
+ echo "=============================================================================="
+ printf '%s\n' "${RESULTS[@]+"${RESULTS[@]}"}"
+}
+
+want() { [[ "$1" == all || "$1" == "$2" ]]; }
+
+# ---------------------------------------------------------------------------
+# Unit legs -- PCC/correctness against the torch reference
+# ---------------------------------------------------------------------------
+if want "$SUITE" unit; then
+ # VAE decoder, pruned to 1 resnet per up_block by the test itself.
+ run flux2-unit-vae 3000 "$FLUX2_DIR/test_vae_flux2.py" "$VAE_ID"
+ # Mistral3 prompt encoder + the prompt-embedding upsampler.
+ run flux2-unit-encoder 3000 "$FLUX2_DIR/test_prompt_encoder.py" "$ENC_ID"
+ # Both all_blocks and single_blocks variants at 1024x1024.
+ run flux2-unit-transformer 10000 "$FLUX2_DIR/test_transformer_flux2.py::test_transformer" "$XF_ID"
+ # 12-step 1024x1024 generation; writes flux2_*.png next to the logs.
+ run flux2-unit-pipeline 10000 "$FLUX2_DIR/test_pipeline_flux2.py" "$PIPE_ID"
+fi
+
+# ---------------------------------------------------------------------------
+# e2e legs -- end-to-end throughput, one leg per resolution
+# ---------------------------------------------------------------------------
+if want "$SUITE" e2e; then
+ if [[ -z "$PERF_IDS" ]]; then
+ skip_leg flux2-e2e-perf "test_performance_flux2.py parametrizes Blackhole meshes only"
+ else
+ for r in "${RES[@]}"; do
+ run "flux2-e2e-perf-${r}" 10000 "$FLUX2_DIR/test_performance_flux2.py" \
+ "($PERF_IDS) and ${r}x${r}"
+ done
+ fi
+
+ if (( WITH_PROFILE )); then
+ if [[ -z "$PROFILE_IDS" ]]; then
+ skip_leg flux2-e2e-transformer-profile "test_transformer_profile is Blackhole-only"
+ else
+ for r in "${RES[@]}"; do
+ (( r > 4096 )) && continue # profile sweep tops out at 4096
+ run "flux2-e2e-xf-profile-${r}" 10000 \
+ "$FLUX2_DIR/test_transformer_flux2.py::test_transformer_profile" \
+ "($PROFILE_IDS) and ${r}"
+ done
+ fi
+ else
+ skip_leg flux2-e2e-transformer-profile "profiling sweep, opt in with --with-profile"
+ fi
+fi
+
+summary
+exit $FAILED
diff --git a/code/models/tt_dit/tests/models/flux2/test_performance_flux2.py b/code/models/tt_dit/tests/models/flux2/test_performance_flux2.py
new file mode 100644
index 0000000000000000000000000000000000000000..535b739257a17f03c1b834861d9a1d65986090a3
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux2/test_performance_flux2.py
@@ -0,0 +1,293 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+import os
+import statistics
+
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.common.utility_functions import is_blackhole
+from models.perf.benchmarking_utils import BenchmarkData, BenchmarkProfiler
+
+from ....pipelines.flux2.pipeline_flux2 import Flux2Pipeline
+from .device_params import line_params_8k_flux2_perf, line_params_flux2_perf, ring_params_8k_flux2_perf
+
+NUM_INFERENCE_STEPS = 50
+NUM_PERF_RUNS = 3
+
+# Upper bounds in seconds, 1.5x the 1024x1024 / 50-step numbers
+# These are more like regression tripwires, not a-priori determined perf targets.
+#
+# The key is (mesh shape, width, topology, sp_axis, is_fsdp), which is what it takes to name one
+# parametrization uniquely.
+#
+# Configs absent from this table are reported but we don't asssert on any perf targets.
+PERF_THRESHOLDS = {
+ ((4, 8), 1024, ttnn.Topology.Ring, 0, False): {
+ "total_encoding_time": 0.13, # measured 0.0971
+ "denoising_steps_time": 0.20 * NUM_INFERENCE_STEPS, # measured 0.1476/step, 7.3997 total
+ "vae_decoding_time": 0.49, # measured 0.1797
+ "total_time": 9.8125, # measured 7.6956
+ },
+}
+
+# Profiler step name -> the PERF_THRESHOLDS key it is gated against.
+_STEP_TO_METRIC = {
+ "encoder": "total_encoding_time",
+ "denoising": "denoising_steps_time",
+ "vae": "vae_decoding_time",
+ "run": "total_time",
+}
+
+
+@pytest.mark.timeout(6000)
+@pytest.mark.parametrize(
+ "width, height",
+ [
+ (1024, 1024),
+ (2048, 2048),
+ (4096, 4096),
+ (8192, 8192),
+ ],
+ ids=[
+ "1024x1024",
+ "2048x2048",
+ "4096x4096",
+ "8192x8192",
+ ],
+)
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis, encoder_tp_axis, vae_tp_axis, topology, num_links, is_fsdp, dynamic_load, device_params",
+ [
+ # Without dynamic_load the encoder OOMs during weight conversion on 4 chips.
+ [(2, 2), 0, 1, 1, 1, ttnn.Topology.Linear, 2, True, True, line_params_flux2_perf],
+ [(2, 4), 0, 1, 1, 1, ttnn.Topology.Linear, 2, False, False, line_params_flux2_perf],
+ [(4, 8), 0, 1, 1, 0, ttnn.Topology.Linear, 2, False, False, line_params_8k_flux2_perf],
+ [(4, 8), 0, 1, 1, 0, ttnn.Topology.Ring, 2, False, False, ring_params_8k_flux2_perf],
+ [(4, 8), 1, 0, 1, 0, ttnn.Topology.Ring, 2, False, False, ring_params_8k_flux2_perf],
+ [(4, 8), 0, 1, 1, 0, ttnn.Topology.Ring, 2, True, False, ring_params_8k_flux2_perf],
+ ],
+ ids=[
+ "bh_qb",
+ "bh_lb",
+ "bh_glx_linear",
+ "bh_glx_ring_sp0tp1_nofsdp",
+ "bh_glx_ring_sp1tp0",
+ "bh_glx_ring_sp0tp1_fsdp",
+ ],
+ indirect=["mesh_device", "device_params"],
+)
+def test_flux2_performance(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ sp_axis: int,
+ tp_axis: int,
+ encoder_tp_axis: int,
+ vae_tp_axis: int,
+ topology: ttnn.Topology,
+ num_links: int,
+ is_fsdp: bool,
+ dynamic_load: bool,
+ width: int,
+ height: int,
+ model_location_generator,
+ is_ci_env: bool,
+) -> None:
+ """Performance test for Flux2 pipeline across machine types and resolutions."""
+
+ benchmark_profiler = BenchmarkProfiler()
+
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ tp_factor = tuple(mesh_device.shape)[tp_axis]
+
+ logger.info(
+ f"Config: {width}x{height}, mesh={tuple(mesh_device.shape)}, "
+ f"sp={sp_factor}, tp={tp_factor}, topology={topology}"
+ )
+
+ # Pipeline creation includes internal warmup (2 steps at target resolution).
+ pipeline = Flux2Pipeline.create_pipeline(
+ mesh_device=mesh_device,
+ checkpoint_name=model_location_generator("black-forest-labs/FLUX.2-dev"),
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ encoder_tp_axis=encoder_tp_axis,
+ vae_tp_axis=vae_tp_axis,
+ vae_h_axis=1 - vae_tp_axis,
+ vae_w_axis=None, # 1-vae_tp_axis,
+ num_links=num_links,
+ topology=topology,
+ width=width,
+ height=height,
+ dynamic_load=dynamic_load,
+ is_fsdp=is_fsdp,
+ trace_warmup=True,
+ shard_prompt=True,
+ )
+
+ # Performance measurement runs
+ logger.info(f"Running {NUM_PERF_RUNS} timed iterations...")
+
+ prompts = [
+ "A photo of a cat sitting on a windowsill at sunset",
+ "A lifelike, warm portrait of a woman in her late 30s, shot on an 85mm lens, golden-hour lighting casting a soft glow, shallow depth of field, sharp focus on the eyes, highly detailed skin texture, photorealistic editorial style.",
+ "Create an image of an alien world with floating rock islands, massive glowing tropical plants, and twin moons in the night sky. Wide-angle shot, cinematic lighting, vivid colors, highly detailed sci-fi concept art.",
+ ]
+
+ results = []
+ for i in range(NUM_PERF_RUNS):
+ logger.info(f"Performance run {i + 1}/{NUM_PERF_RUNS}...")
+
+ ttnn.synchronize_device(mesh_device)
+ ttnn.distributed_context_barrier()
+
+ with benchmark_profiler("run", iteration=i):
+ with torch.no_grad():
+ images = pipeline(
+ prompts=[prompts[i % len(prompts)]],
+ num_inference_steps=NUM_INFERENCE_STEPS,
+ seed=42,
+ traced=True,
+ profiler=benchmark_profiler,
+ profiler_iteration=i,
+ )
+ results.append(images)
+ ttnn.synchronize_device(mesh_device)
+
+ logger.info(f" Run {i + 1} completed in {benchmark_profiler.get_duration('run', i):.2f}s")
+
+ # Collect timing data across all runs
+ encoder_times = [benchmark_profiler.get_duration("encoder", i) for i in range(NUM_PERF_RUNS)]
+ denoising_times = [benchmark_profiler.get_duration("denoising", i) for i in range(NUM_PERF_RUNS)]
+ vae_times = [benchmark_profiler.get_duration("vae", i) for i in range(NUM_PERF_RUNS)]
+ total_times = [benchmark_profiler.get_duration("run", i) for i in range(NUM_PERF_RUNS)]
+
+ all_denoising_steps = []
+ for i in range(NUM_PERF_RUNS):
+ for j in range(NUM_INFERENCE_STEPS):
+ if benchmark_profiler.contains_step(f"denoising_step_{j}", i):
+ all_denoising_steps.append(benchmark_profiler.get_duration(f"denoising_step_{j}", i))
+
+ # Save output image on rank 0 (from last run)
+ if not is_ci_env:
+ rank = int(ttnn.distributed_context_get_rank())
+ if rank == 0:
+ for i in range(NUM_PERF_RUNS):
+ mesh_tag = "x".join(str(s) for s in mesh_device.shape)
+ output_path = f"flux2_{mesh_tag}_{width}x{height}_{i}.png"
+ results[i][0].save(output_path)
+ logger.info(f"Image {i} saved as {output_path}")
+
+ # Report results
+ print("\n" + "=" * 80)
+ print("FLUX2 PERFORMANCE RESULTS")
+ print("=" * 80)
+ print(f"Image Size: {width}x{height}")
+ print(f"Inference Steps: {NUM_INFERENCE_STEPS}")
+ print(f"Performance Runs: {NUM_PERF_RUNS}")
+ print(f"DiT Configuration: sp={sp_factor}, tp={tp_factor}")
+ print(f"Mesh Shape: {tuple(mesh_device.shape)}")
+ print(f"Topology: {topology}")
+ print("-" * 80)
+
+ def print_stats(name, times):
+ if not times:
+ print(f"{name:25} | No data available")
+ return
+ mean_time = statistics.mean(times)
+ std_time = statistics.stdev(times) if len(times) > 1 else 0
+ min_time = min(times)
+ max_time = max(times)
+ print(
+ f"{name:25} | Mean: {mean_time:8.4f}s | Std: {std_time:8.4f}s | "
+ f"Min: {min_time:8.4f}s | Max: {max_time:8.4f}s"
+ )
+
+ print_stats("Encoding", encoder_times)
+ print_stats("Denoising (per step)", all_denoising_steps)
+ print_stats("Denoising (total)", denoising_times)
+ print_stats("VAE Decoding", vae_times)
+ print_stats("Total Pipeline", total_times)
+ print("-" * 80)
+
+ # Additional metrics
+ avg_total = statistics.mean(total_times)
+ avg_encoding = statistics.mean(encoder_times)
+ avg_denoising = statistics.mean(denoising_times)
+ avg_vae = statistics.mean(vae_times)
+
+ if all_denoising_steps:
+ print(f"Average total denoising time: {avg_denoising:.4f}s")
+ print(f"Denoising throughput: {NUM_INFERENCE_STEPS / avg_denoising:.2f} steps/second")
+
+ print(f"Overall throughput: {1 / avg_total:.4f} images/second")
+
+ print(f"\nTime breakdown:")
+ print(f" Encoding: {avg_encoding / avg_total * 100:5.1f}%")
+ print(f" Denoising: {avg_denoising / avg_total * 100:5.1f}%")
+ print(f" VAE: {avg_vae / avg_total * 100:5.1f}%")
+
+ print("=" * 80)
+
+ measurements = {
+ "total_encoding_time": avg_encoding,
+ "denoising_steps_time": avg_denoising,
+ "vae_decoding_time": avg_vae,
+ "total_time": avg_total,
+ }
+ perf_key = (tuple(mesh_device.shape), width, topology, sp_axis, is_fsdp)
+ expected_metrics = PERF_THRESHOLDS.get(perf_key)
+ if expected_metrics is None:
+ logger.warning(
+ f"No perf thresholds for {perf_key}: reporting timings only. "
+ "Add a PERF_THRESHOLDS entry to gate this config against regressions."
+ )
+
+ if is_ci_env:
+ device_name_map = {
+ (2, 2): "BH_QB",
+ (2, 4): "BH_LB" if is_blackhole() else "WH_T3K",
+ (4, 8): "BH_GLX" if is_blackhole() else "WH_GLX",
+ }
+
+ benchmark_data = BenchmarkData()
+ for i in range(NUM_PERF_RUNS):
+ for step_name in ["encoder", "denoising", "vae", "run"]:
+ value = benchmark_profiler.get_duration(step_name, i)
+ benchmark_data.add_measurement(
+ profiler=benchmark_profiler,
+ iteration=i,
+ step_name=step_name,
+ name=step_name,
+ value=value,
+ target=(expected_metrics or {}).get(_STEP_TO_METRIC[step_name], value),
+ )
+ benchmark_data.save_partial_run_json(
+ benchmark_profiler,
+ run_type=device_name_map.get(tuple(mesh_device.shape), "UNKNOWN"),
+ ml_model_name=os.path.basename(pipeline.checkpoint_name),
+ batch_size=1,
+ config_params={
+ "width": width,
+ "height": height,
+ "num_steps": NUM_INFERENCE_STEPS,
+ "num_perf_runs": NUM_PERF_RUNS,
+ "sp_factor": sp_factor,
+ "tp_factor": tp_factor,
+ "topology": str(topology),
+ },
+ )
+
+ if expected_metrics is not None:
+ regressions = [
+ f" {k}: {measurements[k]:.4f}s exceeds threshold {expected_metrics[k]:.4f}s"
+ for k in expected_metrics
+ if measurements[k] > expected_metrics[k]
+ ]
+ assert not regressions, f"FLUX.2 performance regression for {perf_key}:\n" + "\n".join(regressions)
+
+ logger.info("Performance test completed successfully!")
diff --git a/code/models/tt_dit/tests/models/flux2/test_pipeline_flux2.py b/code/models/tt_dit/tests/models/flux2/test_pipeline_flux2.py
new file mode 100644
index 0000000000000000000000000000000000000000..e19757a2a5b237ff04c830397bb05503651f7fa9
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux2/test_pipeline_flux2.py
@@ -0,0 +1,128 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+import itertools
+import os
+
+import pytest
+from loguru import logger
+
+import ttnn
+from models.common.utility_functions import is_blackhole
+
+from ....pipelines.flux2.pipeline_flux2 import Flux2Pipeline
+from ....utils.test import skip_if_unsupported_num_links
+from .device_params import line_params_flux2
+
+
+@pytest.mark.parametrize(
+ "no_prompt",
+ [{"1": True, "0": False}.get(os.environ.get("NO_PROMPT"), False)],
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [line_params_flux2],
+ indirect=True,
+)
+@pytest.mark.parametrize(("width", "height", "num_inference_steps"), [(1024, 1024, 12)])
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis, encoder_tp_axis, vae_tp_axis, topology, num_links, is_fsdp, dynamic_load, traced",
+ [
+ # is_fsdp and dynamic_load are both mandatory for 2x2 -- the 32B transformer, the 24B encoder and the VAE
+ # cannot be co-resident on 4 chips.
+ [(2, 2), 0, 1, 1, 1, ttnn.Topology.Linear, 2, True, True, False],
+ [(1, 8), 0, 1, 1, 1, ttnn.Topology.Linear, 1, False, True, False],
+ [(4, 8), 0, 1, 1, 0, ttnn.Topology.Linear, 4, True, False, True],
+ [(4, 8), 0, 1, 1, 0, ttnn.Topology.Linear, 2, False, False, True],
+ ],
+ ids=[
+ "2x2sp0tp1vaetp1nl2_linear_is_fsdp1",
+ "1x8sp0tp1vaetp1nl1_linear_is_fsdp0",
+ "4x8sp0tp1vaetp0nl4_linear_is_fsdp1",
+ "4x8sp0tp1vaetp0nl2_linear_is_fsdp0",
+ ],
+ indirect=["mesh_device"],
+)
+def test_pipeline(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ width: int,
+ height: int,
+ num_inference_steps: int,
+ sp_axis: int,
+ tp_axis: int,
+ encoder_tp_axis: int,
+ vae_tp_axis: int,
+ topology: ttnn.Topology,
+ num_links: int,
+ no_prompt: bool,
+ traced: bool,
+ is_fsdp: bool,
+ dynamic_load: bool,
+ model_location_generator,
+ is_ci_env: bool,
+) -> None:
+ skip_if_unsupported_num_links(mesh_device, num_links)
+
+ pipeline = Flux2Pipeline.create_pipeline(
+ mesh_device=mesh_device,
+ checkpoint_name=model_location_generator("black-forest-labs/FLUX.2-dev"),
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ encoder_tp_axis=encoder_tp_axis,
+ vae_tp_axis=vae_tp_axis,
+ vae_h_axis=1 - vae_tp_axis,
+ vae_w_axis=None,
+ num_links=num_links,
+ topology=topology,
+ width=width,
+ height=height,
+ is_fsdp=is_fsdp,
+ dynamic_load=dynamic_load,
+ trace_warmup=traced,
+ shard_prompt=True,
+ )
+
+ prompts = [
+ "Neon-lit cyberpunk alley, rain-soaked, cinematic wide shot",
+ # "Golden retriever astronaut drifting in sunlit space",
+ # "Minimalist Scandinavian kitchen, morning light, ultra clean",
+ # "Ancient desert temple at dawn, soft fog, wide angle",
+ # "Steampunk airship over Victorian city, dramatic clouds",
+ # "Macro dewdrops on fern, shallow depth of field",
+ # "Luxury wristwatch on marble, studio lighting, hyper-detail",
+ # "Stormy coastline lighthouse, crashing waves, long exposure",
+ # "Futuristic Tokyo street market, vibrant signage, motion blur",
+ ]
+
+ arch = "bh" if is_blackhole() else "wh"
+ mesh_tag = "x".join(str(s) for s in mesh_device.shape)
+ filename_prefix = f"flux2_{arch}_{width}_{height}_{mesh_tag}"
+ if not traced:
+ filename_prefix += "_untraced"
+
+ def run(*, prompt: str, number: int, seed: int) -> None:
+ images = pipeline(
+ prompts=[prompt],
+ num_inference_steps=num_inference_steps,
+ seed=seed,
+ traced=traced,
+ )
+
+ output_filename = f"{filename_prefix}_{number}.png"
+ images[0].save(output_filename)
+ logger.info(f"Image saved as {output_filename}")
+
+ if no_prompt:
+ for i, prompt in enumerate(prompts):
+ run(prompt=prompt, number=i, seed=0)
+ else:
+ prompt = prompts[0]
+ for i in itertools.count():
+ new_prompt = input("Enter the input prompt, or q to exit: ")
+ if new_prompt:
+ prompt = new_prompt
+ if prompt[0] == "q":
+ break
+ run(prompt=prompt, number=i, seed=i)
diff --git a/code/models/tt_dit/tests/models/flux2/test_prompt_encoder.py b/code/models/tt_dit/tests/models/flux2/test_prompt_encoder.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c09da8c25b14fcca5b2e483155f4790cae7d553
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux2/test_prompt_encoder.py
@@ -0,0 +1,108 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+
+import pytest
+from loguru import logger
+
+import ttnn
+
+from ....parallel.config import EncoderParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....pipelines.flux2.prompt_encoder import PromptEncoder
+from ....utils.check import assert_quality
+from .device_params import prompt_encoder_params_flux2
+
+
+@pytest.mark.parametrize(
+ "mesh_device",
+ [
+ pytest.param((2, 2), id="2x2"),
+ pytest.param((4, 8), id="4x8"),
+ ],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [prompt_encoder_params_flux2],
+ indirect=True,
+)
+def test_encode(mesh_device: ttnn.MeshDevice) -> None:
+ tp_axis = 1
+ sequence_length = 64
+ checkpoint_name = "black-forest-labs/FLUX.2-dev"
+ prompts = ["A painting", "A futuristic city skyline at sunset during winter"]
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=mesh_device.shape[tp_axis], mesh_axis=tp_axis),
+ )
+
+ tt_encoder = PromptEncoder(
+ checkpoint_name=checkpoint_name,
+ use_torch_encoder=False,
+ device=mesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+ tt_encoder.load_weights()
+
+ torch_encoder = PromptEncoder(
+ checkpoint_name=checkpoint_name,
+ use_torch_encoder=True,
+ device=mesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+
+ logger.info("running Torch model...")
+ torch_embeds, torch_mask = torch_encoder.encode(prompts, num_images_per_prompt=2, sequence_length=sequence_length)
+
+ logger.info("running TT model...")
+ tt_embeds, tt_mask = tt_encoder.encode(prompts, num_images_per_prompt=2, sequence_length=sequence_length)
+
+ assert torch_mask.equal(tt_mask)
+
+ assert_quality(torch_embeds, tt_embeds, pcc=0.9997, relative_rmse=0.04)
+
+
+@pytest.mark.parametrize(
+ "mesh_device",
+ [
+ pytest.param((2, 2), id="2x2"),
+ pytest.param((4, 8), id="4x8"),
+ ],
+ indirect=True,
+)
+@pytest.mark.parametrize(
+ "device_params",
+ [prompt_encoder_params_flux2],
+ indirect=True,
+)
+def test_upsample(mesh_device: ttnn.MeshDevice) -> None:
+ tp_axis = 1
+ checkpoint_name = "black-forest-labs/FLUX.2-dev"
+ prompts = ["A futuristic city skyline at sunset"]
+ temperature = 0.15
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+
+ parallel_config = EncoderParallelConfig(
+ tensor_parallel=ParallelFactor(factor=mesh_device.shape[tp_axis], mesh_axis=tp_axis),
+ )
+
+ tt_encoder = PromptEncoder(
+ checkpoint_name=checkpoint_name,
+ use_torch_encoder=False,
+ device=mesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+
+ tt_encoder.load_weights()
+
+ logger.info("running TT model...")
+ tt_output = tt_encoder.upsample(prompts, max_length=224, temperature=temperature)
+ print(tt_output)
diff --git a/code/models/tt_dit/tests/models/flux2/test_transformer_flux2.py b/code/models/tt_dit/tests/models/flux2/test_transformer_flux2.py
new file mode 100644
index 0000000000000000000000000000000000000000..81eac76333ef843c06e1aba3215fd7b6ad347394
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux2/test_transformer_flux2.py
@@ -0,0 +1,434 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+import os
+from typing import Protocol
+
+import diffusers
+import diffusers.models.transformers.transformer_flux2
+import pytest
+import torch
+from loguru import logger
+from tracy import signpost
+
+import ttnn
+
+from ....models.transformers.transformer_flux2 import Flux2Transformer
+from ....parallel.config import DiTGParallelConfigNoCFG, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....utils import cache, tensor
+from ....utils.check import assert_quality
+from ....utils.padding import PaddingConfig
+from .device_params import line_params_flux2_transformer, ring_params_8k_flux2, ring_params_8k_flux2_req_exact
+
+
+class ModelLocationGenerator(Protocol):
+ def __call__(
+ self,
+ model_version: str,
+ *,
+ model_subdir: str = "",
+ download_if_ci_v2: bool = False,
+ ci_v2_timeout_in_s: int = 300,
+ endpoint_prefix: str = "",
+ download_dir_suffix: str = "",
+ ) -> str:
+ ...
+
+
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis, topology, num_links, fsdp, shard_prompt, device_params",
+ [
+ [(2, 2), 0, 1, ttnn.Topology.Linear, 2, True, False, line_params_flux2_transformer],
+ [(1, 8), 0, 1, ttnn.Topology.Linear, 1, False, False, line_params_flux2_transformer],
+ [(2, 4), 0, 1, ttnn.Topology.Linear, 1, False, False, line_params_flux2_transformer],
+ # shard_prompt=True on 4x8. With it False the prompt is replicated across the sp axis
+ # while the spatial stream is sharded, and the joint attention PCC in the single blocks
+ # degrades as sp increases.
+ #
+ # In practice, for sp=1/2, PCC is not sufficiently degraded to matter, but it's terrible
+ # for sp=4 (way below PCC thresholds), which is why we _must_ set shard_prompt=True for this mesh.
+ # True is also what the pipeline and test_performance_flux2.py use.
+ [(4, 8), 0, 1, ttnn.Topology.Ring, 2, False, True, ring_params_8k_flux2_req_exact],
+ ],
+ ids=[
+ "bh_2x2_linear",
+ "1x8_linear",
+ "wh_2x4_linear",
+ "bh_4x8_ring",
+ ],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ ("batch_size", "height", "width", "prompt_seq_len"),
+ [
+ (1, 1024, 1024, 512),
+ ],
+)
+@pytest.mark.parametrize(
+ ("skip_layers", "skip_single_layers"),
+ [
+ pytest.param(0, 0, id="all_blocks"),
+ pytest.param(7, 47, id="single_blocks"),
+ ],
+)
+def test_transformer(
+ mesh_device: ttnn.MeshDevice,
+ sp_axis: int,
+ tp_axis: int,
+ topology: ttnn.Topology,
+ num_links: int,
+ fsdp: bool,
+ shard_prompt: bool,
+ batch_size: int,
+ height: int,
+ width: int,
+ prompt_seq_len: int,
+ skip_layers: int,
+ skip_single_layers: int,
+ model_location_generator: ModelLocationGenerator,
+) -> None:
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ tp_factor = tuple(mesh_device.shape)[tp_axis]
+
+ logger.info(
+ f"test_transformer: mesh={tuple(mesh_device.shape)}, sp={sp_factor}, tp={tp_factor}, "
+ f"topology={topology}, num_links={num_links}, fsdp={fsdp}"
+ )
+
+ model_name = model_location_generator("black-forest-labs/FLUX.2-dev", model_subdir="transformer")
+ torch_model = diffusers.Flux2Transformer2DModel.from_pretrained(model_name, subfolder="transformer")
+ assert isinstance(torch_model, diffusers.Flux2Transformer2DModel)
+ torch_model.eval()
+
+ assert 0 <= skip_layers < len(torch_model.transformer_blocks)
+ assert 0 <= skip_single_layers < len(torch_model.single_transformer_blocks)
+
+ del torch_model.transformer_blocks[len(torch_model.transformer_blocks) - skip_layers :]
+ del torch_model.single_transformer_blocks[len(torch_model.single_transformer_blocks) - skip_single_layers :]
+
+ head_dim = torch_model.config.attention_head_dim
+ num_heads = torch_model.config.num_attention_heads
+ in_channels = torch_model.in_channels
+ joint_attention_dim = torch_model.config.joint_attention_dim
+
+ ccl_manager = CCLManager(
+ mesh_device=mesh_device,
+ num_links=num_links,
+ topology=topology,
+ )
+
+ parallel_config = DiTGParallelConfigNoCFG(
+ tensor_parallel=ParallelFactor(factor=tp_factor, mesh_axis=tp_axis),
+ sequence_parallel=ParallelFactor(factor=sp_factor, mesh_axis=sp_axis),
+ )
+
+ if num_heads % tp_factor != 0:
+ padding_config = PaddingConfig.from_tensor_parallel_factor(num_heads, head_dim, tp_factor)
+ else:
+ padding_config = None
+
+ num_layers = torch_model.config.num_layers - skip_layers
+ num_single_layers = torch_model.config.num_single_layers - skip_single_layers
+
+ # HACK: The cache subfolder name modification is to ensure the cache contains all tensors for both the
+ # all_blocks and single_blocks test variants. Without this, we would get invalid caches for one or the other.
+ cache_subfolder = (
+ "transformer" if skip_layers == skip_single_layers == 0 else f"transformer_L{num_layers}_S{num_single_layers}"
+ )
+
+ tt_model = Flux2Transformer(
+ in_channels=in_channels,
+ num_layers=num_layers,
+ num_single_layers=num_single_layers,
+ attention_head_dim=head_dim,
+ num_attention_heads=num_heads,
+ joint_attention_dim=joint_attention_dim,
+ out_channels=torch_model.out_channels,
+ device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ padding_config=padding_config,
+ is_fsdp=fsdp,
+ shard_prompt=shard_prompt,
+ )
+
+ cache.load_model(
+ tt_model,
+ get_torch_state_dict=torch_model.state_dict,
+ model_name="FLUX.2-dev",
+ subfolder=cache_subfolder,
+ parallel_config=parallel_config,
+ mesh_shape=tuple(mesh_device.shape),
+ mesh_device=mesh_device,
+ is_fsdp=fsdp,
+ )
+
+ spatial_seq_len = height * width // 16**2
+
+ torch.manual_seed(0)
+ spatial = torch.randn([batch_size, spatial_seq_len, in_channels])
+ prompt = torch.randn([batch_size, prompt_seq_len, joint_attention_dim])
+ timestep = torch.full([batch_size], fill_value=500)
+ guidance = torch.full([batch_size], fill_value=3)
+
+ # prepare for ROPE
+ text_ids = _prepare_ids(text_sequence_length=prompt_seq_len)
+ image_ids = _prepare_ids(height=height // 16, width=width // 16)
+ prompt_rope_cos, prompt_rope_sin = torch_model.pos_embed.forward(text_ids)
+ spatial_rope_cos, spatial_rope_sin = torch_model.pos_embed.forward(image_ids)
+
+ tt_spatial = tensor.from_torch(spatial, device=mesh_device, mesh_axes=[None, sp_axis, None])
+ # The prompt and its RoPE follow shard_prompt, the same pairing test_transformer_profile uses.
+ prompt_mesh_axes = [None, sp_axis, None] if shard_prompt else None
+ prompt_rope_mesh_axes = [None, None, sp_axis, None] if shard_prompt else None
+ tt_prompt = tensor.from_torch(prompt, device=mesh_device, mesh_axes=prompt_mesh_axes)
+ tt_embedded_prompt = tt_model.context_embedder(tt_prompt)
+ tt_timestep = tensor.from_torch(timestep.unsqueeze(-1), dtype=ttnn.float32, device=mesh_device)
+ tt_guidance = tensor.from_torch(guidance.unsqueeze(-1), device=mesh_device)
+
+ tt_spatial_rope_cos = tensor.from_torch(
+ spatial_rope_cos.unsqueeze(0).unsqueeze(0), device=mesh_device, mesh_axes=[None, None, sp_axis, None]
+ )
+ tt_spatial_rope_sin = tensor.from_torch(
+ spatial_rope_sin.unsqueeze(0).unsqueeze(0), device=mesh_device, mesh_axes=[None, None, sp_axis, None]
+ )
+ tt_prompt_rope_cos = tensor.from_torch(
+ prompt_rope_cos.unsqueeze(0).unsqueeze(0), device=mesh_device, mesh_axes=prompt_rope_mesh_axes
+ )
+ tt_prompt_rope_sin = tensor.from_torch(
+ prompt_rope_sin.unsqueeze(0).unsqueeze(0), device=mesh_device, mesh_axes=prompt_rope_mesh_axes
+ )
+ tt_combined_rope = (
+ ttnn.concat([tt_spatial_rope_cos, tt_prompt_rope_cos], dim=2),
+ ttnn.concat([tt_spatial_rope_sin, tt_prompt_rope_sin], dim=2),
+ )
+
+ logger.info("running Torch model...")
+ with torch.no_grad():
+ torch_output = torch_model.forward(
+ hidden_states=spatial,
+ encoder_hidden_states=prompt,
+ timestep=timestep / 1000,
+ guidance=guidance,
+ img_ids=image_ids,
+ txt_ids=text_ids,
+ ).sample
+
+ logger.info("running TT model...")
+
+ signpost("t_start")
+ tt_output = tt_model.forward(
+ spatial=tt_spatial,
+ embedded_prompt=tt_embedded_prompt,
+ timestep=tt_timestep,
+ guidance=tt_guidance,
+ spatial_rope=(tt_spatial_rope_cos, tt_spatial_rope_sin),
+ prompt_rope=(tt_prompt_rope_cos, tt_prompt_rope_sin),
+ combined_rope=tt_combined_rope,
+ spatial_sequence_length=spatial_seq_len,
+ prompt_sequence_length=prompt_seq_len,
+ )
+ signpost("t_end")
+ tt_output_torch = tensor.to_torch(tt_output, mesh_axes=[None, sp_axis, None])
+ assert_quality(torch_output, tt_output_torch, pcc=0.996, relative_rmse=0.09)
+
+
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis, topology, num_links, fsdp, device_params",
+ [
+ [(4, 8), 0, 1, ttnn.Topology.Ring, 2, False, ring_params_8k_flux2],
+ [(4, 8), 1, 0, ttnn.Topology.Ring, 2, False, ring_params_8k_flux2],
+ [(4, 8), 0, 1, ttnn.Topology.Ring, 2, True, ring_params_8k_flux2],
+ ],
+ ids=[
+ "bh_4x8_ring_nofsdp",
+ "bh_4x8_ring_sp1tp0_nofsdp",
+ "bh_4x8_ring_fsdp",
+ ],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ ("batch_size", "height", "width", "prompt_seq_len"),
+ [
+ (1, 1024, 1024, 512),
+ (1, 2048, 2048, 512),
+ (1, 4096, 4096, 512),
+ ],
+ ids=[
+ "1024",
+ "2048",
+ "4096",
+ ],
+)
+@pytest.mark.parametrize("xformer_idx", [0, 1, 3, 4], ids=["x_single", "x_double", "x_both", "x_all"])
+@pytest.mark.parametrize("shard_prompt", [False, True], ids=["no_shard_prompt", "yes_shard_prompt"])
+def test_transformer_profile(
+ mesh_device: ttnn.MeshDevice,
+ sp_axis: int,
+ tp_axis: int,
+ topology: ttnn.Topology,
+ num_links: int,
+ fsdp: bool,
+ batch_size: int,
+ height: int,
+ width: int,
+ prompt_seq_len: int,
+ xformer_idx: int,
+ shard_prompt: bool,
+) -> None:
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ tp_factor = tuple(mesh_device.shape)[tp_axis]
+
+ logger.info(
+ f"test_transformer: mesh={tuple(mesh_device.shape)}, sp={sp_factor}, tp={tp_factor}, "
+ f"topology={topology}, num_links={num_links}"
+ )
+
+ model_name = "black-forest-labs/FLUX.2-dev"
+ torch_model = diffusers.Flux2Transformer2DModel.from_pretrained(
+ model_name, subfolder="transformer", torch_dtype=torch.bfloat16
+ )
+ torch_model.eval()
+ logger.info(f"single transformer blocks: {len(torch_model.single_transformer_blocks)}")
+ logger.info(f"transformer blocks: {len(torch_model.transformer_blocks)}")
+ if xformer_idx != 4:
+ torch_model.transformer_blocks = torch.nn.ModuleList(
+ [torch_model.transformer_blocks[0]] if xformer_idx > 0 else []
+ )
+ torch_model.single_transformer_blocks = torch.nn.ModuleList(
+ [torch_model.single_transformer_blocks[0]] if xformer_idx != 1 else []
+ )
+
+ ccl_manager = CCLManager(
+ mesh_device=mesh_device,
+ num_links=num_links,
+ topology=topology,
+ )
+
+ parallel_config = DiTGParallelConfigNoCFG(
+ tensor_parallel=ParallelFactor(factor=tp_factor, mesh_axis=tp_axis),
+ sequence_parallel=ParallelFactor(factor=sp_factor, mesh_axis=sp_axis),
+ )
+
+ pos_embed = torch_model.pos_embed
+ head_dim = 128
+ num_heads = 48
+ in_channels = 128
+ joint_attention_dim = 15360
+ num_channels_latents = 128 # after patchify
+
+ if num_heads % parallel_config.tensor_parallel.factor != 0:
+ padding_config = PaddingConfig.from_tensor_parallel_factor(
+ num_heads,
+ head_dim,
+ parallel_config.tensor_parallel.factor,
+ )
+ else:
+ padding_config = None
+
+ tt_model = Flux2Transformer(
+ in_channels=in_channels,
+ num_layers=torch_model.config.num_layers if xformer_idx == 4 else 1 if xformer_idx > 0 else 0,
+ num_single_layers=torch_model.config.num_single_layers if xformer_idx == 4 else 1 if xformer_idx != 1 else 0,
+ attention_head_dim=head_dim,
+ num_attention_heads=num_heads,
+ joint_attention_dim=joint_attention_dim,
+ out_channels=128,
+ device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ padding_config=padding_config,
+ is_fsdp=fsdp,
+ shard_prompt=shard_prompt,
+ )
+
+ cache.load_model(
+ tt_model,
+ get_torch_state_dict=torch_model.state_dict,
+ model_name=os.path.basename(model_name),
+ subfolder="transformer",
+ parallel_config=parallel_config,
+ mesh_shape=tuple(mesh_device.shape),
+ is_fsdp=fsdp,
+ )
+
+ spatial_seq_len = height * width // 16**2
+
+ torch.manual_seed(0)
+ spatial = torch.randn([batch_size, spatial_seq_len, num_channels_latents])
+ prompt = torch.randn([batch_size, prompt_seq_len, joint_attention_dim])
+ timestep = torch.full([batch_size], fill_value=500)
+ guidance = torch.full([batch_size], fill_value=3)
+
+ # prepare for ROPE
+ text_ids = _prepare_ids(text_sequence_length=prompt_seq_len)
+ image_ids = _prepare_ids(height=height // 16, width=width // 16)
+ prompt_rope_cos, prompt_rope_sin = pos_embed.forward(text_ids)
+ spatial_rope_cos, spatial_rope_sin = pos_embed.forward(image_ids)
+
+ prompt_mesh_axes = [None, sp_axis, None] if shard_prompt else None
+ prompt_rope_mesh_axes = [None, None, sp_axis, None] if shard_prompt else None
+
+ tt_spatial = tensor.from_torch(spatial, device=mesh_device, mesh_axes=[None, sp_axis, None])
+ tt_prompt = tensor.from_torch(prompt, device=mesh_device, mesh_axes=prompt_mesh_axes)
+ tt_embedded_prompt = tt_model.context_embedder(tt_prompt)
+ tt_timestep = tensor.from_torch(timestep.unsqueeze(-1), dtype=ttnn.float32, device=mesh_device)
+ tt_guidance = tensor.from_torch(guidance.unsqueeze(-1), device=mesh_device)
+
+ tt_spatial_rope_cos = tensor.from_torch(
+ spatial_rope_cos.unsqueeze(0).unsqueeze(0), device=mesh_device, mesh_axes=[None, None, sp_axis, None]
+ )
+ tt_spatial_rope_sin = tensor.from_torch(
+ spatial_rope_sin.unsqueeze(0).unsqueeze(0), device=mesh_device, mesh_axes=[None, None, sp_axis, None]
+ )
+ tt_prompt_rope_cos = tensor.from_torch(
+ prompt_rope_cos.unsqueeze(0).unsqueeze(0), device=mesh_device, mesh_axes=prompt_rope_mesh_axes
+ )
+ tt_prompt_rope_sin = tensor.from_torch(
+ prompt_rope_sin.unsqueeze(0).unsqueeze(0), device=mesh_device, mesh_axes=prompt_rope_mesh_axes
+ )
+ tt_combined_rope = (
+ ttnn.concat([tt_spatial_rope_cos, tt_prompt_rope_cos], dim=2),
+ ttnn.concat([tt_spatial_rope_sin, tt_prompt_rope_sin], dim=2),
+ )
+
+ # warmup
+ tt_model.forward(
+ spatial=tt_spatial,
+ embedded_prompt=tt_embedded_prompt,
+ timestep=tt_timestep,
+ guidance=tt_guidance,
+ spatial_rope=(tt_spatial_rope_cos, tt_spatial_rope_sin),
+ prompt_rope=(tt_prompt_rope_cos, tt_prompt_rope_sin),
+ combined_rope=tt_combined_rope,
+ spatial_sequence_length=spatial_seq_len,
+ prompt_sequence_length=prompt_seq_len,
+ compute_prompt_output=(xformer_idx in [0, 3]), # force computing eveything
+ )
+ ttnn.synchronize_device(mesh_device)
+ signpost("t_start")
+ tt_output = tt_model.forward(
+ spatial=tt_spatial,
+ embedded_prompt=tt_embedded_prompt,
+ timestep=tt_timestep,
+ guidance=tt_guidance,
+ spatial_rope=(tt_spatial_rope_cos, tt_spatial_rope_sin),
+ prompt_rope=(tt_prompt_rope_cos, tt_prompt_rope_sin),
+ combined_rope=tt_combined_rope,
+ spatial_sequence_length=spatial_seq_len,
+ prompt_sequence_length=prompt_seq_len,
+ compute_prompt_output=(xformer_idx in [0, 3]), # force computing eveything
+ )
+ ttnn.synchronize_device(mesh_device)
+ signpost("t_end")
+
+
+def _prepare_ids(*, height: int = 1, width: int = 1, text_sequence_length: int = 1) -> torch.Tensor:
+ t = torch.arange(1)
+ h = torch.arange(height)
+ w = torch.arange(width)
+ s = torch.arange(text_sequence_length)
+
+ return torch.cartesian_prod(t, h, w, s)
diff --git a/code/models/tt_dit/tests/models/flux2/test_vae_flux2.py b/code/models/tt_dit/tests/models/flux2/test_vae_flux2.py
new file mode 100644
index 0000000000000000000000000000000000000000..cbdb723288e638fb043d1105f570b55a3fa6b07e
--- /dev/null
+++ b/code/models/tt_dit/tests/models/flux2/test_vae_flux2.py
@@ -0,0 +1,163 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+
+import diffusers.models.autoencoders.autoencoder_kl_flux2 as reference
+import pytest
+import torch
+
+import ttnn
+
+from ....models.vae.vae_flux2 import Flux2VaeDecoder
+from ....parallel.config import Flux2VaeParallelConfig
+from ....parallel.manager import CCLManager
+from ....utils import tensor
+from ....utils.check import assert_quality
+from .device_params import line_params_flux2, single_device_params_flux2
+
+_LAYERS_PER_BLOCK = 1 # pruned from pretrained (2) to keep host run fast
+
+
+def _load_pruned_torch_model() -> reference.AutoencoderKLFlux2:
+ """Load the pretrained VAE and truncate each up_block to _LAYERS_PER_BLOCK resnets.
+
+ The mid_block is left intact — it already uses num_layers=1 (2 resnets, 1 attention)
+ matching TT's VaeMidBlock default, regardless of layers_per_block.
+ """
+ model = reference.AutoencoderKLFlux2.from_pretrained("black-forest-labs/FLUX.2-dev", subfolder="vae")
+ assert isinstance(model, reference.AutoencoderKLFlux2)
+ model.eval()
+ keep = _LAYERS_PER_BLOCK + 1
+ for up_block in model.decoder.up_blocks:
+ up_block.resnets = torch.nn.ModuleList(list(up_block.resnets)[:keep])
+ return model
+
+
+def _torch_decode_reference(torch_model: reference.AutoencoderKLFlux2, inp: torch.Tensor) -> torch.Tensor:
+ """Apply BN inv-normalize, unpatchify, then run the torch decoder.
+
+ inp: [B, C*p^2, H_t, W_t] patchified latent.
+ Returns [B, 3, H, W] (NCHW) for comparison with the TT output permuted to NCHW.
+ """
+ p = 2 # _PATCH_SIZE
+ z_channels = torch_model.config.latent_channels
+ bn_eps = torch_model.bn.eps
+ s = (torch_model.bn.running_var + bn_eps).sqrt() # [C*p^2]
+ m = torch_model.bn.running_mean # [C*p^2]
+ # Matches TT _inv_normalize: z * sqrt(var + eps) + mean
+ inp_norm = inp * s.view(1, -1, 1, 1) + m.view(1, -1, 1, 1)
+
+ # Unpatchify: [B, C*p^2, H_t, W_t] -> [B, C, H_lat, W_lat]
+ B, _, h_t, w_t = inp_norm.shape
+ z = inp_norm.reshape(B, z_channels, p, p, h_t, w_t)
+ z = z.permute(0, 1, 4, 2, 5, 3).contiguous()
+ z = z.reshape(B, z_channels, h_t * p, w_t * p)
+
+ with torch.no_grad():
+ return torch_model.decode(z).sample # [B, 3, H, W]
+
+
+def prep_data(
+ vae_parallel_config: Flux2VaeParallelConfig,
+ inp: torch.Tensor,
+ mesh_device: ttnn.Device,
+ tt_model: Flux2VaeDecoder,
+) -> ttnn.Tensor:
+ # inp: [B, C*p^2, H_t, W_t] patchified latent in torch.
+ # Convert to patchified token format [B, H_t*W_t, C*p^2].
+ inp_flat = inp.permute(0, 2, 3, 1).flatten(1, 2)
+
+ if vae_parallel_config.h_parallel is not None:
+ tt_latents = tensor.from_torch(
+ inp_flat,
+ device=mesh_device,
+ mesh_axes=(None, vae_parallel_config.h_parallel.mesh_axis, None),
+ )
+ else:
+ tt_latents = tensor.from_torch(inp_flat, device=mesh_device)
+
+ p = Flux2VaeDecoder._PATCH_SIZE
+ height_for_unp = inp.shape[2] * p
+ width_for_unp = inp.shape[3] * p
+
+ tt_latents = tt_model.preprocess_and_unpatchify(
+ tt_latents,
+ height=height_for_unp,
+ width=width_for_unp,
+ )
+
+ # W-sharding can only be applied after unpatchify because H and W are interleaved
+ # in the patchified token dimension.
+ if vae_parallel_config.w_parallel is not None:
+ tt_latents = ttnn.mesh_partition(
+ tt_latents,
+ dim=2,
+ cluster_axis=vae_parallel_config.w_parallel.mesh_axis,
+ memory_config=tt_latents.memory_config(),
+ )
+
+ return tt_latents
+
+
+@pytest.mark.parametrize(
+ "mesh_device, device_params",
+ [
+ [(1, 1), single_device_params_flux2],
+ [(2, 2), line_params_flux2],
+ [(1, 8), line_params_flux2],
+ [(4, 8), line_params_flux2],
+ ],
+ ids=["1x1", "2x2", "1x8", "4x8"],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ ("batch_size", "height", "width"),
+ [(1, 1024, 1024)],
+ ids=["1024"],
+)
+def test_vae_flux2_decoder(
+ *,
+ mesh_device: ttnn.Device,
+ batch_size: int,
+ height: int,
+ width: int,
+) -> None:
+ torch.manual_seed(0)
+
+ tp_axis = 0
+ h_axis = 1 - tp_axis
+ w_axis = None
+
+ torch_model = _load_pruned_torch_model()
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+ vae_parallel_config = Flux2VaeParallelConfig.from_axes(mesh_device, tp_axis=tp_axis, h_axis=h_axis, w_axis=w_axis)
+
+ z_channels = torch_model.config.latent_channels
+ patch_size = 2
+ vae_scale_factor = 8
+
+ tt_model = Flux2VaeDecoder(
+ out_channels=torch_model.config.out_channels,
+ block_out_channels=list(torch_model.config.block_out_channels),
+ layers_per_block=_LAYERS_PER_BLOCK,
+ z_channels=z_channels,
+ device=mesh_device,
+ parallel_config=vae_parallel_config,
+ ccl_manager=ccl_manager,
+ use_conv3d=False,
+ )
+ tt_model.load_torch_state_dict(torch_model.state_dict())
+
+ f = vae_scale_factor * patch_size
+ inp = torch.randn(batch_size, z_channels * patch_size**2, height // f, width // f)
+
+ torch_output = _torch_decode_reference(torch_model, inp) # [B, 3, H, W]
+
+ tt_inp = prep_data(vae_parallel_config, inp, mesh_device, tt_model=tt_model)
+ tt_out = tt_model.forward(tt_inp)
+ ttnn.synchronize_device(mesh_device)
+
+ tt_out_torch = tensor.to_torch(tt_out).permute(0, 3, 1, 2) # [B, 3, H, W]
+ assert_quality(torch_output, tt_out_torch, pcc=0.9978, relative_rmse=0.034)
diff --git a/code/models/tt_dit/tests/models/ideogram4/test_perf_ideogram4.py b/code/models/tt_dit/tests/models/ideogram4/test_perf_ideogram4.py
new file mode 100644
index 0000000000000000000000000000000000000000..1266a4b6a3a885997b5225133c919ab5d7026cf6
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ideogram4/test_perf_ideogram4.py
@@ -0,0 +1,83 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# =============================================================================
+# Ideogram 4.0 perf: end-to-end latency of the device-resident Ideogram4Pipeline.
+# Warms the program cache, then times encode / denoise / decode via the standard
+# tt_dit event -> BenchmarkProfiler path, emits BenchmarkData (in CI), and asserts
+# each section stays under a conservative ceiling so a gross regression fails the
+# test. Single-block device perf comes from the PCC test (test_transformer_ideogram4.py).
+# =============================================================================
+
+import os
+
+import pytest
+from loguru import logger
+
+from models.perf.benchmarking_utils import BenchmarkData, BenchmarkProfiler
+
+from ....pipelines.events import profiler_event_callback
+from ....pipelines.ideogram4.pipeline_ideogram4 import Ideogram4Pipeline
+from ....utils.test import line_params, ring_params
+
+# The pipeline needs the gated fp8 checkpoint (create_pipeline raises ValueError without it);
+# skip (don't error) when it isn't configured, matching the component tests.
+_NEEDS_WEIGHTS = pytest.mark.skipif(
+ not os.environ.get("IDEOGRAM4_WEIGHTS"), reason="IDEOGRAM4_WEIGHTS not set (gated fp8 checkpoint)"
+)
+
+PROMPT = "a watercolor painting of a red panda reading a book under a cherry tree, soft morning light"
+
+_LINE_PIPE = {**line_params, "l1_small_size": 32768, "trace_region_size": 60000000}
+_RING_PIPE = {**ring_params, "l1_small_size": 32768, "trace_region_size": 60000000}
+
+# Conservative upper-bound targets (seconds), program-cache warm, 512px V4_TURBO_12. Generous
+# placeholders that catch a gross (>~2x) regression without flaking; tighten once stable CI
+# baselines exist. Keyed by mesh shape.
+_PERF_TARGETS = {
+ (2, 4): {"encoder": 30.0, "denoising": 90.0, "vae": 30.0, "total": 130.0}, # BH LoudBox 2x4 SP4xTP2
+ (4, 8): {"encoder": 30.0, "denoising": 60.0, "vae": 30.0, "total": 100.0}, # BH Galaxy TP4xSP8
+}
+
+
+# Parallel config (tp/sp/num_links/topology) is DISCOVERED from the mesh shape via
+# Ideogram4Pipeline._PRESETS, so the perf test only picks a mesh shape + its device_params.
+@_NEEDS_WEIGHTS
+@pytest.mark.parametrize(
+ ("mesh_device", "device_params"),
+ [
+ pytest.param((2, 4), _LINE_PIPE, id="sp4tp2"), # BH loudbox 2x4: SP4 x TP2
+ pytest.param((4, 8), _RING_PIPE, id="bh_galaxy_sp8tp4"), # BH Galaxy: TP4 x SP8 (Ring)
+ ],
+ indirect=True,
+)
+@pytest.mark.parametrize(("height", "width", "preset"), [(512, 512, "V4_TURBO_12")], ids=["512px_turbo12"])
+def test_latency(*, mesh_device, is_ci_env, height, width, preset) -> None:
+ pipe = Ideogram4Pipeline.create_pipeline(mesh_device=mesh_device, height=height, width=width)
+
+ pipe(prompts=[PROMPT], preset=preset, seed=1) # warmup (compile + cache)
+ profiler = BenchmarkProfiler()
+ pipe(prompts=[PROMPT], preset=preset, seed=2, on_event=profiler_event_callback(profiler, 0)) # timed (warm)
+
+ steps = ("encoder", "denoising", "vae", "total")
+ measurements = {s: profiler.get_duration(s) for s in steps}
+ logger.info(f"LATENCY {height}px {preset}: " + " | ".join(f"{s} {measurements[s]:.2f}s" for s in steps))
+
+ targets = _PERF_TARGETS[tuple(mesh_device.shape)]
+ if is_ci_env:
+ benchmark_data = BenchmarkData()
+ for s in steps:
+ benchmark_data.add_measurement(
+ profiler=profiler, iteration=0, step_name=s, name=s, value=measurements[s], target=targets[s]
+ )
+ benchmark_data.save_partial_run_json(
+ profiler,
+ run_type="BH_LB" if tuple(mesh_device.shape) == (2, 4) else "BH_GLX",
+ ml_model_name="Ideogram4",
+ batch_size=1,
+ config_params={"width": width, "height": height, "preset": preset},
+ )
+
+ regressions = [f"{s}: {measurements[s]:.2f}s > {targets[s]:.1f}s" for s in steps if measurements[s] > targets[s]]
+ assert not regressions, "perf regression: " + "; ".join(regressions)
diff --git a/code/models/tt_dit/tests/models/ideogram4/test_pipeline_class_ideogram4.py b/code/models/tt_dit/tests/models/ideogram4/test_pipeline_class_ideogram4.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c071029b2640e62be4998e1938925e07389e17e
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ideogram4/test_pipeline_class_ideogram4.py
@@ -0,0 +1,147 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# =============================================================================
+# Drive the Ideogram4Pipeline class end-to-end on device (TP=4, all models
+# resident: encoder + cond/uncond transformers + VAE) with real weights, across
+# presets and resolutions. Saves each image and asserts it is non-degenerate
+# (correct shape, std > 1). A generation smoke test, not a PCC gate.
+# =============================================================================
+
+import os
+
+import numpy as np
+import pytest
+from loguru import logger
+
+from ....pipelines.ideogram4.pipeline_ideogram4 import Ideogram4Pipeline
+from ....utils.test import line_params, ring_params
+
+# The pipeline needs the gated fp8 checkpoint (create_pipeline raises ValueError without it);
+# skip (don't error) when it isn't configured, matching the component tests.
+_NEEDS_WEIGHTS = pytest.mark.skipif(
+ not os.environ.get("IDEOGRAM4_WEIGHTS"), reason="IDEOGRAM4_WEIGHTS not set (gated fp8 checkpoint)"
+)
+
+
+def _typography_prompt_json() -> str:
+ """Typography-heavy in-distribution JSON caption that showcases Ideogram 4's
+ best-in-class text rendering (crisp legible lettering + bbox layout + palette).
+
+ Ideogram 4 was trained exclusively on structured-JSON captions; quoting the exact
+ text strings and placing them with bbox is the reliable way to get them rendered.
+ """
+ import json
+
+ caption = {
+ "aspect_ratio": "1:1",
+ "high_level_description": (
+ "A bold retro travel poster with large, crisp typography. A condensed sans-serif "
+ 'headline reading "EXPLORE THE WILD" arcs across the top, a clean letter-spaced '
+ 'subtitle "NATIONAL PARKS · EST. 1872" sits beneath it, and a small banner at the '
+ 'bottom reads "ADVENTURE AWAITS". Set over a stylized sunrise mountain landscape '
+ "with pine silhouettes; flat vintage screen-print look, sharp legible lettering."
+ ),
+ "colour_palette": ["#F4A259", "#2E4600", "#1B3A4B", "#F2E8CF", "#BC4749"],
+ "compositional_deconstruction": {
+ "background": (
+ "Stylized sunrise sky in warm cream and orange bands, layered mountain ridges in "
+ "deep green and teal, a row of pine-tree silhouettes along the lower third, flat "
+ "screen-print texture with subtle grain."
+ ),
+ "elements": [
+ {
+ "type": "text",
+ "bbox": [70, 70, 290, 930],
+ "text": "EXPLORE THE WILD",
+ "desc": "Large bold condensed sans-serif headline in cream, all caps, gentle "
+ "upward arc, high contrast against the sky.",
+ },
+ {
+ "type": "text",
+ "bbox": [300, 180, 400, 820],
+ "text": "NATIONAL PARKS · EST. 1872",
+ "desc": "Smaller clean uppercase subtitle in warm orange, generously "
+ "letter-spaced, centered beneath the headline.",
+ },
+ {
+ "type": "text",
+ "bbox": [840, 250, 960, 750],
+ "text": "ADVENTURE AWAITS",
+ "desc": "Small bold uppercase banner text in cream on a deep-red rounded ribbon.",
+ },
+ {
+ "type": "obj",
+ "bbox": [410, 60, 880, 940],
+ "desc": "Layered mountain range at sunrise with a bold circular sun disc, deep "
+ "green and teal ridges, vintage poster shading.",
+ },
+ ],
+ },
+ }
+ return json.dumps(caption, ensure_ascii=False, separators=(",", ":"))
+
+
+# Typography showcase prompt (JSON is the known-good format; plain prose washes out
+# at >=1024px per the in-weights-moderation finding).
+PROMPT = _typography_prompt_json()
+
+
+# Trace region for the per-branch transformer-forward traces (cond + uncond).
+# 60MB headroom; bump if "trace region" OOM at higher resolution.
+# l1_small_size=32KB: only the VAE conv draws from L1_SMALL (proven sufficient in the VAE tests);
+# the denoiser matmuls use none, so 64KB over-reserved the shared L1 region and made the SP4xTP2
+# path's resident ring/all-gather buffers clash with the matmul circular buffers.
+_LINE_PIPE = {**line_params, "l1_small_size": 32768, "trace_region_size": 60000000}
+_RING_PIPE = {**ring_params, "l1_small_size": 32768, "trace_region_size": 60000000}
+
+
+# Pipeline mesh configs: the parallel config (tp/sp/num_links/topology) is DISCOVERED from the
+# mesh shape via Ideogram4Pipeline._PRESETS, so tests only pick a mesh shape + its device_params.
+_PIPE_CONFIGS = [
+ pytest.param((2, 4), _LINE_PIPE, id="sp4tp2"), # BH loudbox 2x4: SP4 x TP2
+ pytest.param((4, 8), _RING_PIPE, id="bh_galaxy_sp8tp4"), # BH Galaxy: TP4 x SP8 (Ring)
+]
+
+
+@_NEEDS_WEIGHTS
+@pytest.mark.parametrize(("mesh_device", "device_params"), _PIPE_CONFIGS, indirect=True)
+@pytest.mark.parametrize(
+ ("height", "width"), [(512, 512), (1024, 1024), (2048, 2048)], ids=["512px", "1024px", "2048px"]
+)
+@pytest.mark.parametrize("preset", ["V4_TURBO_12", "V4_QUALITY_48"])
+def test_pipeline_class(*, mesh_device, height, width, preset) -> None:
+ # Parallel config is discovered from the mesh shape (_PRESETS); no per-test tp/sp/links wiring.
+ pipe = Ideogram4Pipeline.create_pipeline(mesh_device=mesh_device, height=height, width=width)
+ img = pipe(prompts=[PROMPT], preset=preset, seed=1234, traced=False)[0]
+
+ out_dir = os.environ.get("IDEOGRAM4_OUT_DIR", "generated")
+ os.makedirs(out_dir, exist_ok=True)
+ out = os.path.join(out_dir, f"ideogram4_pipeline_class_{height}_{preset}.png")
+ img.save(out)
+ arr = np.asarray(img)
+ logger.info(f"pipeline class saved {out} shape={arr.shape} std={arr.std():.1f} preset={preset}")
+ assert arr.shape == (height, width, 3)
+ assert arr.std() > 1.0
+
+
+@_NEEDS_WEIGHTS
+@pytest.mark.parametrize(("mesh_device", "device_params"), _PIPE_CONFIGS, indirect=True)
+def test_2048_presets(*, mesh_device) -> None:
+ """UNTRACED 2048px generation across all 3 sampler presets, saved for manual review.
+
+ Builds the pipeline ONCE and loops the presets (avoids a per-preset model reload).
+ Untraced (traced=False) per the serving plan; 2048px doesn't benefit from tracing.
+ """
+ pipe = Ideogram4Pipeline.create_pipeline(mesh_device=mesh_device, height=2048, width=2048)
+ for preset in ("V4_TURBO_12", "V4_DEFAULT_20", "V4_QUALITY_48"):
+ img = pipe(prompts=[PROMPT], preset=preset, seed=1234, traced=False)[0]
+ out_dir = os.environ.get("IDEOGRAM4_OUT_DIR", "generated")
+ os.makedirs(out_dir, exist_ok=True)
+ out = os.path.join(out_dir, f"ideogram4_2048_{preset}.png")
+ img.save(out)
+ arr = np.asarray(img)
+ logger.info(f"2048px UNTRACED {preset}: saved {out} std={arr.std():.1f}")
+ assert arr.shape == (2048, 2048, 3)
+ assert arr.std() > 1.0
diff --git a/code/models/tt_dit/tests/models/ideogram4/test_pipeline_ideogram4.py b/code/models/tt_dit/tests/models/ideogram4/test_pipeline_ideogram4.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b00529782d8d3424eebd9266aa85414b9d82271
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ideogram4/test_pipeline_ideogram4.py
@@ -0,0 +1,121 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# =============================================================================
+# Ideogram 4.0 pipeline glue. The denoise loop (CFG blend + Euler) is covered by
+# the sampler + transformer tests and the text encode by the encoder test; this
+# verifies the pipeline-specific NEW device computation:
+# * the decode tail: per-channel latent denorm -> 2x2 unpatch -> VAE decode,
+# end-to-end on device vs a torch reference (latent_norm + diffusers
+# AutoencoderKL.decode);
+# * the asymmetric-CFG velocity blend v = gw*v_cond + (1-gw)*v_uncond.
+# =============================================================================
+
+import pytest
+import torch
+from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
+from loguru import logger
+
+import ttnn
+
+from ....models.vae.vae_ideogram4 import Ideogram4VAEDecoder
+from ....parallel.config import ParallelFactor, VAEParallelConfig
+from ....parallel.manager import CCLManager
+from ....pipelines.ideogram4.pipeline_ideogram4 import (
+ Ideogram4DecodeStage,
+ cfg_blend,
+ interleave_layer_taps,
+ unpatchify_latent,
+)
+from ....reference.ideogram4.constants import QWEN3_VL_ACTIVATION_LAYERS
+from ....reference.ideogram4.latent_norm import get_latent_norm
+from ....utils import tensor
+from ....utils.check import assert_quality
+from ....utils.tensor import bf16_tensor
+
+Z_CHANNELS = 32
+PATCH = 2
+
+
+def test_llm_tap_interleave_feature_major() -> None:
+ """Host-only: lock the 13-layer Qwen tap assembly to FEATURE-MAJOR (out[..., f*L + l] =
+ taps[l][..., f]). This is the vendor order the real llm_cond_proj weights expect; a layer-major
+ flip would silently scramble conditioning. Marks each tap l with the constant l so the index
+ mapping is checkable without weights/device."""
+ layers = len(QWEN3_VL_ACTIVATION_LAYERS) # 13
+ d, n = 4, 3 # tiny feature dim + seq for a fast host check
+ taps = [torch.full((1, n, d), float(l)) for l in range(layers)] # tap l is all-l
+ out = interleave_layer_taps(taps)
+
+ assert out.shape == (1, n, d * layers)
+ # feature-major: the value at flat index f*layers + l must be tap l's marker (l)
+ for f in range(d):
+ for l in range(layers):
+ assert out[0, 0, f * layers + l].item() == float(l), f"interleave not feature-major at f={f}, l={l}"
+
+
+def _build_akl(torch_dtype):
+ return (
+ AutoencoderKL(
+ in_channels=3,
+ out_channels=3,
+ latent_channels=Z_CHANNELS,
+ down_block_types=("DownEncoderBlock2D",) * 4,
+ up_block_types=("UpDecoderBlock2D",) * 4,
+ block_out_channels=(128, 256, 512, 512),
+ layers_per_block=2,
+ norm_num_groups=32,
+ )
+ .to(torch_dtype)
+ .eval()
+ )
+
+
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
+@pytest.mark.parametrize("device_params", [{"l1_small_size": 32768}], indirect=True)
+@pytest.mark.parametrize(("grid_h", "grid_w"), [(16, 16)], ids=["256px"])
+def test_pipeline_decode(*, mesh_device: ttnn.MeshDevice, grid_h: int, grid_w: int) -> None:
+ """Decode tail end-to-end (latent denorm + 2x2 unpatch + VAE) vs torch reference."""
+ torch.manual_seed(0)
+ torch_dtype = torch.bfloat16
+ tokens = grid_h * grid_w
+
+ akl = _build_akl(torch_dtype)
+ parallel_config = VAEParallelConfig(tensor_parallel=ParallelFactor(factor=1, mesh_axis=1))
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+ vae = Ideogram4VAEDecoder.from_torch(
+ akl, mesh_device=mesh_device, parallel_config=parallel_config, ccl_manager=ccl_manager
+ )
+ stage = Ideogram4DecodeStage(vae, mesh_device=mesh_device, patch=PATCH)
+
+ z = torch.randn(1, tokens, PATCH * PATCH * Z_CHANNELS, dtype=torch.float32) # patchified latent
+
+ # torch reference: denorm -> unpatch -> diffusers decode
+ shift, scale = get_latent_norm()
+ z_dn = (z * scale + shift).to(torch_dtype)
+ z_nchw = unpatchify_latent(z_dn, grid_h=grid_h, grid_w=grid_w, patch=PATCH)
+ with torch.no_grad():
+ ref = akl.decode(z_nchw).sample
+
+ out = stage.decode(bf16_tensor(z, device=mesh_device), grid_h=grid_h, grid_w=grid_w)
+
+ logger.info(f"ideogram4 pipeline decode: {tokens} tokens -> image {grid_h*PATCH*8}x{grid_w*PATCH*8}")
+ assert_quality(ref.float(), out.float(), pcc=0.99)
+
+
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
+@pytest.mark.parametrize("device_params", [{}], indirect=True)
+def test_pipeline_cfg_blend(*, mesh_device: ttnn.MeshDevice) -> None:
+ """Asymmetric CFG: v = gw*v_cond + (1-gw)*v_uncond on device vs torch."""
+ torch.manual_seed(0)
+ gw = 7.0
+ v_cond = torch.randn(1, 4096, 128, dtype=torch.float32)
+ v_uncond = torch.randn(1, 4096, 128, dtype=torch.float32)
+ ref = gw * v_cond + (1.0 - gw) * v_uncond
+
+ out = cfg_blend(bf16_tensor(v_cond, device=mesh_device), bf16_tensor(v_uncond, device=mesh_device), gw)
+ out_torch = tensor.to_torch(out, mesh_axes=[None, None, None])
+
+ logger.info("ideogram4 pipeline CFG blend")
+ assert_quality(ref, out_torch, pcc=0.999)
diff --git a/code/models/tt_dit/tests/models/ideogram4/test_sampler_ideogram4.py b/code/models/tt_dit/tests/models/ideogram4/test_sampler_ideogram4.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b32f0a216a67b8724aa6df0a28e3935bad44dd2
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ideogram4/test_sampler_ideogram4.py
@@ -0,0 +1,80 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# =============================================================================
+# Ideogram 4.0 Euler flow-matching sampler. Two checks:
+# 1. host: the preset table + logit-normal schedule / guidance weights are well-formed
+# (num_steps/guidance/mu/std match spec; times in [0,1], monotonic; gw per schedule).
+# 2. device: a full Euler step loop z <- z + v*(s-t) matches a torch reference.
+# =============================================================================
+
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+
+from ....pipelines.ideogram4.sampler import PRESETS, Ideogram4Sampler
+from ....utils import tensor
+from ....utils.check import assert_quality
+from ....utils.tensor import bf16_tensor
+
+# Expected preset spec (num_steps, guidance schedule, logit-normal mean/std) — the
+# official Ideogram 4.0 values, kept here as an independent regression against sampler.PRESETS.
+_EXPECTED = {
+ "V4_QUALITY_48": dict(num_steps=48, gw=(3.0,) * 3 + (7.0,) * 45, mu=0.0, std=1.5),
+ "V4_DEFAULT_20": dict(num_steps=20, gw=(3.0,) * 2 + (7.0,) * 18, mu=0.0, std=1.75),
+ "V4_TURBO_12": dict(num_steps=12, gw=(3.0,) * 1 + (7.0,) * 11, mu=0.5, std=1.75),
+}
+
+
+@pytest.mark.parametrize("preset", list(PRESETS))
+@pytest.mark.parametrize("resolution", [(1024, 1024), (2048, 768)], ids=["1024sq", "2048x768"])
+def test_sampler_schedule(*, preset: str, resolution: tuple[int, int]) -> None:
+ """Host-only: preset table matches spec and the logit-normal schedule is well-formed."""
+ h, w = resolution
+ params = PRESETS[preset]
+ exp = _EXPECTED[preset]
+
+ assert params.num_steps == exp["num_steps"]
+ assert tuple(float(g) for g in params.guidance_schedule) == exp["gw"]
+ assert (params.mu, params.std) == (exp["mu"], exp["std"])
+
+ samp = Ideogram4Sampler.from_preset(preset, height=h, width=w)
+ times = [samp.times_for_step(i) for i in range(params.num_steps)]
+
+ for i, (t_val, s_val) in enumerate(times):
+ assert 0.0 <= t_val <= 1.0, f"step {i}: t {t_val} out of [0,1]"
+ assert 0.0 <= s_val <= 1.0, f"step {i}: s {s_val} out of [0,1]"
+ assert samp.guidance_weight(i) == float(params.guidance_schedule[i])
+
+ # the schedule must sweep monotonically across the denoising loop
+ t_seq = [t for t, _ in times]
+ assert t_seq == sorted(t_seq) or t_seq == sorted(t_seq, reverse=True), f"non-monotonic schedule: {t_seq}"
+
+ logger.info(f"sampler schedule OK: {preset} @ {resolution}, {params.num_steps} steps")
+
+
+@pytest.mark.parametrize("mesh_device", [(1, 1)], indirect=True)
+@pytest.mark.parametrize("device_params", [{}], indirect=True)
+@pytest.mark.parametrize("preset", ["V4_TURBO_12"])
+def test_sampler_euler_step_device(*, mesh_device: ttnn.MeshDevice, preset: str) -> None:
+ """Device: the full Euler step loop matches torch z <- z + v*(s-t)."""
+ torch.manual_seed(0)
+ h = w = 1024
+ samp = Ideogram4Sampler.from_preset(preset, height=h, width=w)
+
+ batch, tokens, ch = 1, 4096, 128
+ z = torch.randn(batch, tokens, ch, dtype=torch.float32)
+ tt_z = bf16_tensor(z, device=mesh_device)
+
+ for i in reversed(range(samp.num_steps)):
+ v = torch.randn(batch, tokens, ch, dtype=torch.float32) # stand-in velocity
+ t_val, s_val = samp.times_for_step(i)
+ z = z + v * (s_val - t_val) # torch reference
+ tt_z = samp.step(tt_z, bf16_tensor(v, device=mesh_device), i)
+
+ tt_z_torch = tensor.to_torch(tt_z, mesh_axes=[None, None, None])
+ logger.info(f"sampler device step OK: {preset}, {samp.num_steps} steps")
+ assert_quality(z, tt_z_torch, pcc=0.999)
diff --git a/code/models/tt_dit/tests/models/ideogram4/test_transformer_ideogram4.py b/code/models/tt_dit/tests/models/ideogram4/test_transformer_ideogram4.py
new file mode 100644
index 0000000000000000000000000000000000000000..41a0884c32a358cd1a9815dbb112f309da4f86a2
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ideogram4/test_transformer_ideogram4.py
@@ -0,0 +1,522 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# =============================================================================
+# Ideogram 4.0 single-stream transformer PCC vs the OFFICIAL reference. Two tests:
+# * test_transformer_block: one Ideogram4TransformerBlock (random init) on real
+# Ideogram dims across the TP/SP/segment sweep.
+# * test_transformer_model: the full transformer (top + blocks + bottom) under
+# parallelism, random or real weights.
+# Both load the reference state_dict into the tt module and assert PCC >= 0.99.
+#
+# Block coverage (2x4 BH mesh, FABRIC_1D, Linear): {B=1,2} x {1,2 segments} x
+# {1088, 4224 seq} x {tp1sp1, tp2, tp4(pad 20), sp2, sp2tp2, sp2tp4}.
+#
+# 1. rotate-half MRoPE (cos/sin = cat(freqs, freqs)). Precomputed on host here
+# from the reference Ideogram4MRoPE so the convention is identical.
+# 2. block-diagonal segment-id mask -> additive bias (0 / -inf) fed to SDPA;
+# with SP it is sharded on the query-row dim (K/V are all-gathered).
+# 3. tanh-gated 4-branch AdaLN (no shift terms).
+# 4. double-RMSNorm sandwich residual (norm2 on the attn/ff *output*).
+# 5. head_dim=256 / 18 heads — TP pads heads up to a multiple of tp_factor
+# (18 -> 20 for tp=4); SP sharding uses ring attention (or K/V gather + mask).
+# =============================================================================
+
+import math
+import os
+
+import pytest
+import torch
+from loguru import logger
+from safetensors.torch import load_file
+
+import ttnn
+
+from ....models.transformers.transformer_ideogram4 import (
+ Ideogram4Transformer,
+ Ideogram4TransformerBlock,
+ rope_halfsplit_to_interleaved,
+)
+from ....parallel.config import DiTParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....reference.ideogram4 import modeling_ideogram4
+from ....reference.ideogram4.constants import IMAGE_POSITION_OFFSET, LLM_TOKEN_INDICATOR, OUTPUT_IMAGE_INDICATOR
+from ....reference.ideogram4.dequant import dequant_fp8_state_dict
+from ....utils import tensor
+from ....utils.check import assert_quality
+from ....utils.padding import PaddingConfig
+from ....utils.tensor import bf16_tensor
+from ....utils.test import line_params, ring_params
+
+# Real shipped fp8 checkpoint (ideogram-ai/ideogram-4-fp8), dequantized to bf16. Override via
+# env for CI. Used by the weights="real" parametrizations (weights="random" uses fresh init).
+FP8 = os.environ.get("IDEOGRAM4_WEIGHTS")
+# Real-weight cases need the gated fp8 checkpoint; skip (don't error) when it isn't configured.
+_NEEDS_WEIGHTS = pytest.mark.skipif(not FP8, reason="IDEOGRAM4_WEIGHTS not set (gated fp8 checkpoint)")
+
+
+def _real_transformer_sd(num_layers: int) -> dict:
+ """Dequantized real transformer weights, filtered to the first num_layers layers so the
+ (strict) tt loader accepts a truncated-depth model."""
+ sd = dequant_fp8_state_dict(load_file(f"{FP8}/transformer/diffusion_pytorch_model.safetensors"))
+ return {k: v for k, v in sd.items() if not (k.startswith("layers.") and int(k.split(".")[1]) >= num_layers)}
+
+
+# Ring SDPA (sequence parallel) only needs each per-device shard tile-aligned, i.e. the
+# padded sequence divisible by tile_size * sp_factor. Must match pipeline._sp_padded_len.
+_SP_TILE = 32
+
+
+def _sp_padded_len(seq_len: int, sp_factor: int) -> int:
+ if sp_factor == 1:
+ return seq_len
+ divisor = _SP_TILE * sp_factor
+ return math.ceil(seq_len / divisor) * divisor
+
+
+# Real Ideogram 4.0 denoiser dims (confirmed against reference Ideogram4Config).
+EMB_DIM = 4608
+NUM_HEADS = 18
+HEAD_DIM = EMB_DIM // NUM_HEADS # 256
+INTERMEDIATE_SIZE = 12288
+ADALN_DIM = 512
+ROPE_THETA = 5_000_000
+MROPE_SECTION = (24, 20, 20)
+NORM_EPS = 1e-5
+
+
+def _build_inputs(batch_size: int, text_len: int, image_len: int, torch_dtype, num_segments: int = 1):
+ """Construct a realistic single-stream batch: [text tokens | image tokens].
+
+ Returns torch tensors mirroring the reference block's forward signature plus
+ the derived cos/sin (B, L, head_dim) and a (B, 1, L, L) additive attn mask.
+
+ num_segments > 1 packs that many independent samples into the sequence (contiguous,
+ roughly equal spans) so the block-diagonal segment mask blocks cross-segment
+ attention — exercising the masked SDPA path.
+ """
+ seq_len = text_len + image_len
+
+ # hidden states (already-fused h = x + llm_features + indicator embed upstream).
+ x = torch.randn(batch_size, seq_len, EMB_DIM, dtype=torch_dtype)
+
+ # adaln_input: reference passes F.silu(adaln_proj(t_cond)); per-sample => (B,1,adaln_dim).
+ adaln_input = torch.randn(batch_size, 1, ADALN_DIM, dtype=torch_dtype)
+
+ # segment ids: contiguous spans => block-diagonal attention. num_segments==1 is the
+ # all-same-segment (full attention) case.
+ segment_ids = torch.zeros(batch_size, seq_len, dtype=torch.long)
+ if num_segments > 1:
+ bounds = torch.linspace(0, seq_len, num_segments + 1).round().long()
+ for s in range(num_segments):
+ segment_ids[:, bounds[s] : bounds[s + 1]] = s
+
+ # position_ids (B, L, 3) = (t, h, w). Text tokens: 1D positions on all 3 axes.
+ # Image tokens: a (h, w) grid offset so they never collide with text positions.
+ position_ids = torch.zeros(batch_size, seq_len, 3, dtype=torch.long)
+ # text: linear positions 0..text_len-1 on every axis.
+ text_pos = torch.arange(text_len)
+ position_ids[:, :text_len, 0] = text_pos
+ position_ids[:, :text_len, 1] = text_pos
+ position_ids[:, :text_len, 2] = text_pos
+ # image: square-ish grid.
+ grid_h = int(round(image_len**0.5))
+ while image_len % grid_h != 0:
+ grid_h -= 1
+ grid_w = image_len // grid_h
+ hh = torch.arange(grid_h).repeat_interleave(grid_w)
+ ww = torch.arange(grid_w).repeat(grid_h)
+ position_ids[:, text_len:, 0] = IMAGE_POSITION_OFFSET
+ position_ids[:, text_len:, 1] = IMAGE_POSITION_OFFSET + hh
+ position_ids[:, text_len:, 2] = IMAGE_POSITION_OFFSET + ww
+
+ # cos/sin from the reference MRoPE (ground-truth convention).
+ rope = modeling_ideogram4.Ideogram4MRoPE(head_dim=HEAD_DIM, base=ROPE_THETA, mrope_section=MROPE_SECTION)
+ cos, sin = rope(position_ids) # (B, L, head_dim), float32
+ cos = cos.to(torch_dtype)
+ sin = sin.to(torch_dtype)
+
+ # additive attention bias from segment ids: (B, 1, L, L), 0 attend / -inf block.
+ same_seg = segment_ids.unsqueeze(2) == segment_ids.unsqueeze(1) # (B, L, L)
+ attn_bias = torch.zeros(batch_size, 1, seq_len, seq_len, dtype=torch.float32)
+ attn_bias.masked_fill_(~same_seg.unsqueeze(1), float("-inf"))
+
+ return x, adaln_input, segment_ids, cos, sin, attn_bias
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "submesh_shape", "sp_axis", "tp_axis", "num_links", "device_params", "topology"),
+ [
+ # factor == submesh axis size. TP shards heads (padded to a multiple of tp);
+ # SP shards the sequence (ring attention). Full mesh is the 2x4 BH loudbox.
+ # factor == submesh axis size; sp on axis 0 (size up to 2), tp on axis 1 (size up to 4).
+ pytest.param(
+ (2, 4), (1, 1), 0, 1, 1, line_params, ttnn.Topology.Linear, id="tp1sp1"
+ ), # regression: single device
+ pytest.param(
+ (2, 4), (1, 2), 0, 1, 1, line_params, ttnn.Topology.Linear, id="tp2"
+ ), # 18 heads % 2 == 0, no padding
+ pytest.param((2, 4), (1, 4), 0, 1, 1, line_params, ttnn.Topology.Linear, id="tp4_pad20"), # 18 -> 20 heads
+ pytest.param(
+ (2, 4), (2, 1), 0, 1, 1, line_params, ttnn.Topology.Linear, id="sp2"
+ ), # sequence parallel only (TP=1)
+ pytest.param((2, 4), (2, 2), 0, 1, 1, line_params, ttnn.Topology.Linear, id="sp2tp2"),
+ pytest.param(
+ (2, 4), (2, 4), 0, 1, 1, line_params, ttnn.Topology.Linear, id="sp2tp4_pad20"
+ ), # SP=2, TP=4 (pad 20)
+ pytest.param(
+ (2, 4), (2, 4), 1, 0, 2, line_params, ttnn.Topology.Linear, id="sp4tp2"
+ ), # SP=4 (axis 1), TP=2 (axis 0) — full 2x4 loudbox; num_links=2
+ # BH Galaxy 4x8, 2D torus Ring: SP=8 (axis 1), TP=4 (axis 0), 2 links/neighbor. 18 heads -> pad 20.
+ pytest.param((4, 8), (4, 8), 1, 0, 2, ring_params, ttnn.Topology.Ring, id="bh_galaxy_sp8tp4"),
+ ],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ ("batch_size", "text_len", "image_len"),
+ [
+ pytest.param(1, 64, 1024, id="b1_text64_img1024"),
+ pytest.param(1, 128, 4096, id="b1_text128_img4096"), # larger sequence (4224)
+ ],
+)
+@pytest.mark.parametrize("num_segments", [1, 2], ids=["1seg", "2seg"])
+def test_transformer_block(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ submesh_shape: tuple[int, int],
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+ batch_size: int,
+ text_len: int,
+ image_len: int,
+ num_segments: int,
+) -> None:
+ torch.manual_seed(0)
+ torch_dtype = torch.bfloat16
+
+ seq_len = text_len + image_len
+
+ submesh_device = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ sp_factor = tuple(submesh_device.shape)[sp_axis]
+ tp_factor = tuple(submesh_device.shape)[tp_axis]
+
+ # ---- reference (ground truth), random init ----
+ torch_block = modeling_ideogram4.Ideogram4TransformerBlock(
+ hidden_size=EMB_DIM,
+ intermediate_size=INTERMEDIATE_SIZE,
+ num_heads=NUM_HEADS,
+ norm_eps=NORM_EPS,
+ adanln_dim=ADALN_DIM,
+ ).to(dtype=torch_dtype)
+ torch_block.eval()
+
+ x, adaln_input, segment_ids, cos, sin, attn_bias = _build_inputs(
+ batch_size, text_len, image_len, torch_dtype, num_segments
+ )
+
+ with torch.no_grad():
+ torch_out = torch_block(
+ x,
+ segment_ids=segment_ids,
+ cos=cos,
+ sin=sin,
+ adaln_input=adaln_input,
+ )
+
+ # ---- tt block ----
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=1, mesh_axis=0),
+ tensor_parallel=ParallelFactor(factor=tp_factor, mesh_axis=tp_axis),
+ sequence_parallel=ParallelFactor(factor=sp_factor, mesh_axis=sp_axis),
+ )
+ ccl_manager = CCLManager(submesh_device, num_links=num_links, topology=topology)
+
+ # Head padding for TP divisibility (18 heads -> next multiple of tp_factor).
+ padding_config = (
+ PaddingConfig.from_tensor_parallel_factor(NUM_HEADS, HEAD_DIM, tp_factor)
+ if NUM_HEADS % tp_factor != 0
+ else None
+ )
+
+ tt_block = Ideogram4TransformerBlock(
+ hidden_size=EMB_DIM,
+ intermediate_size=INTERMEDIATE_SIZE,
+ num_heads=NUM_HEADS,
+ norm_eps=NORM_EPS,
+ adaln_dim=ADALN_DIM,
+ mesh_device=submesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ padding_config=padding_config,
+ )
+ tt_block.load_torch_state_dict(torch_block.state_dict())
+
+ # Sequence-parallel sharding: pad the sequence to k_chunk*sp, shard x and the
+ # cos/sin rope tables on the sequence dim across the SP axis. The hidden dim stays
+ # replicated (TP only shards inside the matmuls), so a single-axis shard suffices.
+ padded_len = _sp_padded_len(seq_len, sp_factor)
+ cos4 = cos.unsqueeze(1) # (B, 1, L, head_dim) to broadcast over heads
+ sin4 = sin.unsqueeze(1)
+ # tt block uses interleaved RoPE (rotary_embedding_llama); permute cos/sin to match.
+ # The torch reference above keeps the original half-split tables (unchanged).
+ cos4, sin4 = rope_halfsplit_to_interleaved(cos4, sin4, HEAD_DIM)
+ # The residual stream is FRACTURED on hidden (TP axis) and sharded on sequence (SP).
+ # Shard x on both axes when tp>1: SP on the sequence dim, TP on the hidden dim.
+ if sp_factor > 1:
+ x = torch.nn.functional.pad(x, (0, 0, 0, padded_len - seq_len))
+ cos4 = torch.nn.functional.pad(cos4, (0, 0, 0, padded_len - seq_len))
+ sin4 = torch.nn.functional.pad(sin4, (0, 0, 0, padded_len - seq_len))
+ if tp_factor > 1:
+ from ....utils.tensor import bf16_tensor_2dshard
+
+ tt_x = bf16_tensor_2dshard(x, device=submesh_device, shard_mapping={sp_axis: 1, tp_axis: 2})
+ else:
+ tt_x = bf16_tensor(x, device=submesh_device, mesh_axis=sp_axis, shard_dim=1)
+ tt_cos = bf16_tensor(cos4, device=submesh_device, mesh_axis=sp_axis, shard_dim=2)
+ tt_sin = bf16_tensor(sin4, device=submesh_device, mesh_axis=sp_axis, shard_dim=2)
+ else:
+ if tp_factor > 1:
+ tt_x = bf16_tensor(x, device=submesh_device, mesh_axis=tp_axis, shard_dim=2)
+ else:
+ tt_x = bf16_tensor(x, device=submesh_device)
+ tt_cos = bf16_tensor(cos4, device=submesh_device)
+ tt_sin = bf16_tensor(sin4, device=submesh_device)
+ tt_adaln = bf16_tensor(adaln_input, device=submesh_device)
+
+ # Block-diagonal segment mask (additive 0 / -inf). num_segments==1 => full attention.
+ if num_segments == 1:
+ tt_attn_mask = None
+ elif sp_factor == 1:
+ # Replicated [B, 1, L, L] mask fed to plain SDPA.
+ assert torch.isneginf(attn_bias).any(), "multi-segment input should produce a non-trivial mask"
+ tt_attn_mask = bf16_tensor(attn_bias, device=submesh_device)
+ else:
+ # SP + mask: the block all-gathers K/V to the full (padded) sequence and runs
+ # masked SDPA with Q kept sequence-sharded, so the mask is [B, 1, padded_L, padded_L]
+ # sharded on the query-row dim. Pad segment ids with a sentinel id so real queries
+ # never attend to padded keys (and vice-versa).
+ seg_padded = torch.full((batch_size, padded_len), num_segments, dtype=torch.long)
+ seg_padded[:, :seq_len] = segment_ids
+ same_seg = seg_padded.unsqueeze(2) == seg_padded.unsqueeze(1) # (B, padded_L, padded_L)
+ bias = torch.zeros(batch_size, 1, padded_len, padded_len, dtype=torch.float32)
+ bias.masked_fill_(~same_seg.unsqueeze(1), float("-inf"))
+ tt_attn_mask = bf16_tensor(bias, device=submesh_device, mesh_axis=sp_axis, shard_dim=2)
+
+ tt_out = tt_block(
+ tt_x,
+ cos=tt_cos,
+ sin=tt_sin,
+ adaln_input=tt_adaln,
+ attn_mask=tt_attn_mask,
+ spatial_sequence_length=seq_len,
+ )
+
+ # Output is FRACTURED on hidden (TP), sharded on SP. mesh_axes maps tensor dim ->
+ # mesh axis: sequence (dim 1) on sp_axis, hidden (dim 2) on tp_axis.
+ out_mesh_axes = [None, sp_axis, tp_axis if tp_factor > 1 else None]
+ tt_out_torch = tensor.to_torch(tt_out, mesh_axes=out_mesh_axes)
+ tt_out_torch = tt_out_torch[:, :seq_len, :]
+
+ logger.info(
+ f"ideogram4 block: B={batch_size} seq={seq_len} tp={tp_factor} sp={sp_factor} "
+ f"padded_heads={tt_block.padded_heads} padded_len={padded_len}"
+ )
+ assert_quality(torch_out, tt_out_torch, pcc=0.99, relative_rmse=0.05)
+
+
+# =============================================================================
+# Full transformer (embeddings + input_proj + block stack + adaLN + final layer,
+# i.e. the top and bottom outside the decoder blocks) vs the OFFICIAL reference
+# Ideogram4Transformer, under TP / SP. 2 layers is enough to prove the wiring;
+# weights="random" is the parallelism sweep, weights="real" the shipped-weight
+# fidelity check. Asserts PCC >= 0.99 on the image-position velocity prediction.
+# =============================================================================
+
+
+def _build_model_inputs(config, batch_size, llm_len, image_len):
+ """[llm tokens | image tokens] packed single-stream batch for the full model."""
+ seq_len = llm_len + image_len
+ llm_features = torch.randn(batch_size, seq_len, config.llm_features_dim)
+ x = torch.randn(batch_size, seq_len, config.in_channels)
+ t = torch.rand(batch_size) # flow-matching time in [0, 1]
+ segment_ids = torch.zeros(batch_size, seq_len, dtype=torch.long) # single segment
+
+ indicator = torch.full((batch_size, seq_len), OUTPUT_IMAGE_INDICATOR, dtype=torch.long)
+ indicator[:, :llm_len] = LLM_TOKEN_INDICATOR
+
+ # position_ids (B, L, 3) = (t, h, w): llm tokens linear on all axes, image on an offset grid.
+ position_ids = torch.zeros(batch_size, seq_len, 3, dtype=torch.long)
+ lpos = torch.arange(llm_len)
+ position_ids[:, :llm_len, 0] = lpos
+ position_ids[:, :llm_len, 1] = lpos
+ position_ids[:, :llm_len, 2] = lpos
+ grid_h = int(round(image_len**0.5))
+ while image_len % grid_h != 0:
+ grid_h -= 1
+ grid_w = image_len // grid_h
+ hh = torch.arange(grid_h).repeat_interleave(grid_w)
+ ww = torch.arange(grid_w).repeat(grid_h)
+ position_ids[:, llm_len:, 0] = IMAGE_POSITION_OFFSET
+ position_ids[:, llm_len:, 1] = IMAGE_POSITION_OFFSET + hh
+ position_ids[:, llm_len:, 2] = IMAGE_POSITION_OFFSET + ww
+
+ return llm_features, x, t, position_ids, segment_ids, indicator
+
+
+def _seq_shard(t: torch.Tensor, seq_dim: int, padded_len: int, sp_factor: int, sp_axis: int, device, *, dtype):
+ """Pad a tensor on its sequence dim and shard it across the SP axis (or replicate)."""
+ if sp_factor > 1:
+ pad = padded_len - t.shape[seq_dim]
+ if pad:
+ spec = [0, 0] * (t.ndim - seq_dim - 1) + [0, pad]
+ t = torch.nn.functional.pad(t, spec)
+ mesh_axes = [None] * t.ndim
+ mesh_axes[seq_dim] = sp_axis
+ layout = ttnn.ROW_MAJOR_LAYOUT if dtype == ttnn.uint32 else ttnn.TILE_LAYOUT
+ return tensor.from_torch(t, device=device, dtype=dtype, layout=layout, mesh_axes=mesh_axes)
+ if dtype == ttnn.uint32:
+ return tensor.from_torch(t, device=device, dtype=dtype, layout=ttnn.ROW_MAJOR_LAYOUT)
+ return bf16_tensor(t, device=device)
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "submesh_shape", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "weights"),
+ [
+ # random-weight parallelism sweep (proves the full-model wiring under TP / SP).
+ pytest.param((1, 1), (1, 1), 0, 1, 1, {}, ttnn.Topology.Linear, "random", id="tp1sp1"),
+ pytest.param((2, 4), (1, 2), 0, 1, 1, line_params, ttnn.Topology.Linear, "random", id="tp2"),
+ pytest.param((2, 4), (1, 4), 0, 1, 1, line_params, ttnn.Topology.Linear, "random", id="tp4_pad20"),
+ pytest.param((2, 4), (2, 1), 0, 1, 1, line_params, ttnn.Topology.Linear, "random", id="sp2"),
+ pytest.param((2, 4), (2, 2), 0, 1, 1, line_params, ttnn.Topology.Linear, "random", id="sp2tp2"),
+ pytest.param((2, 4), (2, 4), 0, 1, 1, line_params, ttnn.Topology.Linear, "random", id="sp2tp4_pad20"),
+ # BH Galaxy 4x8, 2D torus Ring: SP=8 (axis 1), TP=4 (axis 0), 2 links/neighbor.
+ pytest.param((4, 8), (4, 8), 1, 0, 2, ring_params, ttnn.Topology.Ring, "random", id="bh_galaxy_sp8tp4"),
+ # real-weight fidelity: shipped checkpoint on single device and under production TP.
+ pytest.param((1, 1), (1, 1), 0, 1, 1, {}, ttnn.Topology.Linear, "real", id="tp1sp1_real", marks=_NEEDS_WEIGHTS),
+ pytest.param(
+ (2, 4), (1, 4), 0, 1, 1, line_params, ttnn.Topology.Linear, "real", id="tp4_real", marks=_NEEDS_WEIGHTS
+ ),
+ ],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize("num_layers", [2], ids=["layers2"])
+@pytest.mark.parametrize(
+ ("batch_size", "llm_len", "image_len"),
+ [pytest.param(1, 64, 256, id="llm64_img256")],
+)
+def test_transformer_model(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ submesh_shape: tuple[int, int],
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+ weights: str,
+ num_layers: int,
+ batch_size: int,
+ llm_len: int,
+ image_len: int,
+) -> None:
+ torch.manual_seed(0)
+ torch_dtype = torch.bfloat16
+ seq_len = llm_len + image_len
+
+ submesh_device = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ sp_factor = tuple(submesh_device.shape)[sp_axis]
+ tp_factor = tuple(submesh_device.shape)[tp_axis]
+
+ config = modeling_ideogram4.Ideogram4Config(num_layers=num_layers)
+ torch_model = modeling_ideogram4.Ideogram4Transformer(config).to(dtype=torch_dtype)
+ if weights == "real":
+ state_dict = _real_transformer_sd(num_layers)
+ _load = torch_model.load_state_dict(state_dict, strict=False)
+ # strict=False only to tolerate non-persistent buffers (e.g. rotary_emb) — those are NOT in
+ # state_dict, so a correct real load has BOTH missing and unexpected empty. Enforce both, so a
+ # broken checkpoint key map fails loudly rather than silently dropping weights to random init.
+ assert not _load.unexpected_keys, f"unexpected checkpoint keys: {_load.unexpected_keys[:5]}"
+ assert not _load.missing_keys, f"missing checkpoint keys: {_load.missing_keys[:5]}"
+ else:
+ state_dict = torch_model.state_dict() # random init
+ torch_model.eval()
+
+ llm_features, x, t, position_ids, segment_ids, indicator = _build_model_inputs(
+ config, batch_size, llm_len, image_len
+ )
+ llm_features = llm_features.to(torch_dtype)
+ x = x.to(torch_dtype)
+
+ with torch.no_grad():
+ torch_out = torch_model(
+ llm_features=llm_features,
+ x=x,
+ t=t,
+ position_ids=position_ids,
+ segment_ids=segment_ids,
+ indicator=indicator,
+ )
+
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=1, mesh_axis=0),
+ tensor_parallel=ParallelFactor(factor=tp_factor, mesh_axis=tp_axis),
+ sequence_parallel=ParallelFactor(factor=sp_factor, mesh_axis=sp_axis),
+ )
+ ccl_manager = CCLManager(submesh_device, num_links=num_links, topology=topology)
+ padding_config = (
+ PaddingConfig.from_tensor_parallel_factor(config.num_heads, config.emb_dim // config.num_heads, tp_factor)
+ if config.num_heads % tp_factor != 0
+ else None
+ )
+
+ tt_model = Ideogram4Transformer(
+ emb_dim=config.emb_dim,
+ num_layers=config.num_layers,
+ num_heads=config.num_heads,
+ intermediate_size=config.intermediate_size,
+ adaln_dim=config.adanln_dim,
+ in_channels=config.in_channels,
+ llm_features_dim=config.llm_features_dim,
+ norm_eps=config.norm_eps,
+ mesh_device=submesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ padding_config=padding_config,
+ )
+ tt_model.load_torch_state_dict(state_dict)
+
+ cos, sin = torch_model.rotary_emb(position_ids)
+ t_sin = Ideogram4Transformer.sinusoidal_embedding(t, config.emb_dim)
+ llm_token_mask = (indicator == LLM_TOKEN_INDICATOR).to(torch.float32).unsqueeze(-1)
+ output_image_mask = (indicator == OUTPUT_IMAGE_INDICATOR).to(torch.float32).unsqueeze(-1)
+ image_idx = (indicator == OUTPUT_IMAGE_INDICATOR).to(torch.int32)
+ padded_len = _sp_padded_len(seq_len, sp_factor)
+
+ dev = submesh_device
+ _cos_il, _sin_il = rope_halfsplit_to_interleaved(
+ cos.unsqueeze(1), sin.unsqueeze(1), config.emb_dim // config.num_heads
+ )
+ tt_out = tt_model(
+ x=_seq_shard(x, 1, padded_len, sp_factor, sp_axis, dev, dtype=ttnn.bfloat16),
+ llm_features=_seq_shard(llm_features, 1, padded_len, sp_factor, sp_axis, dev, dtype=ttnn.bfloat16),
+ t_sin=bf16_tensor(t_sin.unsqueeze(1), device=dev),
+ cos=_seq_shard(_cos_il, 2, padded_len, sp_factor, sp_axis, dev, dtype=ttnn.bfloat16),
+ sin=_seq_shard(_sin_il, 2, padded_len, sp_factor, sp_axis, dev, dtype=ttnn.bfloat16),
+ image_indicator_index=_seq_shard(image_idx, 1, padded_len, sp_factor, sp_axis, dev, dtype=ttnn.uint32),
+ llm_token_mask=_seq_shard(llm_token_mask, 1, padded_len, sp_factor, sp_axis, dev, dtype=ttnn.bfloat16),
+ output_image_mask=_seq_shard(output_image_mask, 1, padded_len, sp_factor, sp_axis, dev, dtype=ttnn.bfloat16),
+ attn_mask=None,
+ spatial_sequence_length=seq_len,
+ )
+ tt_out_torch = tensor.to_torch(tt_out, mesh_axes=[None, sp_axis, None])[:, :seq_len]
+
+ logger.info(
+ f"ideogram4 model [{weights}]: layers={num_layers} tp={tp_factor} sp={sp_factor} "
+ f"padded_heads={tt_model.layers[0].padded_heads} seq={seq_len} padded_len={padded_len}"
+ )
+ # Only OUTPUT_IMAGE positions carry a meaningful velocity (the reference zeros the
+ # image-latent input at llm positions; their output is unused conditioning noise).
+ image_mask = (indicator == OUTPUT_IMAGE_INDICATOR)[0] # (L,), same across batch
+ assert_quality(torch_out[:, image_mask], tt_out_torch[:, image_mask], pcc=0.99, relative_rmse=0.05)
diff --git a/code/models/tt_dit/tests/models/ideogram4/test_vae_ideogram4.py b/code/models/tt_dit/tests/models/ideogram4/test_vae_ideogram4.py
new file mode 100644
index 0000000000000000000000000000000000000000..85c96c3396f26b646917708ff930433ab873b148
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ideogram4/test_vae_ideogram4.py
@@ -0,0 +1,155 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+# =============================================================================
+# Ideogram 4.0 "Flux2 KL autoencoder" decode path. The reference autoencoder.py
+# ships a diffusers->Flux state-dict converter, so the model IS a diffusers
+# AutoencoderKL (block_out [128,256,512,512], latent_channels 32, mid attention).
+# We reuse vae_sd35.VAEDecoder + a post_quant_conv and verify the decode against a
+# diffusers AutoencoderKL with Ideogram's config. weights="random" is the wiring /
+# channel-parallel (GroupNorm shards channels) check across single device + TP;
+# weights="real" loads the shipped VAE checkpoint for a fidelity check.
+# =============================================================================
+
+import os
+
+import pytest
+import torch
+from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
+from loguru import logger
+from safetensors.torch import load_file
+
+import ttnn
+
+from ....models.vae.vae_ideogram4 import Ideogram4VAEDecoder
+from ....parallel.config import ParallelFactor, VAEParallelConfig
+from ....parallel.manager import CCLManager
+from ....utils.check import assert_quality
+from ....utils.tensor import bf16_tensor
+from ....utils.test import line_params, ring_params
+
+Z_CHANNELS = 32
+FP8 = os.environ.get("IDEOGRAM4_WEIGHTS")
+# Real-weight cases need the gated fp8 checkpoint; skip (don't error) when it isn't configured.
+_NEEDS_WEIGHTS = pytest.mark.skipif(not FP8, reason="IDEOGRAM4_WEIGHTS not set (gated fp8 checkpoint)")
+_L1 = {"l1_small_size": 32768}
+
+
+def _make_akl(weights: str, torch_dtype) -> AutoencoderKL:
+ """diffusers AutoencoderKL with Ideogram's config; random init or the shipped VAE weights."""
+ akl = AutoencoderKL(
+ in_channels=3,
+ out_channels=3,
+ latent_channels=Z_CHANNELS,
+ down_block_types=("DownEncoderBlock2D",) * 4,
+ up_block_types=("UpDecoderBlock2D",) * 4,
+ block_out_channels=(128, 256, 512, 512),
+ layers_per_block=2,
+ norm_num_groups=32,
+ )
+ if weights == "real":
+ vae_sd = load_file(f"{FP8}/vae/diffusion_pytorch_model.safetensors") # diffusers layout, not fp8
+ # bn.* are checkpoint-only batchnorm running stats absent from diffusers AutoencoderKL.
+ incompat = akl.load_state_dict({k: v for k, v in vae_sd.items() if not k.startswith("bn.")}, strict=False)
+ # Prove the shipped weights actually landed; otherwise random init passes PCC without
+ # testing the real checkpoint. (Verified empty on the real load.)
+ assert not incompat.missing_keys and not incompat.unexpected_keys, (
+ f"real VAE load key mismatch: missing={incompat.missing_keys[:5]} "
+ f"unexpected={incompat.unexpected_keys[:5]}"
+ )
+ return akl.to(torch_dtype).eval()
+
+
+@pytest.mark.parametrize(
+ (
+ "mesh_device",
+ "submesh_shape",
+ "tp_axis",
+ "num_links",
+ "device_params",
+ "topology",
+ "latent_h",
+ "latent_w",
+ "weights",
+ ),
+ [
+ # single device (TP=1): small latents, random + real fidelity.
+ pytest.param((1, 1), (1, 1), 1, 1, _L1, ttnn.Topology.Linear, 32, 32, "random", id="tp1_256sq"),
+ pytest.param((1, 1), (1, 1), 1, 1, _L1, ttnn.Topology.Linear, 32, 48, "random", id="tp1_256x384"),
+ pytest.param(
+ (1, 1), (1, 1), 1, 1, _L1, ttnn.Topology.Linear, 32, 32, "real", id="tp1_256sq_real", marks=_NEEDS_WEIGHTS
+ ),
+ # TP=4 on the 2x4 loudbox (channel-parallel GroupNorm), 512px + 1024px.
+ pytest.param(
+ (2, 4), (1, 4), 1, 1, {**line_params, **_L1}, ttnn.Topology.Linear, 64, 64, "random", id="tp4_512px"
+ ),
+ pytest.param(
+ (2, 4), (1, 4), 1, 1, {**line_params, **_L1}, ttnn.Topology.Linear, 128, 128, "random", id="tp4_1024px"
+ ),
+ # SP4xTP2 denoiser: VAE TP=2, replicated on the size-4 axis.
+ pytest.param(
+ (2, 4), (2, 4), 0, 1, {**line_params, **_L1}, ttnn.Topology.Linear, 64, 64, "random", id="tp2_512px"
+ ),
+ pytest.param(
+ (2, 4), (2, 4), 0, 1, {**line_params, **_L1}, ttnn.Topology.Linear, 128, 128, "random", id="tp2_1024px"
+ ),
+ # BH Galaxy 4x8, 2D torus Ring: VAE TP=4 (axis 0), replicated on the size-8 axis, 2 links/neighbor.
+ pytest.param(
+ (4, 8), (4, 8), 0, 2, {**ring_params, **_L1}, ttnn.Topology.Ring, 64, 64, "random", id="bh_galaxy_tp4_512px"
+ ),
+ pytest.param(
+ (4, 8),
+ (4, 8),
+ 0,
+ 2,
+ {**ring_params, **_L1},
+ ttnn.Topology.Ring,
+ 128,
+ 128,
+ "random",
+ id="bh_galaxy_tp4_1024px",
+ ),
+ ],
+ indirect=["mesh_device", "device_params"],
+)
+def test_vae_decoder(
+ *,
+ mesh_device: ttnn.MeshDevice,
+ submesh_shape: tuple[int, int],
+ tp_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+ latent_h: int,
+ latent_w: int,
+ weights: str,
+) -> None:
+ """VAE decode, channel-parallel at TP across the mesh (GroupNorm shards channels)."""
+ torch.manual_seed(0)
+ torch_dtype = torch.bfloat16
+ submesh = mesh_device.create_submesh(ttnn.MeshShape(*submesh_shape))
+ tp_factor = tuple(submesh.shape)[tp_axis]
+
+ akl = _make_akl(weights, torch_dtype)
+
+ parallel_config = VAEParallelConfig(tensor_parallel=ParallelFactor(factor=tp_factor, mesh_axis=tp_axis))
+ ccl_manager = CCLManager(submesh, num_links=num_links, topology=topology)
+ tt_decoder = Ideogram4VAEDecoder.from_torch(
+ akl, mesh_device=submesh, parallel_config=parallel_config, ccl_manager=ccl_manager
+ )
+
+ z = torch.randn(1, Z_CHANNELS, latent_h, latent_w, dtype=torch_dtype)
+ with torch.no_grad():
+ ref = akl.decode(z).sample # post_quant_conv + decoder
+
+ # Feed replicated NHWC: post_quant_conv is replicated; the decoder's conv_in
+ # (out_mesh_axis=tp) shards channels from there. Output is replicated -> take device 0.
+ tt_z = bf16_tensor(z.permute(0, 2, 3, 1), device=submesh) # NCHW -> NHWC
+ tt_out = tt_decoder(tt_z)
+ tt_out_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_out)[0]).permute(0, 3, 1, 2) # NHWC -> NCHW
+
+ logger.info(
+ f"ideogram4 VAE decode [{weights}] TP={tp_factor}: latent {latent_h}x{latent_w} "
+ f"-> image {latent_h*8}x{latent_w*8}"
+ )
+ assert_quality(ref.float(), tt_out_torch.float(), pcc=0.99, relative_rmse=0.15)
diff --git a/code/models/tt_dit/tests/models/ltx/conftest.py b/code/models/tt_dit/tests/models/ltx/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..56a5f3763f0684487f6c900fb25a44d2ee54345a
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/conftest.py
@@ -0,0 +1,34 @@
+# SPDX-FileCopyrightText: (c) 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""LTX test hooks.
+
+The earliest pytest hook available to a conftest: pin the process to one hardware thread per physical
+core and ``os.execv`` re-exec BEFORE torch/ttnn thread pools and the tt-metal device threads spawn, so
+they inherit the pinned mask (taskset-equivalent placement, fully in-process). On by default so every
+LTX test run gets it (galaxy ring traced replay 6.2 s re-execed vs 6.6-6.7 s without, the same as a
+launch-time ``taskset``); ``LTX_PIN_PREIMPORT=0`` skips the re-exec, ``LTX_PIN_CORES=0`` disables all
+pinning. A no-op on hosts without SMT or whose mask is already one thread per core, so CI runners
+without SMT behave exactly as before.
+
+The re-exec fires before the mesh_device fixture opens the device -- re-execing after a device is open
+risks wedging it.
+"""
+
+from __future__ import annotations
+
+import os
+
+import pytest
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_cmdline_main(config): # noqa: ARG001
+ """Runs before collection and before any fixture/device open -- the earliest safe re-exec point."""
+ if os.environ.get("LTX_PIN_PREIMPORT", "1") != "0":
+ # Imported lazily and locally: keeps this hook free of torch/ttnn.
+ from models.tt_dit.utils.host_affinity import reexec_pinned_before_torch
+
+ reexec_pinned_before_torch("LTX tests (pytest_cmdline_main)")
+ yield
diff --git a/code/models/tt_dit/tests/models/ltx/ltx_mesh_params.py b/code/models/tt_dit/tests/models/ltx/ltx_mesh_params.py
new file mode 100644
index 0000000000000000000000000000000000000000..4774cb5fc8e2d16170d8b3b174d87a2eff77c789
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/ltx_mesh_params.py
@@ -0,0 +1,292 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+import os
+
+import pytest
+
+import ttnn
+from models.tt_dit.utils.test import (
+ line_params_req_exact_devices,
+ ring_params_8k_req_exact_devices,
+ ring_params_req_exact_devices,
+)
+
+_line = line_params_req_exact_devices
+_ring = ring_params_req_exact_devices
+
+_1x1sp0tp1nl1_line_is_fsdp0 = pytest.param(
+ (1, 1), 0, 1, 1, {}, ttnn.Topology.Linear, False, id="1x1sp0tp1nl1_line_is_fsdp0"
+)
+_2x2sp0tp1nl2_line_is_fsdp1 = pytest.param(
+ (2, 2), 0, 1, 2, _line, ttnn.Topology.Linear, True, id="2x2sp0tp1nl2_line_is_fsdp1"
+)
+_2x4sp0tp1nl1_line_is_fsdp1 = pytest.param(
+ (2, 4), 0, 1, 1, _line, ttnn.Topology.Linear, True, id="2x4sp0tp1nl1_line_is_fsdp1"
+)
+_2x4sp1tp0nl1_line_is_fsdp1 = pytest.param(
+ (2, 4), 1, 0, 1, _line, ttnn.Topology.Linear, True, id="2x4sp1tp0nl1_line_is_fsdp1"
+)
+# is_fsdp=False mirrors production BH 2x4 (dynamic_load path, not per-layer FSDP gathers)
+_2x4sp1tp0nl2_line_is_fsdp0 = pytest.param(
+ (2, 4), 1, 0, 2, _line, ttnn.Topology.Linear, False, id="2x4sp1tp0nl2_line_is_fsdp0"
+)
+
+# WH has 4 links.
+_4x8sp1tp0nl4_ring_is_fsdp1 = pytest.param(
+ (4, 8), 1, 0, 4, _ring, ttnn.Topology.Ring, True, id="4x8sp1tp0nl4_ring_is_fsdp1"
+)
+_4x8sp1tp0nl2_line_is_fsdp0 = pytest.param(
+ (4, 8), 1, 0, 2, _line, ttnn.Topology.Linear, False, id="4x8sp1tp0nl2_line_is_fsdp0"
+)
+_4x8sp1tp0nl2_ring_is_fsdp0 = pytest.param(
+ (4, 8), 1, 0, 2, _ring, ttnn.Topology.Ring, False, id="4x8sp1tp0nl2_ring_is_fsdp0"
+)
+_4x32sp1tp0nl2_ring_is_fsdp0 = pytest.param(
+ (4, 32), 1, 0, 2, _ring, ttnn.Topology.Ring, False, id="4x32sp1tp0nl2_ring_is_fsdp0"
+)
+
+LTX_PIPELINE_MESH_PARAMS = [
+ _2x2sp0tp1nl2_line_is_fsdp1,
+ _2x4sp0tp1nl1_line_is_fsdp1,
+ _2x4sp1tp0nl2_line_is_fsdp0,
+ _4x8sp1tp0nl4_ring_is_fsdp1,
+ _4x8sp1tp0nl2_line_is_fsdp0,
+ _4x8sp1tp0nl2_ring_is_fsdp0,
+ _4x32sp1tp0nl2_ring_is_fsdp0,
+]
+
+
+def _with_dynamic_load(param, dynamic_load):
+ """Append dynamic_node arg to a 7-field mesh param, yielding the pipeline signature
+ (mesh_device, sp_axis, tp_axis, num_links, device_params, topology, is_fsdp, dynamic_load).
+ Some tests use 7-field mesh_device params, some need an 8th dynamic_load. This function
+ allows us concatenate them together"""
+ v = param.values # (mesh_shape, sp, tp, num_links, device_params, topology, is_fsdp)
+ assert len(v) == 7, f"expected a 7-field mesh param, got {len(v)}: {param.id}"
+ return pytest.param(*v, dynamic_load, marks=param.marks, id=param.id)
+
+
+# dynamic_load=True only for the 2x4 (BH-like) configs — mirrors production, where 2x4 pages
+# weights in/out (dynamic_load path) to avoid init-time DRAM OOM rather than per-layer FSDP gathers.
+_PIPELINE_DL_TRUE = {"2x4sp0tp1nl1_line_is_fsdp1", "2x4sp1tp0nl2_line_is_fsdp0"}
+
+LTX_PIPELINE_MESH_PARAMS_DL = [_with_dynamic_load(p, p.id in _PIPELINE_DL_TRUE) for p in LTX_PIPELINE_MESH_PARAMS]
+
+
+def _override_base_device_params(base, device_params, *, id=None):
+ """Swap the device_params (element 4) of a shared building block, keeping its geometry.
+ Optional id override for variants that need a distinct test id (e.g. the i2v prefix)."""
+ v = base.values # (mesh_shape, sp, tp, num_links, device_params, topology, is_fsdp)
+ return pytest.param(*v[:4], device_params, *v[5:], marks=base.marks, id=id or base.id)
+
+
+# One-stage (Pro) AV needs the L1_SMALL pool for the audio-vocoder conv1d taps. The base pipeline
+# params leave l1_small_size=0, which OOMs the vocoder on the 4x8 galaxy ("L1_SMALL ... bank size is
+# 0 B" in bank_manager). Reserve 32 KB L1_SMALL on the BH 4x8 configs (untraced -> no trace region),
+# matching the distilled AV configs. Other geometries (deselected by the CI -k) pass through unchanged.
+_LTX_ONE_STAGE_L1SMALL_IDS = {"4x8sp1tp0nl2_ring_is_fsdp0", "4x8sp1tp0nl2_line_is_fsdp0"}
+LTX_ONE_STAGE_MESH_PARAMS_DL = [
+ _override_base_device_params(p, {**p.values[4], "l1_small_size": 32768}, id=p.id)
+ if p.id in _LTX_ONE_STAGE_L1SMALL_IDS
+ else p
+ for p in LTX_PIPELINE_MESH_PARAMS_DL
+]
+
+# Two-stages (Pro) drives the same audio vocoder, so it needs the same pool for the same reason.
+LTX_TWO_STAGES_MESH_PARAMS_DL = LTX_ONE_STAGE_MESH_PARAMS_DL
+
+
+# ---------------------------------------------------------------------------
+# Distilled AV pipeline mesh params. Same geometry as the pipeline configs, but the audio
+# decode chain needs bigger device pools on a few configs, and the WH 4x8 ring config runs
+# dynamic_load. Reuse the shared building blocks for geometry (single-sourced); override
+# device_params only where audio decode diverges, then append the distilled dynamic_load.
+# ---------------------------------------------------------------------------
+# Override device_params for the audio decode chain. Each dict augments the base line/ring
+# params only with the pool(s) that config actually needs:
+# l1_small_size: the audio vocoder's native conv1d/conv2d taps (depthwise audio filters) run an
+# UntilizeWithHalo gather whose sharding/config tensors allocate from the dedicated L1_SMALL
+# pool. It defaults to 0, which OOMs the vocoder in decode; 32 KB matches the audio component
+# tests.
+# worker_l1_size: the WH 4x8 ring config's RingAttention otherwise hits a kernel code-size error;
+# the larger worker L1 gives its command stream room.
+# trace_region_size: under LTX_TRACED=1 both stage traces' command streams (stage-1 + the
+# larger-sequence stage-2) live here; measured need is ~236 MB at 1080p (get_trace_buffers_size),
+# so 500 MB leaves headroom.
+_ring_worker_l1 = {"worker_l1_size": 1344544, **_ring}
+_line_trace = {**_line, "trace_region_size": 500_000_000, "l1_small_size": 32768}
+# fabric_router_config (8 KB payload): the strided all-gather packs up to 4 bf16 tiles per fabric packet
+_ring_trace = {**ring_params_8k_req_exact_devices, "trace_region_size": 500_000_000, "l1_small_size": 32768}
+
+LTX_DISTILLED_MESH_PARAMS_DL = [
+ _with_dynamic_load(_2x2sp0tp1nl2_line_is_fsdp1, False),
+ _with_dynamic_load(_2x4sp0tp1nl1_line_is_fsdp1, True),
+ # BH on 2x4 (production loudbox): L1_SMALL scratch for the vocoder conv taps + trace region for the traced
+ # decode -- the same device params the I2V and audio lists already give this id, so the distilled e2e runs
+ # the served (traced) path here too.
+ _with_dynamic_load(_override_base_device_params(_2x4sp1tp0nl2_line_is_fsdp0, _line_trace), True),
+ # WH (ring) on 4x8: bigger worker L1 for RingAttention.
+ _with_dynamic_load(_override_base_device_params(_4x8sp1tp0nl4_ring_is_fsdp1, _ring_worker_l1), True),
+ # BH (linear) on 4x8: L1_SMALL for the vocoder conv taps (without it every tap filter OOMs on the
+ # L1_SMALL pool and falls through to the MAC path, ~13x slower audio) + trace region for the traced
+ # decode -- the same device params the ring row and the I2V/Pro lists already give this id.
+ _with_dynamic_load(_override_base_device_params(_4x8sp1tp0nl2_line_is_fsdp0, _line_trace), False),
+ # BH (ring) on 4x8: trace region + L1_SMALL for the traced decode.
+ _with_dynamic_load(_override_base_device_params(_4x8sp1tp0nl2_ring_is_fsdp0, _ring_trace), False),
+ _with_dynamic_load(_4x32sp1tp0nl2_ring_is_fsdp0, False),
+]
+
+# I2V chained t2v->i2v; both configs are traced (trace region + L1_SMALL). 4x8 Galaxy (ring):
+# full-res 1088x1920 latent shards unevenly on the 4x8 mesh (s1 cond latent 17x30, full 34x60),
+# so the VAE encoder fold + even-shard padding must handle non-mesh-aligned dims here — the 2x4
+# loudbox shards evenly and never hits this, so it's kept as a distinct id.
+LTX_DISTILLED_I2V_MESH_PARAMS_DL = [
+ _with_dynamic_load(_override_base_device_params(_2x4sp1tp0nl2_line_is_fsdp0, _line_trace), True),
+ _with_dynamic_load(
+ _override_base_device_params(_4x8sp1tp0nl2_ring_is_fsdp0, _ring_trace, id="i2v_4x8sp1tp0nl2_ring_is_fsdp0"),
+ False,
+ ),
+]
+
+# Two-stages (Pro) I2V smoke reuses the same geometry + device params as the distilled i2v chain
+# (2x4 loudbox + 4x8 galaxy, traced + L1_SMALL for the audio vocoder), so the Pro-I2V leg can run on
+# the single 4x8 galaxy in addition to the 2x4 loudbox.
+LTX_TWO_STAGES_I2V_MESH_PARAMS_DL = LTX_DISTILLED_I2V_MESH_PARAMS_DL
+
+# Audio-decode-only profiling: both configs use the traced line params (trace region + L1_SMALL)
+# so LTX_TRACED=1 can capture the vocoder.
+LTX_DISTILLED_AUDIO_MESH_PARAMS_DL = [
+ _with_dynamic_load(_override_base_device_params(_2x4sp1tp0nl2_line_is_fsdp0, _line_trace), True),
+ _with_dynamic_load(_override_base_device_params(_4x8sp1tp0nl2_line_is_fsdp0, _line_trace), False),
+]
+
+# No 1x1 config: real-grid shapes require SP padding, and video self-attention only masks
+# padded keys via ring SDPA's logical_n (sp>1). Production never runs sp=1.
+LTX_TRANSFORMER_MESH_PARAMS = [
+ _2x4sp0tp1nl1_line_is_fsdp1,
+ _2x4sp1tp0nl2_line_is_fsdp0,
+ _4x8sp1tp0nl4_ring_is_fsdp1,
+ _4x8sp1tp0nl2_ring_is_fsdp0,
+ _4x8sp1tp0nl2_line_is_fsdp0,
+]
+
+LTX_ATTENTION_MESH_PARAMS = [
+ _1x1sp0tp1nl1_line_is_fsdp0,
+ _2x4sp0tp1nl1_line_is_fsdp1,
+ _2x4sp1tp0nl1_line_is_fsdp1,
+ _4x8sp1tp0nl4_ring_is_fsdp1,
+ _4x8sp1tp0nl2_line_is_fsdp0,
+]
+
+# Upsampler uses VaeHWParallelConfig (h_axis, w_axis) instead of DiT sp/tp axes.
+_2x4h0w1nl1_line = pytest.param(
+ (2, 4), 0, 1, 1, {**_line, "trace_region_size": 23887872}, ttnn.Topology.Linear, id="2x4h0w1nl1_line"
+)
+
+_4x8h0w1nl2_line = pytest.param(
+ (4, 8), 0, 1, 2, {**_line, "trace_region_size": 23887872}, ttnn.Topology.Linear, id="4x8h0w1nl2_line"
+)
+
+LTX_UPSAMPLER_MESH_PARAMS = [
+ _2x4h0w1nl1_line,
+ _4x8h0w1nl2_line,
+]
+
+# VAE decoder mesh params — multi-device configs for full LTXVideoDecoder parity.
+# Format: (mesh_shape, h_axis, w_axis, num_links, device_params, topology)
+_LTX_SKIP_SMALL_MESH_FABRIC = os.environ.get("LTX_VAE_FORCE_SMALL_MESH_FABRIC", "0") != "1"
+
+_line_fabric = {"fabric_config": ttnn.FabricConfig.FABRIC_1D, "require_exact_physical_num_devices": True}
+
+_1x1h0w1nl1_line_vae = pytest.param(
+ (1, 1),
+ 0,
+ 1,
+ 1,
+ _line_fabric,
+ ttnn.Topology.Linear,
+ marks=pytest.mark.skipif(
+ _LTX_SKIP_SMALL_MESH_FABRIC,
+ reason="Known flaky fabric handshake on 1x1 mesh; set LTX_VAE_FORCE_SMALL_MESH_FABRIC=1 to force-run.",
+ ),
+ id="1x1h0w1nl1_line",
+)
+
+_2x4h0w1nl1_line_vae = pytest.param((2, 4), 0, 1, 1, _line_fabric, ttnn.Topology.Linear, id="2x4h0w1nl1_line")
+
+_2x4h1w0nl1_line_vae = pytest.param((2, 4), 1, 0, 1, _line_fabric, ttnn.Topology.Linear, id="2x4h1w0nl1_line")
+
+_1x8h0w1nl1_line_vae = pytest.param((1, 8), 0, 1, 1, _line_fabric, ttnn.Topology.Linear, id="1x8h0w1nl1_line")
+
+_1x4h1w0nl1_line_vae = pytest.param(
+ (1, 4),
+ 1,
+ 0,
+ 1,
+ _line_fabric,
+ ttnn.Topology.Linear,
+ marks=pytest.mark.skipif(
+ _LTX_SKIP_SMALL_MESH_FABRIC,
+ reason="Known flaky fabric handshake on 1x4 mesh; set LTX_VAE_FORCE_SMALL_MESH_FABRIC=1 to force-run.",
+ ),
+ id="1x4h1w0nl1_line",
+)
+
+_4x8h0w1nl2_line_vae = pytest.param((4, 8), 0, 1, 2, _line_fabric, ttnn.Topology.Linear, id="4x8h0w1nl2_line")
+
+LTX_VAE_DECODER_MESH_PARAMS = [
+ _1x1h0w1nl1_line_vae,
+ _2x4h0w1nl1_line_vae,
+ _2x4h1w0nl1_line_vae,
+ _1x8h0w1nl1_line_vae,
+ _1x4h1w0nl1_line_vae,
+ _4x8h0w1nl2_line_vae,
+]
+
+# Multi-device only (2K decode doesn't fit on a single device).
+LTX_VAE_DECODER_MULTI_ONLY_MESH_PARAMS = [
+ _2x4h0w1nl1_line_vae,
+ _2x4h1w0nl1_line_vae,
+ _4x8h0w1nl2_line_vae,
+]
+
+# Encoder production mesh (mirrors the I2V pipeline layout).
+LTX_VAE_ENCODER_PROD_MESH_PARAMS = [
+ _4x8h0w1nl2_line_vae,
+]
+
+# ---------------------------------------------------------------------------
+# Audio mesh params — (mesh_device, mesh_shape, sp_axis, tp_axis, num_links,
+# dynamic_load, device_params, topology, is_fsdp)
+# Audio tests create a submesh from mesh_device using mesh_shape, hence the
+# duplicate shape field.
+# ---------------------------------------------------------------------------
+_line_audio = {**_line, "l1_small_size": 32768}
+_ring_audio = {**_ring, "l1_small_size": 32768}
+_ring_trace_audio = {**_ring_audio, "trace_region_size": 300_000_000}
+
+LTX_AUDIO_MESH_PARAMS_FULL = [
+ pytest.param(
+ (2, 2), (2, 2), 0, 1, 2, False, _line_audio, ttnn.Topology.Linear, True, id="2x2sp0tp1nl2_line_is_fsdp1"
+ ),
+ pytest.param(
+ (2, 4), (2, 4), 0, 1, 1, True, _line_audio, ttnn.Topology.Linear, True, id="2x4sp0tp1nl1_line_is_fsdp1"
+ ),
+ pytest.param(
+ (2, 4), (2, 4), 1, 0, 2, True, _line_audio, ttnn.Topology.Linear, False, id="2x4sp1tp0nl2_line_is_fsdp0"
+ ),
+ pytest.param(
+ (4, 8), (4, 8), 1, 0, 4, False, _ring_audio, ttnn.Topology.Ring, True, id="4x8sp1tp0nl4_ring_is_fsdp1"
+ ),
+ pytest.param(
+ (4, 8), (4, 8), 1, 0, 2, False, _line_audio, ttnn.Topology.Linear, False, id="4x8sp1tp0nl2_line_is_fsdp0"
+ ),
+ pytest.param(
+ (4, 8), (4, 8), 1, 0, 2, False, _ring_trace_audio, ttnn.Topology.Ring, False, id="4x8sp1tp0nl2_ring_is_fsdp0"
+ ),
+ pytest.param(
+ (4, 32), (4, 32), 1, 0, 2, False, _ring_audio, ttnn.Topology.Ring, False, id="4x32sp1tp0nl2_ring_is_fsdp0"
+ ),
+]
diff --git a/code/models/tt_dit/tests/models/ltx/scaleout_configs/single_galaxy/mesh_graph_descriptor.textproto b/code/models/tt_dit/tests/models/ltx/scaleout_configs/single_galaxy/mesh_graph_descriptor.textproto
new file mode 100644
index 0000000000000000000000000000000000000000..3c8df695db0cc55adecbf5a00849b2d86cd66486
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/scaleout_configs/single_galaxy/mesh_graph_descriptor.textproto
@@ -0,0 +1,20 @@
+# Single-rank single-galaxy mesh for the LTX-2.3 multihost CI (bh_sc1) legs.
+# LTX's 4x8sp1tp0nl2_ring config uses FabricConfig FABRIC_1D_RING, which requires the MGD to
+# provide TORUS_XY connectivity (BOTH dims wrap). A FabricConfig can only *restrict* topology
+# (torus->mesh), not add links, so a single-axis ring (TORUS_X, i.e. dim_types [LINE, RING]) fails
+# at mesh_graph init with "requires more connectivity than MGD provides". Both dims are therefore
+# RING, matching the proven tt_metal/fabric single_bh_galaxy_torus_xy descriptor (that one is [8,4];
+# here it is [4,8] because the LTX test requests a 4x8 mesh). One rank owns the whole 32-device
+# galaxy (confirmed on bh_sc1: a single rank sees all 32 chips).
+mesh_descriptors {
+ name: "M0"
+ arch: BLACKHOLE
+ device_topology { dims: [ 4, 8 ] dim_types: [ RING, RING ] }
+ host_topology { dims: [ 1, 1 ] }
+ channels {
+ count: 2
+ policy: RELAXED
+ }
+}
+
+top_level_instance { mesh { mesh_descriptor: "M0" mesh_id: 0 } }
diff --git a/code/models/tt_dit/tests/models/ltx/test_attention_ltx.py b/code/models/tt_dit/tests/models/ltx/test_attention_ltx.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c0597ae96ee9169f3c6a9496ff4ad82468d62dd
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_attention_ltx.py
@@ -0,0 +1,258 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""LTX-2 attention unit tests; mesh/fabric parametrization mirrors test_wan_attention."""
+
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.tt_dit.models.transformers.ltx.attention_ltx import LTXAttention
+from models.tt_dit.parallel.config import DiTParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.mochi import get_rot_transformation_mat
+from models.tt_dit.utils.tensor import bf16_tensor, bf16_tensor_2dshard
+from models.tt_dit.utils.test import skip_if_unsupported_num_links
+
+from .ltx_mesh_params import LTX_ATTENTION_MESH_PARAMS
+
+
+def _diffusers_qk_to_split(t: torch.Tensor, num_heads: int, head_dim: int) -> torch.Tensor:
+ """diffusers (interleaved-rotation) Q/K → Lightricks SPLIT convention the TT loader expects."""
+ D = head_dim
+ D_half = D // 2
+ inv = torch.empty(D, dtype=torch.long)
+ inv[:D_half] = torch.arange(0, D, 2)
+ inv[D_half:] = torch.arange(1, D, 2)
+ rest = t.shape[1:]
+ return t.reshape(num_heads, D, *rest).index_select(1, inv).reshape(num_heads * D, *rest)
+
+
+def _convert_diffusers_attn_state(state: dict, num_heads: int, head_dim: int) -> dict:
+ """diffusers LTX2Attention state_dict → TT LTXAttention loader input (split Q/K convention)."""
+ out = dict(state)
+ for base in ("to_q", "to_k"):
+ for suffix in ("weight", "bias"):
+ k = f"{base}.{suffix}"
+ if k in out:
+ out[k] = _diffusers_qk_to_split(out[k], num_heads, head_dim)
+ for k in ("norm_q.weight", "norm_k.weight"):
+ if k in out:
+ out[k] = _diffusers_qk_to_split(out[k], num_heads, head_dim)
+ return out
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ LTX_ATTENTION_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_self_attention(
+ mesh_device: ttnn.MeshDevice,
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+ is_fsdp: bool,
+) -> None:
+ """
+ Test LTX-2 self-attention: compare TT LTXAttention vs PyTorch Attention.
+ """
+ from diffusers.models.transformers.transformer_ltx2 import LTX2Attention, LTX2AudioVideoRotaryPosEmbed
+
+ skip_if_unsupported_num_links(mesh_device, num_links)
+
+ dim = 4096
+ num_heads = 32
+ head_dim = dim // num_heads
+ B = 1
+ seq_len = 256 # Small for fast test
+
+ # interleaved matches the (B, N, dim) cos/sin layout the device kernel expects
+ torch_model = LTX2Attention(
+ query_dim=dim, heads=num_heads, kv_heads=num_heads, dim_head=head_dim, norm_eps=1e-6, rope_type="interleaved"
+ )
+ torch_model.eval()
+ torch_state = torch_model.state_dict()
+
+ ccl_manager = CCLManager(mesh_device=mesh_device, num_links=num_links, topology=topology)
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=1, mesh_axis=0),
+ sequence_parallel=ParallelFactor(factor=tuple(mesh_device.shape)[sp_axis], mesh_axis=sp_axis),
+ tensor_parallel=ParallelFactor(factor=tuple(mesh_device.shape)[tp_axis], mesh_axis=tp_axis),
+ )
+
+ tt_model = LTXAttention(
+ dim=dim,
+ num_heads=num_heads,
+ eps=1e-6,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ is_self=True,
+ )
+ tt_model.load_torch_state_dict(_convert_diffusers_attn_state(torch_state, num_heads, head_dim))
+
+ # seq_len=256 → N_local=32 for 4x8 (sp=8); exactly 1 tile, the minimum
+ torch.manual_seed(42)
+ x = torch.randn(B, seq_len, dim, dtype=torch.float32)
+
+ # Build RoPE (small grid for test) with the diffusers reference, like test_rope_ltx.
+ F, H, W = 4, 8, 8 # 4*8*8=256 = seq_len
+ t_ids = torch.arange(F)
+ h_ids = torch.arange(H)
+ w_ids = torch.arange(W)
+ grid_t, grid_h, grid_w = torch.meshgrid(t_ids, h_ids, w_ids, indexing="ij")
+ indices_grid = torch.stack([grid_t.flatten(), grid_h.flatten(), grid_w.flatten()], dim=0).float().unsqueeze(0)
+
+ rope = LTX2AudioVideoRotaryPosEmbed(
+ dim=dim,
+ base_num_frames=20,
+ base_height=2048,
+ base_width=2048,
+ theta=10000.0,
+ modality="video",
+ double_precision=False,
+ rope_type="interleaved",
+ num_attention_heads=num_heads,
+ )
+ # Interleaved cos/sin: (B, seq_len, dim)
+ cos_freq, sin_freq = rope(indices_grid)
+ # Reshape for the device kernel: (B, seq_len, num_heads, head_dim)
+ cos_apply = cos_freq.reshape(B, seq_len, num_heads, head_dim)
+ sin_apply = sin_freq.reshape(B, seq_len, num_heads, head_dim)
+
+ # diffusers applies interleaved RoPE to q/k on (B, T, dim) before the head split.
+ with torch.no_grad():
+ torch_out = torch_model(x, query_rotary_emb=(cos_freq, sin_freq))
+
+ logger.info(f"PyTorch output shape: {torch_out.shape}")
+
+ # Prepare TT tensors
+ # spatial: (1, B, N, D) fractured on SP/TP
+ spatial = x.unsqueeze(0) # (1, B, N, D)
+ tt_spatial = bf16_tensor_2dshard(spatial, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 3})
+
+ # rotary_embedding_llama expects per-head (B, H, N, head_dim)
+ rope_cos_bhnd = cos_apply.permute(0, 2, 1, 3) # (B, H, N, D)
+ rope_sin_bhnd = sin_apply.permute(0, 2, 1, 3)
+ tt_cos = bf16_tensor_2dshard(rope_cos_bhnd, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ tt_sin = bf16_tensor_2dshard(rope_sin_bhnd, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ tt_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+
+ # TT forward
+ tt_out = tt_model(
+ spatial_1BND=tt_spatial,
+ N=seq_len,
+ rope_cos=tt_cos,
+ rope_sin=tt_sin,
+ trans_mat=tt_trans_mat,
+ )
+
+ # Gather and compare
+ concat_dims = [None, None]
+ concat_dims[sp_axis] = 2
+ concat_dims[tp_axis] = 3
+ tt_out_torch = ttnn.to_torch(
+ tt_out,
+ mesh_composer=ttnn.ConcatMesh2dToTensor(mesh_device, dims=concat_dims, mesh_shape=tuple(mesh_device.shape)),
+ ).squeeze(0)
+
+ # 8-way SP-ring all-gathers on 4x8 loosen the bound; <=8 devices hold the single-op bar.
+ pcc = 0.988 if mesh_device.get_num_devices() > 8 else 0.999
+ assert_quality(torch_out, tt_out_torch, pcc=pcc)
+ logger.info("PASSED: LTX self-attention matches PyTorch reference")
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ LTX_ATTENTION_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_cross_attention(
+ mesh_device: ttnn.MeshDevice,
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+ is_fsdp: bool,
+) -> None:
+ """
+ Test LTX-2 cross-attention: compare TT LTXAttention vs PyTorch Attention.
+ """
+ from diffusers.models.transformers.transformer_ltx2 import LTX2Attention
+
+ skip_if_unsupported_num_links(mesh_device, num_links)
+
+ dim = 4096
+ context_dim = 4096
+ num_heads = 32
+ head_dim = dim // num_heads
+ B = 1
+ seq_len = 256
+ prompt_len = 32
+
+ # diffusers reference cross-attention (cross_attention_dim set; no RoPE on text K/V)
+ torch_model = LTX2Attention(
+ query_dim=dim,
+ cross_attention_dim=context_dim,
+ heads=num_heads,
+ kv_heads=num_heads,
+ dim_head=head_dim,
+ norm_eps=1e-6,
+ )
+ torch_model.eval()
+ torch_state = torch_model.state_dict()
+
+ ccl_manager = CCLManager(mesh_device=mesh_device, num_links=num_links, topology=topology)
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=1, mesh_axis=0),
+ sequence_parallel=ParallelFactor(factor=tuple(mesh_device.shape)[sp_axis], mesh_axis=sp_axis),
+ tensor_parallel=ParallelFactor(factor=tuple(mesh_device.shape)[tp_axis], mesh_axis=tp_axis),
+ )
+
+ tt_model = LTXAttention(
+ dim=dim,
+ num_heads=num_heads,
+ eps=1e-6,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ is_self=False,
+ context_dim=context_dim,
+ )
+ tt_model.load_torch_state_dict(torch_state)
+
+ torch.manual_seed(42)
+ x = torch.randn(B, seq_len, dim, dtype=torch.float32)
+ context = torch.randn(B, prompt_len, context_dim, dtype=torch.float32)
+
+ with torch.no_grad():
+ torch_out = torch_model(x, encoder_hidden_states=context)
+
+ logger.info(f"PyTorch cross-attn output shape: {torch_out.shape}")
+
+ spatial = x.unsqueeze(0)
+ prompt = context.unsqueeze(0)
+ tt_spatial = bf16_tensor_2dshard(spatial, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 3})
+ tt_prompt = bf16_tensor(prompt, device=mesh_device)
+
+ tt_out = tt_model(spatial_1BND=tt_spatial, N=seq_len, prompt_1BLP=tt_prompt)
+
+ concat_dims = [None, None]
+ concat_dims[sp_axis] = 2
+ concat_dims[tp_axis] = 3
+ tt_out_torch = ttnn.to_torch(
+ tt_out,
+ mesh_composer=ttnn.ConcatMesh2dToTensor(mesh_device, dims=concat_dims, mesh_shape=tuple(mesh_device.shape)),
+ ).squeeze(0)
+
+ # 8-way SP-ring all-gathers on 4x8 loosen the bound; <=8 devices hold the single-op bar.
+ pcc = 0.988 if mesh_device.get_num_devices() > 8 else 0.999
+ assert_quality(torch_out, tt_out_torch, pcc=pcc)
+ logger.info("PASSED: LTX cross-attention matches PyTorch reference")
diff --git a/code/models/tt_dit/tests/models/ltx/test_audio_ltx.py b/code/models/tt_dit/tests/models/ltx/test_audio_ltx.py
new file mode 100644
index 0000000000000000000000000000000000000000..efa66a87662b4f384277f2e852d5402d05e8a639
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_audio_ltx.py
@@ -0,0 +1,1207 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Audio tests for LTX-2: per-component stage checks + post-denoise decode e2e.
+
+Component stage tests (random weights vs diffusers LTX-2 torch reference):
+- Stage A: ``audio_decoder`` (mel-VAE)
+- Stage B: ``vocoder`` (24 kHz waveform)
+- Stage C: ``vocoder_with_bwe`` (48 kHz BWE path)
+
+End-to-end decode (mel-VAE → vocoder → BWE, requires real checkpoints):
+- on-device decode runs on representative meshes
+- warm-path decode stays functional after cold compile/build
+- on-device decode quality remains close to host reference (PSNR)
+
+Denoise-path audio regressions live in sibling files.
+"""
+
+from __future__ import annotations
+
+import math
+import os
+import time
+from glob import glob
+
+import einops
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.tt_dit.models.audio_vae.audio_decoder_ltx import LTXAudioDecoderAdapter
+from models.tt_dit.models.audio_vae.bwe_ltx import MelSTFT, VocoderWithBWE
+from models.tt_dit.models.audio_vae.mel_decoder_ltx import AudioUpsample, MelDecoder, ResnetBlock
+from models.tt_dit.models.audio_vae.vocoder_ltx import AMPBlock1, Vocoder
+from models.tt_dit.parallel.config import AudioTCParallelConfig, DiTParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.pipelines.ltx.pipeline_ltx import LTXPipeline
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.test import skip_if_unsupported_num_links
+from models.tt_dit.utils.video import Audio
+
+from .ltx_mesh_params import LTX_AUDIO_MESH_PARAMS_FULL
+
+_WARM_ITERS = 3
+
+
+# ---------------------------------------------------------------------------
+# Checkpoint discovery
+# ---------------------------------------------------------------------------
+# Default checkpoint filenames used by the e2e/perf decode tests. Any of these can
+# be overridden at runtime with LTX_CHECKPOINT, which is honored regardless of its
+# basename (see _resolve_checkpoint). The tuples below only drive *fallback*
+# discovery when LTX_CHECKPOINT is unset.
+_DEV_CHECKPOINT = "ltx-2.3-22b-dev.safetensors"
+_DISTILLED_CHECKPOINT = "ltx-2.3-22b-distilled-1.1.safetensors"
+_LOCAL_CHECKPOINT_DIR = "~/.cache/ltx-checkpoints"
+# HF cache snapshot glob for the LTX-2.3 repo; {name} is the checkpoint filename.
+_HF_SNAPSHOT_GLOB = "~/.cache/huggingface/hub/models--Lightricks--LTX-2.3/snapshots/*/{name}"
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+def _resolve_checkpoint(candidates: list[str], expected_filenames: tuple[str, ...]) -> str | None:
+ # An explicit LTX_CHECKPOINT override wins unconditionally as long as the file
+ # exists — that's the whole point of the env var. The expected_filenames tuple
+ # only gates the fallback discovery below, never the override.
+ explicit = os.environ.get("LTX_CHECKPOINT")
+ if explicit and os.path.exists(explicit):
+ return explicit
+ for path in candidates:
+ expanded = os.path.expanduser(path)
+ if os.path.exists(expanded):
+ return expanded
+ # Reuse default HF cache location if present; avoid network dependency.
+ for name in expected_filenames:
+ hits = glob(os.path.expanduser(_HF_SNAPSHOT_GLOB.format(name=name)))
+ if hits:
+ return hits[0]
+ return None
+
+
+def _build_pipeline(
+ mesh_device: ttnn.MeshDevice,
+ *,
+ sp_axis: int,
+ tp_axis: int,
+ checkpoint: str,
+ num_links: int,
+ topology: ttnn.Topology,
+) -> tuple[LTXPipeline, DiTParallelConfig]:
+ mesh_shape = tuple(mesh_device.shape)
+ parallel_config = DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=1, mesh_axis=0),
+ sequence_parallel=ParallelFactor(factor=mesh_shape[sp_axis], mesh_axis=sp_axis),
+ tensor_parallel=ParallelFactor(factor=mesh_shape[tp_axis], mesh_axis=tp_axis),
+ )
+ ccl_manager = CCLManager(mesh_device, num_links=num_links, topology=topology)
+ pipeline = LTXPipeline(
+ mesh_device=mesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ checkpoint_name=None,
+ )
+ pipeline.checkpoint_name = checkpoint
+ pipeline._audio_adapter = LTXAudioDecoderAdapter(
+ pipeline.checkpoint_name,
+ mesh_device=pipeline.mesh_device,
+ vae_ccl_manager=pipeline.vae_ccl_manager,
+ dit_parallel_config=pipeline.parallel_config,
+ traced=pipeline._traced,
+ )
+ return pipeline, parallel_config
+
+
+def _psnr(ref: torch.Tensor, test: torch.Tensor) -> float:
+ ref = ref.float()
+ test = test.float()
+ mse = torch.mean((ref - test) ** 2).item()
+ if mse == 0.0:
+ return float("inf")
+ peak = ref.abs().max().item()
+ if peak == 0.0:
+ return float("inf")
+ return 20.0 * math.log10(peak) - 10.0 * math.log10(mse)
+
+
+# ---------------------------------------------------------------------------
+# Tests
+# ---------------------------------------------------------------------------
+@pytest.mark.parametrize(
+ (
+ "mesh_device",
+ "mesh_shape",
+ "sp_axis",
+ "tp_axis",
+ "num_links",
+ "dynamic_load",
+ "device_params",
+ "topology",
+ "is_fsdp",
+ ),
+ LTX_AUDIO_MESH_PARAMS_FULL,
+ indirect=["mesh_device", "device_params"],
+)
+def test_audio_decode_perf(
+ mesh_device: ttnn.MeshDevice,
+ mesh_shape: tuple[int, int],
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ dynamic_load: bool,
+ topology: ttnn.Topology,
+ is_fsdp: bool,
+):
+ ckpt = _resolve_checkpoint(
+ [f"{_LOCAL_CHECKPOINT_DIR}/{_DEV_CHECKPOINT}"],
+ (_DEV_CHECKPOINT,),
+ )
+ if ckpt is None:
+ pytest.skip("checkpoint not found (set LTX_CHECKPOINT)")
+
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ _ = (dynamic_load, is_fsdp) # parity with fast_av config schema
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ torch.manual_seed(0)
+ pipeline, parallel_config = _build_pipeline(
+ mesh_device,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ checkpoint=ckpt,
+ num_links=num_links,
+ topology=topology,
+ )
+ mesh_shape = tuple(mesh_device.shape)
+
+ audio_latent = torch.randn(1, 256, 128, dtype=torch.float32)
+ num_frames, fps = 145, 24.0
+
+ t0 = time.time()
+ dev_audio = pipeline.decode_audio(audio_latent, num_frames, fps=fps)
+ t_cold = time.time() - t0
+ assert dev_audio is not None
+
+ warm_times = []
+ for _ in range(_WARM_ITERS):
+ t0 = time.time()
+ out = pipeline.decode_audio(audio_latent, num_frames, fps=fps)
+ warm_times.append(time.time() - t0)
+ assert out is not None
+
+ logger.info(
+ f"Audio decode perf (mesh {mesh_shape}, T-shard={parallel_config.sequence_parallel.factor}): "
+ f"cold={t_cold:.2f}s warm_min={min(warm_times):.2f}s warm_mean={sum(warm_times)/len(warm_times):.2f}s "
+ f"wave={tuple(dev_audio.waveform.shape)} sr={dev_audio.sampling_rate}"
+ )
+
+
+@pytest.mark.parametrize(
+ (
+ "mesh_device",
+ "mesh_shape",
+ "sp_axis",
+ "tp_axis",
+ "num_links",
+ "dynamic_load",
+ "device_params",
+ "topology",
+ "is_fsdp",
+ ),
+ LTX_AUDIO_MESH_PARAMS_FULL,
+ indirect=["mesh_device", "device_params"],
+)
+def test_audio_decode_e2e_psnr(
+ mesh_device: ttnn.MeshDevice,
+ mesh_shape: tuple[int, int],
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ dynamic_load: bool,
+ topology: ttnn.Topology,
+ is_fsdp: bool,
+):
+ ckpt = _resolve_checkpoint(
+ [f"{_LOCAL_CHECKPOINT_DIR}/{_DISTILLED_CHECKPOINT}"],
+ (_DISTILLED_CHECKPOINT,),
+ )
+ if ckpt is None:
+ pytest.skip("distilled checkpoint not found (set LTX_CHECKPOINT)")
+
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ _ = (dynamic_load, is_fsdp) # parity with fast_av config schema
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ torch.manual_seed(0)
+ mesh_shape = tuple(mesh_device.shape)
+ pipeline, _ = _build_pipeline(
+ mesh_device,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ checkpoint=ckpt,
+ num_links=num_links,
+ topology=topology,
+ )
+
+ audio_latent = torch.randn(1, 256, 128, dtype=torch.float32)
+ num_frames, fps = 145, 24.0
+
+ ref_audio = _decode_audio_reference(ckpt, audio_latent, num_frames, fps=fps)
+ assert ref_audio is not None, "host reference audio decode returned None"
+
+ dev_audio = pipeline.decode_audio(audio_latent, num_frames, fps=fps)
+ assert dev_audio is not None, "on-device audio decode returned None"
+ assert dev_audio.sampling_rate == ref_audio.sampling_rate
+
+ n = min(ref_audio.waveform.shape[-1], dev_audio.waveform.shape[-1])
+ ref_w = ref_audio.waveform[..., :n].float()
+ dev_w = dev_audio.waveform[..., :n].float()
+ psnr = _psnr(ref_w, dev_w)
+
+ logger.info(
+ f"Audio decode PSNR (mesh {mesh_shape}): {psnr:.2f} dB "
+ f"(ref {tuple(ref_audio.waveform.shape)} -> dev {tuple(dev_audio.waveform.shape)})"
+ )
+ assert psnr >= 28.0, f"audio decode PSNR {psnr:.2f} dB < 28 dB"
+
+
+# ===========================================================================
+# Component stage tests (pipeline_ltx.py audio decode chain)
+#
+# Torch references use diffusers LTX-2 audio modules (same as transformer/rope tests).
+# Stage/component IDs match ``pipeline_ltx.py`` audio decode chain:
+# - Stage A: ``audio_decoder`` (mel-VAE)
+# - Stage B: ``vocoder`` (24 kHz waveform)
+# - Stage C: ``vocoder_with_bwe`` (48 kHz BWE path)
+# ===========================================================================
+_AUDIO_DECODER_CFG = dict(
+ ch=128,
+ out_ch=2,
+ ch_mult=(1, 2, 4),
+ num_res_blocks=2,
+ attn_resolutions=(),
+ mid_block_add_attention=False,
+ z_channels=8,
+ resolution=64,
+ mel_bins=64,
+ sample_rate=16000,
+ mel_hop_length=160,
+ is_causal=True,
+)
+
+_MAIN_VOCODER_CFG = dict(
+ resblock_kernel_sizes=[3, 7, 11],
+ upsample_rates=[5, 2, 2, 2, 2, 2],
+ upsample_kernel_sizes=[11, 4, 4, 4, 4, 4],
+ resblock_dilation_sizes=[[1, 3, 5], [1, 3, 5], [1, 3, 5]],
+ upsample_initial_channel=1536,
+ resblock="AMP1",
+ output_sampling_rate=24000,
+ activation="snakebeta",
+ use_tanh_at_final=False,
+ apply_final_activation=True,
+ use_bias_at_final=False,
+)
+
+_BWE_VOCODER_CFG = dict(
+ resblock_kernel_sizes=[3, 7, 11],
+ upsample_rates=[6, 5, 2, 2, 2],
+ upsample_kernel_sizes=[12, 11, 4, 4, 4],
+ resblock_dilation_sizes=[[1, 3, 5], [1, 3, 5], [1, 3, 5]],
+ upsample_initial_channel=512,
+ resblock="AMP1",
+ output_sampling_rate=48000,
+ activation="snakebeta",
+ use_tanh_at_final=False,
+ apply_final_activation=False,
+ use_bias_at_final=False,
+)
+
+_MEL_STFT_CFG = dict(
+ filter_length=512,
+ hop_length=80,
+ win_length=512,
+ n_mel_channels=64,
+)
+
+_INPUT_SR = 16000
+_OUTPUT_SR = 48000
+_HOP_LENGTH = 80
+
+
+# ---------------------------------------------------------------------------
+# Component helpers
+# ---------------------------------------------------------------------------
+def _vocoder_mel(t_frames: int = 120) -> torch.Tensor:
+ """Stereo mel input. t_frames=120 with T-shard factor 4 leaves t_pad=8 (128-aligned),
+ so the tile-align pad fits inside the last shard and the sharded path's per-op tail
+ materialization is exercised — the boundary the click came from."""
+ return torch.randn(1, 2, t_frames, 64, dtype=torch.float32) * 0.5
+
+
+def _assert_sharded_matches_unsharded(unsharded: torch.Tensor, sharded: torch.Tensor, *, name: str) -> None:
+ """The T-sharded vocoder must reproduce the unsharded device path up to the bf16 storage
+ floor. A t_pad boundary regression (the audible click) shows up as a localized tail spike
+ of order 1e-2 — far above the ~1e-4 floor — so a tight max|Δ| catches it where aggregate
+ PCC against the torch reference (with its own fp32-kernel floor) does not."""
+ max_abs = (unsharded - sharded).abs().max().item()
+ logger.info(f"{name}: sharded vs unsharded device max|Δ| = {max_abs:.3e}")
+ assert max_abs < 5e-3, f"{name}: sharded vs unsharded device max|Δ| {max_abs:.3e} — t_pad boundary drift"
+
+
+def _require_diffusers():
+ pytest.importorskip("diffusers")
+ from diffusers.models.autoencoders.autoencoder_kl_ltx2_audio import AutoencoderKLLTX2Audio
+ from diffusers.pipelines.ltx2.vocoder import LTX2Vocoder, LTX2VocoderWithBWE
+
+ return AutoencoderKLLTX2Audio, LTX2Vocoder, LTX2VocoderWithBWE
+
+
+def _diffusers_vocoder_state_to_tt(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """Invert diffusers LTX-2.3 vocoder key names to the TT/Lightricks layout."""
+ out: dict[str, torch.Tensor] = {}
+ for key, value in state_dict.items():
+ new_key = (
+ key.replace("conv_in.", "conv_pre.")
+ .replace("conv_out.", "conv_post.")
+ .replace("act_out.", "act_post.")
+ .replace("upsamplers.", "ups.")
+ .replace("resnets.", "resblocks.")
+ )
+ # TT derives downsample filter taps at init; these are not loadable Parameters.
+ if new_key.endswith("downsample.filter"):
+ continue
+ out[new_key] = value
+ return out
+
+
+def _diffusers_vocoder_with_bwe_state_to_tt(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ out: dict[str, torch.Tensor] = {}
+ for prefix in ("vocoder.", "bwe_generator."):
+ sub = {k[len(prefix) :]: v for k, v in state_dict.items() if k.startswith(prefix)}
+ for k, v in _diffusers_vocoder_state_to_tt(sub).items():
+ out[f"{prefix}{k}"] = v
+ for key, value in state_dict.items():
+ if key.startswith("mel_stft."):
+ out[key] = value
+ return out
+
+
+def _audio_decoder_state_from_diffusers(
+ vae,
+ *,
+ stats_std: torch.Tensor,
+ stats_mean: torch.Tensor,
+) -> dict[str, torch.Tensor]:
+ state = {k[len("decoder.") :]: v for k, v in vae.state_dict().items() if k.startswith("decoder.")}
+ state["per_channel_statistics.std-of-means"] = stats_std
+ state["per_channel_statistics.mean-of-means"] = stats_mean
+ return state
+
+
+def _decode_latent_ref(
+ vae,
+ latent: torch.Tensor,
+ *,
+ stats_std: torch.Tensor,
+ stats_mean: torch.Tensor,
+) -> torch.Tensor:
+ """Match the Lightricks MelDecoder / TT denormalize + decoder forward."""
+ b, channels, _t, mel_bins = latent.shape
+ patched = einops.rearrange(latent, "b c t f -> b t (c f)")
+ std = stats_std.to(dtype=patched.dtype, device=patched.device)
+ mean = stats_mean.to(dtype=patched.dtype, device=patched.device)
+ last_dim = patched.shape[-1]
+ if std.shape[0] != last_dim:
+ if std.shape[0] == channels and last_dim == channels * mel_bins:
+ std = std.repeat_interleave(mel_bins)
+ mean = mean.repeat_interleave(mel_bins)
+ else:
+ raise ValueError(f"per-channel stats {tuple(std.shape)} do not broadcast to last dim {last_dim}")
+ sample = einops.rearrange(patched * std + mean, "b t (c f) -> b c t f", c=channels, f=mel_bins)
+ return vae.decoder(sample)
+
+
+def _compute_mel_ref(vocoder_with_bwe, audio_bct: torch.Tensor) -> torch.Tensor:
+ batch, n_channels, _ = audio_bct.shape
+ flat = audio_bct.reshape(batch * n_channels, -1).unsqueeze(1)
+ log_mel, _, _, _ = vocoder_with_bwe.mel_stft(flat)
+ return log_mel.reshape(batch, n_channels, log_mel.shape[1], log_mel.shape[2])
+
+
+def _tt_vocoder_cfg(cfg: dict) -> dict:
+ # Torch reference config carries keys that TT constructor does not accept.
+ return {k: v for k, v in cfg.items() if k != "output_sampling_rate"}
+
+
+def _randomize_snake_alphas(module: torch.nn.Module) -> None:
+ with torch.no_grad():
+ for submodule in module.modules():
+ if hasattr(submodule, "alpha") and isinstance(submodule.alpha, torch.nn.Parameter):
+ submodule.alpha.data = torch.randn_like(submodule.alpha.data) * 0.1
+ if hasattr(submodule, "beta") and isinstance(submodule.beta, torch.nn.Parameter):
+ submodule.beta.data = torch.randn_like(submodule.beta.data) * 0.1
+
+
+def _build_torch_stage_b(seed: int = 42):
+ _, LTX2Vocoder, _ = _require_diffusers()
+
+ torch.manual_seed(seed)
+ model = LTX2Vocoder(
+ in_channels=128,
+ hidden_channels=_MAIN_VOCODER_CFG["upsample_initial_channel"],
+ out_channels=2,
+ upsample_kernel_sizes=_MAIN_VOCODER_CFG["upsample_kernel_sizes"],
+ upsample_factors=_MAIN_VOCODER_CFG["upsample_rates"],
+ resnet_kernel_sizes=_MAIN_VOCODER_CFG["resblock_kernel_sizes"],
+ resnet_dilations=_MAIN_VOCODER_CFG["resblock_dilation_sizes"],
+ act_fn=_MAIN_VOCODER_CFG["activation"],
+ antialias=True,
+ final_act_fn=None,
+ final_bias=_MAIN_VOCODER_CFG["use_bias_at_final"],
+ output_sampling_rate=_MAIN_VOCODER_CFG["output_sampling_rate"],
+ ).eval()
+ _randomize_snake_alphas(model)
+ return model
+
+
+def _audio_parallel_config(mesh_shape: tuple[int, int]) -> AudioTCParallelConfig | ParallelFactor | None:
+ t_axis = 0 if mesh_shape[0] >= mesh_shape[1] else 1
+ t_factor = mesh_shape[t_axis]
+ c_axis = 1 - t_axis
+ c_factor = mesh_shape[c_axis]
+ if t_factor > 1 and c_factor > 1:
+ return AudioTCParallelConfig(
+ time_parallel=ParallelFactor(factor=t_factor, mesh_axis=t_axis),
+ channel_parallel=ParallelFactor(factor=c_factor, mesh_axis=c_axis),
+ )
+ if t_factor > 1:
+ return ParallelFactor(factor=t_factor, mesh_axis=t_axis)
+ return None
+
+
+def _build_tt_stage_b(mesh_device: ttnn.MeshDevice, *, parallel_config, ccl_manager: CCLManager | None) -> Vocoder:
+ return Vocoder(
+ mesh_device=mesh_device,
+ dtype=ttnn.float32,
+ in_channels=128,
+ out_channels=2,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ **_tt_vocoder_cfg(_MAIN_VOCODER_CFG),
+ )
+
+
+def _build_torch_stage_c(seed: int = 42):
+ _, LTX2Vocoder, LTX2VocoderWithBWE = _require_diffusers()
+
+ torch.manual_seed(seed)
+ full = LTX2VocoderWithBWE(
+ in_channels=128,
+ hidden_channels=_MAIN_VOCODER_CFG["upsample_initial_channel"],
+ out_channels=2,
+ upsample_kernel_sizes=_MAIN_VOCODER_CFG["upsample_kernel_sizes"],
+ upsample_factors=_MAIN_VOCODER_CFG["upsample_rates"],
+ resnet_kernel_sizes=_MAIN_VOCODER_CFG["resblock_kernel_sizes"],
+ resnet_dilations=_MAIN_VOCODER_CFG["resblock_dilation_sizes"],
+ act_fn=_MAIN_VOCODER_CFG["activation"],
+ antialias=True,
+ final_act_fn=None,
+ final_bias=_MAIN_VOCODER_CFG["use_bias_at_final"],
+ bwe_in_channels=128,
+ bwe_hidden_channels=_BWE_VOCODER_CFG["upsample_initial_channel"],
+ bwe_out_channels=2,
+ bwe_upsample_kernel_sizes=_BWE_VOCODER_CFG["upsample_kernel_sizes"],
+ bwe_upsample_factors=_BWE_VOCODER_CFG["upsample_rates"],
+ bwe_resnet_kernel_sizes=_BWE_VOCODER_CFG["resblock_kernel_sizes"],
+ bwe_resnet_dilations=_BWE_VOCODER_CFG["resblock_dilation_sizes"],
+ bwe_act_fn=_BWE_VOCODER_CFG["activation"],
+ bwe_antialias=True,
+ bwe_final_act_fn=None,
+ bwe_final_bias=_BWE_VOCODER_CFG["use_bias_at_final"],
+ filter_length=_MEL_STFT_CFG["filter_length"],
+ hop_length=_MEL_STFT_CFG["hop_length"],
+ window_length=_MEL_STFT_CFG["win_length"],
+ num_mel_channels=_MEL_STFT_CFG["n_mel_channels"],
+ input_sampling_rate=_INPUT_SR,
+ output_sampling_rate=_OUTPUT_SR,
+ )
+ n_freqs = _MEL_STFT_CFG["filter_length"] // 2 + 1
+ with torch.no_grad():
+ full.mel_stft.stft_fn.forward_basis.copy_(torch.randn(n_freqs * 2, 1, _MEL_STFT_CFG["win_length"]) * 0.05)
+ full.mel_stft.stft_fn.inverse_basis.copy_(torch.randn(n_freqs * 2, 1, _MEL_STFT_CFG["win_length"]) * 0.05)
+ full.mel_stft.mel_basis.copy_(torch.randn(_MEL_STFT_CFG["n_mel_channels"], n_freqs).abs() * 0.1)
+ _randomize_snake_alphas(full)
+ return full.eval()
+
+
+def _build_torch_stage_c_real(checkpoint_name: str):
+ """diffusers ``LTX2VocoderWithBWE`` with the *real* checkpoint weights — the torch oracle
+ for the vocoder+BWE. The checkpoint uses the original BigVGAN key names; invert the
+ diffusers→TT rename (see ``_diffusers_vocoder_state_to_tt``) to load them."""
+ from safetensors import safe_open
+
+ full = _build_torch_stage_c(seed=0) # correct architecture; weights overwritten below
+
+ def ckpt_to_diffusers(s: str) -> str:
+ return (
+ s.replace("conv_pre.", "conv_in.")
+ .replace("conv_post.", "conv_out.")
+ .replace("act_post.", "act_out.")
+ .replace("ups.", "upsamplers.")
+ .replace("resblocks.", "resnets.")
+ .replace("downsample.lowpass.filter", "downsample.filter")
+ )
+
+ state: dict[str, torch.Tensor] = {}
+ with safe_open(checkpoint_name, framework="pt") as f:
+ for k in f.keys():
+ if k.startswith("vocoder."):
+ state[ckpt_to_diffusers(k[len("vocoder.") :])] = f.get_tensor(k).float()
+ res = full.load_state_dict(state, strict=False)
+ assert (
+ not res.missing_keys and not res.unexpected_keys
+ ), f"torch oracle weight load mismatch: {len(res.missing_keys)} missing, {len(res.unexpected_keys)} unexpected"
+ return full.eval()
+
+
+def _build_torch_audio_decoder_real(checkpoint_name: str):
+ """diffusers ``AutoencoderKLLTX2Audio`` (mel-VAE decoder) with the *real* checkpoint weights —
+ the host oracle for the mel-VAE stage. Architecture dims are read from the checkpoint's
+ embedded config, the same source ``LTXAudioDecoderAdapter`` uses, so this tracks the
+ real audio VAE rather than the toy component-test config.
+
+ Returns ``(vae, stats_std, stats_mean, z_channels)`` ready for ``_decode_latent_ref``.
+ """
+ import json
+
+ from safetensors import safe_open
+
+ AutoencoderKLLTX2Audio, _, _ = _require_diffusers()
+
+ state: dict[str, torch.Tensor] = {}
+ stats_std = stats_mean = None
+ with safe_open(checkpoint_name, framework="pt") as f:
+ config = json.loads(f.metadata()["config"])
+ ad = config["audio_vae"]["model"]["params"]
+ ddconfig = ad["ddconfig"]
+ stft_cfg = config["audio_vae"].get("preprocessing", {}).get("stft", {})
+ mel_cfg = config["audio_vae"].get("preprocessing", {}).get("mel", {})
+ mel_bins = ddconfig.get("mel_bins") or mel_cfg.get("n_mel_channels")
+ for k in f.keys():
+ if k.startswith("audio_vae.decoder."):
+ state[k[len("audio_vae.") :]] = f.get_tensor(k).float() # -> "decoder.*"
+ elif k == "audio_vae.per_channel_statistics.std-of-means":
+ stats_std = f.get_tensor(k).float()
+ elif k == "audio_vae.per_channel_statistics.mean-of-means":
+ stats_mean = f.get_tensor(k).float()
+
+ z_channels = ddconfig.get("z_channels", 8)
+ vae = AutoencoderKLLTX2Audio(
+ base_channels=ddconfig.get("ch", 128),
+ output_channels=ddconfig.get("out_ch", 2),
+ ch_mult=tuple(ddconfig.get("ch_mult", (1, 2, 4))),
+ num_res_blocks=ddconfig.get("num_res_blocks", 2),
+ attn_resolutions=tuple(ddconfig.get("attn_resolutions", ())),
+ resolution=ddconfig.get("resolution", 256),
+ latent_channels=z_channels,
+ norm_type="pixel",
+ causality_axis="height",
+ mid_block_add_attention=ddconfig.get("mid_block_add_attention", False),
+ sample_rate=ad.get("sampling_rate", 16000),
+ mel_hop_length=stft_cfg.get("hop_length", 160),
+ is_causal=stft_cfg.get("causal", True),
+ mel_bins=mel_bins,
+ ).eval()
+
+ # Decoder-only load: the checkpoint carries no encoder for this path, so encoder keys are
+ # legitimately missing — only decoder keys must be satisfied, with nothing left unexpected.
+ res = vae.load_state_dict(state, strict=False)
+ missing_decoder = [k for k in res.missing_keys if k.startswith("decoder.")]
+ assert not missing_decoder and not res.unexpected_keys, (
+ f"audio decoder oracle weight load mismatch: {len(missing_decoder)} decoder keys missing, "
+ f"{len(res.unexpected_keys)} unexpected"
+ )
+
+ if stats_std is None or stats_mean is None:
+ # Checkpoint without denormalize stats → identity (matches the component-test default).
+ stats_std = torch.ones(z_channels * mel_bins)
+ stats_mean = torch.zeros(z_channels * mel_bins)
+ return vae, stats_std, stats_mean, z_channels
+
+
+def _decode_audio_reference(checkpoint_name: str, audio_latent: torch.Tensor, num_frames: int, fps: float) -> Audio:
+ """Host (torch) reference for the full audio decode chain — mel-VAE → vocoder+BWE — using the
+ real checkpoint weights. Mirrors ``LTXPipeline.decode_audio`` on CPU so the e2e PSNR test can
+ compare the on-device decode against a torch oracle instead of skipping when the pipeline
+ revision doesn't expose its own reference decode.
+
+ Returns an ``Audio(waveform=(C, T), sampling_rate=...)`` matching the device output layout.
+ """
+ vae, stats_std, stats_mean, z = _build_torch_audio_decoder_real(checkpoint_name)
+ vocoder_with_bwe = _build_torch_stage_c_real(checkpoint_name)
+
+ # Unpatchify exactly as decode_audio does: (1, audio_N, 128) -> (1, z, audio_N, 128 // z).
+ audio_N = audio_latent.shape[1]
+ audio_spatial = audio_latent.reshape(1, audio_N, z, audio_latent.shape[2] // z).permute(0, 2, 1, 3).float()
+
+ with torch.no_grad():
+ mel = _decode_latent_ref(vae, audio_spatial, stats_std=stats_std, stats_mean=stats_mean)
+ waveform = vocoder_with_bwe(mel).squeeze(0).float()
+ sampling_rate = _OUTPUT_SR
+
+ # Trim to the clip duration, matching decode_audio.
+ target_samples = int((num_frames / fps) * sampling_rate)
+ if waveform.shape[-1] > target_samples:
+ waveform = waveform[..., :target_samples]
+ return Audio(waveform=waveform, sampling_rate=sampling_rate)
+
+
+def _build_tt_stage_c(
+ mesh_device: ttnn.MeshDevice, *, parallel_config, ccl_manager: CCLManager | None
+) -> VocoderWithBWE:
+ main_voc = Vocoder(
+ mesh_device=mesh_device,
+ dtype=ttnn.float32,
+ in_channels=128,
+ out_channels=2,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ **_tt_vocoder_cfg(_MAIN_VOCODER_CFG),
+ )
+ # Mirror the pipeline (LTXAudioDecoderAdapter): channel-TP the BWE generator only
+ # where the channel axis pays for the gather (factor > 2); single-axis on 2x4 (factor 2).
+ if isinstance(parallel_config, AudioTCParallelConfig):
+ bwe_pc = parallel_config if parallel_config.channel_parallel.factor > 2 else parallel_config.time_parallel
+ else:
+ bwe_pc = parallel_config
+ bwe_voc = Vocoder(
+ mesh_device=mesh_device,
+ dtype=ttnn.float32,
+ in_channels=128,
+ out_channels=2,
+ parallel_config=bwe_pc,
+ ccl_manager=ccl_manager,
+ **_tt_vocoder_cfg(_BWE_VOCODER_CFG),
+ )
+ mel_stft = MelSTFT(mesh_device=mesh_device, dtype=ttnn.float32, **_MEL_STFT_CFG)
+ return VocoderWithBWE(
+ vocoder=main_voc,
+ bwe_generator=bwe_voc,
+ mel_stft=mel_stft,
+ input_sampling_rate=_INPUT_SR,
+ output_sampling_rate=_OUTPUT_SR,
+ hop_length=_HOP_LENGTH,
+ mesh_device=mesh_device,
+ dtype=ttnn.float32,
+ )
+
+
+# ---------------------------------------------------------------------------
+# Component stage tests
+# ---------------------------------------------------------------------------
+@pytest.mark.parametrize(
+ (
+ "mesh_device",
+ "mesh_shape",
+ "sp_axis",
+ "tp_axis",
+ "num_links",
+ "dynamic_load",
+ "device_params",
+ "topology",
+ "is_fsdp",
+ ),
+ LTX_AUDIO_MESH_PARAMS_FULL,
+ indirect=["mesh_device", "device_params"],
+)
+def test_stage_a_audio_decoder(
+ mesh_device: ttnn.MeshDevice,
+ mesh_shape: tuple[int, int],
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ dynamic_load: bool,
+ topology: ttnn.Topology,
+ is_fsdp: bool,
+):
+ AutoencoderKLLTX2Audio, _, _ = _require_diffusers()
+
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ _ = (sp_axis, tp_axis, num_links, dynamic_load, topology, is_fsdp) # parity with fast_av config schema
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ torch.manual_seed(42)
+ ref_vae = AutoencoderKLLTX2Audio(
+ base_channels=_AUDIO_DECODER_CFG["ch"],
+ output_channels=_AUDIO_DECODER_CFG["out_ch"],
+ ch_mult=_AUDIO_DECODER_CFG["ch_mult"],
+ num_res_blocks=_AUDIO_DECODER_CFG["num_res_blocks"],
+ attn_resolutions=_AUDIO_DECODER_CFG["attn_resolutions"],
+ resolution=_AUDIO_DECODER_CFG["resolution"],
+ latent_channels=_AUDIO_DECODER_CFG["z_channels"],
+ norm_type="pixel",
+ causality_axis="height",
+ mid_block_add_attention=_AUDIO_DECODER_CFG["mid_block_add_attention"],
+ sample_rate=_AUDIO_DECODER_CFG["sample_rate"],
+ mel_hop_length=_AUDIO_DECODER_CFG["mel_hop_length"],
+ is_causal=_AUDIO_DECODER_CFG["is_causal"],
+ mel_bins=_AUDIO_DECODER_CFG["mel_bins"],
+ ).eval()
+
+ z_times_f = _AUDIO_DECODER_CFG["z_channels"] * _AUDIO_DECODER_CFG["mel_bins"]
+ stats_std = torch.ones(z_times_f)
+ stats_mean = torch.zeros(z_times_f)
+
+ tt_decoder = MelDecoder(mesh_device=mesh_device, **_AUDIO_DECODER_CFG)
+ tt_decoder.load_torch_state_dict(
+ _audio_decoder_state_from_diffusers(ref_vae, stats_std=stats_std, stats_mean=stats_mean)
+ )
+
+ latent = torch.randn(1, _AUDIO_DECODER_CFG["z_channels"], 64, _AUDIO_DECODER_CFG["mel_bins"], dtype=torch.float32)
+ with torch.no_grad():
+ torch_out = _decode_latent_ref(ref_vae, latent, stats_std=stats_std, stats_mean=stats_mean)
+ tt_out = tt_decoder(latent)
+
+ assert torch_out.shape == tt_out.shape
+ assert_quality(torch_out, tt_out, pcc=0.998, relative_rmse=0.05)
+
+
+@pytest.mark.parametrize(
+ (
+ "mesh_device",
+ "mesh_shape",
+ "sp_axis",
+ "tp_axis",
+ "num_links",
+ "dynamic_load",
+ "device_params",
+ "topology",
+ "is_fsdp",
+ ),
+ LTX_AUDIO_MESH_PARAMS_FULL,
+ indirect=["mesh_device", "device_params"],
+)
+def test_stage_b_vocoder(
+ mesh_device: ttnn.MeshDevice,
+ mesh_shape: tuple[int, int],
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ dynamic_load: bool,
+ topology: ttnn.Topology,
+ is_fsdp: bool,
+):
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ _ = (sp_axis, tp_axis, dynamic_load, is_fsdp) # parity with fast_av config schema
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+ pc = _audio_parallel_config(mesh_shape)
+ ccl = CCLManager(mesh_device, num_links=num_links, topology=ttnn.Topology.Linear) if pc is not None else None
+
+ torch_voc = _build_torch_stage_b(seed=42)
+ tt_voc = _build_tt_stage_b(mesh_device, parallel_config=pc, ccl_manager=ccl)
+ tt_voc.load_torch_state_dict(_diffusers_vocoder_state_to_tt(torch_voc.state_dict()))
+
+ mel = _vocoder_mel()
+ with torch.no_grad():
+ ref_out = torch_voc(mel)
+ tt_out = tt_voc(mel)
+
+ assert tt_out.shape == ref_out.shape
+ assert_quality(ref_out, tt_out, pcc=0.99)
+
+ # When T-sharded, assert the sharded path reproduces the unsharded device path: the t_pad
+ # boundary click is localized to the tail and slips past aggregate PCC, but breaks this.
+ if pc is not None:
+ tt_voc_un = _build_tt_stage_b(mesh_device, parallel_config=None, ccl_manager=None)
+ tt_voc_un.load_torch_state_dict(_diffusers_vocoder_state_to_tt(torch_voc.state_dict()))
+ _assert_sharded_matches_unsharded(tt_voc_un(mel), tt_out, name="stage_b_vocoder")
+
+
+@pytest.mark.parametrize(
+ (
+ "mesh_device",
+ "mesh_shape",
+ "sp_axis",
+ "tp_axis",
+ "num_links",
+ "dynamic_load",
+ "device_params",
+ "topology",
+ "is_fsdp",
+ ),
+ LTX_AUDIO_MESH_PARAMS_FULL,
+ indirect=["mesh_device", "device_params"],
+)
+def test_stage_c_vocoder_with_bwe(
+ mesh_device: ttnn.MeshDevice,
+ mesh_shape: tuple[int, int],
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ dynamic_load: bool,
+ topology: ttnn.Topology,
+ is_fsdp: bool,
+):
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ _ = (sp_axis, tp_axis, dynamic_load, is_fsdp) # parity with fast_av config schema
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+ pc = _audio_parallel_config(mesh_shape)
+ ccl = CCLManager(mesh_device, num_links=num_links, topology=ttnn.Topology.Linear) if pc is not None else None
+
+ torch_full = _build_torch_stage_c(seed=42)
+ tt_full = _build_tt_stage_c(mesh_device, parallel_config=pc, ccl_manager=ccl)
+
+ incompatible = tt_full.load_torch_state_dict(_diffusers_vocoder_with_bwe_state_to_tt(torch_full.state_dict()))
+ assert not incompatible.missing_keys, f"missing keys: {incompatible.missing_keys}"
+
+ mel = _vocoder_mel()
+
+ # Stage B inside Stage C.
+ with torch.no_grad():
+ ref_main = torch_full.vocoder(mel)
+ tt_main = tt_full.vocoder(mel)
+ assert tt_main.shape == ref_main.shape
+ assert_quality(ref_main, tt_main, pcc=0.99)
+ if pc is not None:
+ tt_full_un = _build_tt_stage_c(mesh_device, parallel_config=None, ccl_manager=None)
+ tt_full_un.load_torch_state_dict(_diffusers_vocoder_with_bwe_state_to_tt(torch_full.state_dict()))
+ _assert_sharded_matches_unsharded(tt_full_un.vocoder(mel), tt_main, name="stage_c_vocoder")
+
+ # Log-mel path (apples-to-apples same waveform).
+ remainder = ref_main.shape[-1] % _HOP_LENGTH
+ if remainder:
+ pad_right = _HOP_LENGTH - remainder
+ ref_main = torch.nn.functional.pad(ref_main, (0, pad_right))
+ with torch.no_grad():
+ ref_mel = _compute_mel_ref(torch_full, ref_main)
+ tt_mel = tt_full._compute_mel_device(ref_main)
+ assert tt_mel.shape == ref_mel.shape
+ assert_quality(ref_mel, tt_mel, pcc=0.99)
+
+ # BWE residual and skip-resample paths.
+ ref_mel_for_bwe = ref_mel.transpose(2, 3).contiguous()
+ with torch.no_grad():
+ ref_residual = torch_full.bwe_generator(ref_mel_for_bwe)
+ ref_skip = torch_full.resampler(ref_main)
+ tt_residual = tt_full.bwe_generator(ref_mel_for_bwe)
+ tt_skip = tt_full._resample_device(ref_main)
+ assert tt_residual.shape == ref_residual.shape
+ assert tt_skip.shape == ref_skip.shape
+ assert_quality(ref_residual, tt_residual, pcc=0.98)
+ assert_quality(ref_skip, tt_skip, pcc=0.99)
+
+ # Full Stage C forward: the muxed waveform is where the sharded tail click surfaced.
+ with torch.no_grad():
+ ref_out = torch_full(mel)
+ tt_out = tt_full(mel)
+ assert tt_out.shape == ref_out.shape
+ if pc is not None:
+ _assert_sharded_matches_unsharded(tt_full_un(mel), tt_out, name="stage_c_full")
+
+
+# Localized audio regression: aggregate PCC over a full clip masks a brief burst, so this
+# drives the real ~6s length and gates each 0.5s window vs the unsharded eager baseline.
+_STATIC_WIN_S = 0.5
+
+
+def _window_error_rows(ref: torch.Tensor, tt: torch.Tensor, *, sr: int, win_s: float):
+ """Per-window (start_s, max|Δ|, rmse/σ_ref) over the time axis of (B, C, T)."""
+ T = ref.shape[-1]
+ n = max(1, int(win_s * sr))
+ rows = []
+ for start in range(0, T, n):
+ r = ref[..., start : start + n]
+ t = tt[..., start : start + n]
+ max_abs = (r - t).abs().max().item()
+ rmse = ((r - t) ** 2).mean().sqrt().item()
+ sigma = ((r**2).mean().sqrt().item()) + 1e-9
+ rows.append((start / sr, max_abs, rmse / sigma))
+ return rows
+
+
+@pytest.mark.parametrize(
+ "device_params",
+ [
+ {
+ "l1_small_size": 32768,
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "trace_region_size": 300000000,
+ "require_exact_physical_num_devices": True,
+ }
+ ],
+ indirect=True,
+)
+@pytest.mark.parametrize("mesh_device", [(2, 4)], indirect=True)
+def test_stage_c_audio_decode(mesh_device, device_params):
+ """End-to-end audio synthesis (VocoderWithBWE): channel-TP main vocoder + conv1d depthwise,
+ gated against the unsharded eager baseline per 0.5s window so a localized burst (the demo
+ static) can't hide behind an aggregate PCC. Drives the real ~6s clip length (601 mel
+ frames)."""
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(2, 4))
+ pc = AudioTCParallelConfig(
+ time_parallel=ParallelFactor(factor=4, mesh_axis=1),
+ channel_parallel=ParallelFactor(factor=2, mesh_axis=0),
+ )
+ ccl = CCLManager(mesh_device, num_links=1, topology=ttnn.Topology.Linear)
+
+ torch_full = _build_torch_stage_c(seed=42)
+ tt_full = _build_tt_stage_c(mesh_device, parallel_config=pc, ccl_manager=ccl)
+ tt_full.load_torch_state_dict(_diffusers_vocoder_with_bwe_state_to_tt(torch_full.state_dict()))
+ tt_un = _build_tt_stage_c(mesh_device, parallel_config=None, ccl_manager=None)
+ tt_un.load_torch_state_dict(_diffusers_vocoder_with_bwe_state_to_tt(torch_full.state_dict()))
+
+ torch.manual_seed(7)
+ _tf = int(os.environ.get("AUDIO_T_FRAMES", "601")) # 601 mel frames ≈ the 6s girl clip
+ mel = torch.randn(1, 2, _tf, 64, dtype=torch.float32) * 0.5
+ tt = tt_full(mel)
+ out_un = tt_un(mel)
+
+ sr = tt_full.output_sampling_rate
+ logger.info(f"[audio] output {tuple(tt.shape)} = {tt.shape[-1]/sr:.2f}s @ {sr}Hz")
+ su = _window_error_rows(out_un, tt, sr=sr, win_s=_STATIC_WIN_S)
+ for s, su_max, su_rmse in su:
+ logger.info(f"[audio] t={s:4.2f}s shard-vs-un max|Δ|={su_max:.3e} rmse/σ={su_rmse:.3e}")
+ worst_su_max = max(r[1] for r in su)
+ worst_su_rmse = max(r[2] for r in su)
+ print(
+ f"\nAUDIO_DECODE shard-vs-un worst_max|Δ|={worst_su_max:.3e} worst_rmse/σ={worst_su_rmse:.3e}",
+ flush=True,
+ )
+ # Regression guard: channel-TP audio must match the unsharded eager TT baseline. Catches
+ # broken sharding (the actual risk); torch-vs-TT on the full muxed BWE output is inherently
+ # loose with random weights and is checked per-component in test_stage_c_vocoder_with_bwe.
+ assert worst_su_max < 5e-3, f"audio window shard-vs-un max|Δ| {worst_su_max:.3e} — divergence"
+ assert worst_su_rmse < 1e-2, f"audio window shard-vs-un rmse/σ {worst_su_rmse:.3e} — divergence"
+
+
+# ===========================================================================
+# Tracy per-op device-time profile of the on-device audio decode chain.
+#
+# Goal: emit every op in the audio decode (mel-VAE -> vocoder -> BWE) with its
+# device duration, and have the summed device time land on the ~0.49s traced
+# steady-state audio decode (within a small delta).
+#
+# Why eager (traced=False): each op dispatches as its own program, so (a) the
+# on-device profiler can be flushed per-block to stay under the 125-scope/core
+# buffer, and (b) every op gets a named DEVICE FW / KERNEL DURATION. The device
+# kernel durations are identical to the traced replay — trace only removes host
+# dispatch — so the summed device time reproduces the traced ~0.49s while the
+# host wall-clock (printed) is far larger; that gap is pure host dispatch.
+#
+# Two entry points:
+# * test_audio_decode_profile — the workload (run under `python -m tracy`)
+# * test_audio_decode_perf_table — driver that runs the profiler on the above
+# and prints the per-op table + device total
+#
+# Manual one-liner (equivalent to the driver):
+# LTX_AUDIO_PROF=1 \
+# LTX_CHECKPOINT=~/.cache/ltx-checkpoints/ltx-2.3-22b-distilled-1.1.safetensors \
+# python -m tracy -p -r -v -o generated/profiler/ltx_audio_decode -m \
+# pytest 'models/tt_dit/tests/models/ltx/test_audio_ltx.py::test_audio_decode_profile' -s
+# then sum "DEVICE FW DURATION [ns]" from
+# generated/profiler/ltx_audio_decode/reports//ops_perf_results_.csv
+# ===========================================================================
+
+# The ~0.49s traced steady-state audio decode (4x8 ring, distilled, 145f@24fps),
+# expressed in ms; the summed device time should land within _PROF_TOL_MS of it.
+_PROF_TARGET_MS = 490.0
+_PROF_TOL_MS = 150.0
+
+# Compute-heavy leaf blocks; a ReadDeviceProfiler flush after each keeps every
+# Tracy zone window well under the per-core 125-scope device buffer.
+_PROF_FLUSH_TYPES = (ResnetBlock, AudioUpsample, AMPBlock1, MelSTFT)
+
+
+def _walk_tt(module):
+ """Depth-first walk over a tt_dit Module tree (root included)."""
+ yield module
+ for _, child in module.named_children():
+ yield from _walk_tt(child)
+
+
+def _flush_forward_after(module, mesh_device):
+ """Wrap ``module.forward`` so the on-device profiler is drained after every call,
+ keeping each Tracy zone window under the per-core scope buffer (see gemma harness)."""
+ orig = module.forward
+
+ def timed(*a, _orig=orig, **k):
+ r = _orig(*a, **k)
+ ttnn.ReadDeviceProfiler(mesh_device)
+ return r
+
+ module.forward = timed
+
+
+@pytest.mark.skipif(
+ os.environ.get("LTX_AUDIO_PROF") != "1",
+ reason="Tracy profiling workload — run via test_audio_decode_perf_table, or manually with "
+ "LTX_AUDIO_PROF=1 python -m tracy -p -r -o -m pytest '::test_audio_decode_profile' -s",
+)
+# Profiler-instrumented kernels compile fresh on the first profiling run and the eager decode
+# runs op-by-op, so this comfortably exceeds the default 300s per-test cap.
+@pytest.mark.timeout(3600)
+@pytest.mark.parametrize(
+ "device_params",
+ [
+ {
+ "l1_small_size": 32768,
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D_RING,
+ "trace_region_size": 300_000_000,
+ "require_exact_physical_num_devices": True,
+ }
+ ],
+ indirect=True,
+)
+@pytest.mark.parametrize("mesh_device", [(4, 8)], indirect=True)
+def test_audio_decode_profile(mesh_device, device_params):
+ """Eager on-device audio decode wrapped in Tracy signposts, one profiled iteration.
+
+ Mirrors the 4x8 ring distilled pipeline's audio-decode config (T-shard=8, replicated
+ channel; the pipeline default with LTX_AUDIO_CHANNEL_TP off). Emits ``signpost("start")``
+ / ``signpost("stop")`` around a single decode so ``post_process_ops_log(has_signposts=True)``
+ slices out warmup/compile; per-block ReadDeviceProfiler flushes keep the device buffer live.
+ Pure timing — no PCC (correctness is covered by the sibling e2e/component tests)."""
+ ckpt = _resolve_checkpoint(
+ [f"{_LOCAL_CHECKPOINT_DIR}/{_DISTILLED_CHECKPOINT}"],
+ (_DISTILLED_CHECKPOINT,),
+ )
+ if ckpt is None:
+ pytest.skip("distilled checkpoint not found (set LTX_CHECKPOINT)")
+
+ from tracy import signpost
+
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(4, 8))
+
+ torch.manual_seed(0)
+ # traced=False (LTXPipeline default): decode_audio runs eager, one program per op.
+ pipeline, _ = _build_pipeline(
+ mesh_device,
+ sp_axis=1,
+ tp_axis=0,
+ checkpoint=ckpt,
+ num_links=2,
+ topology=ttnn.Topology.Ring,
+ )
+ assert not pipeline._traced, "profile expects the eager decode path (per-op programs)"
+
+ audio_latent = torch.randn(1, 256, 128, dtype=torch.float32)
+ num_frames, fps = 145, 24.0
+
+ # Warmup: compile kernels + one-time device weight prep, unprofiled. Two iters so the
+ # program cache and any lazy device state are fully warm before the measured pass.
+ for _ in range(2):
+ assert pipeline.decode_audio(audio_latent, num_frames, fps=fps) is not None
+ ttnn.synchronize_device(mesh_device)
+ ttnn.ReadDeviceProfiler(mesh_device) # drain warmup zones (they precede the "start" marker)
+
+ # Install per-block flushes only for the measured pass.
+ for root in (pipeline.tt_mel_decoder, pipeline.tt_vocoder_with_bwe):
+ for mod in _walk_tt(root):
+ if isinstance(mod, _PROF_FLUSH_TYPES):
+ _flush_forward_after(mod, mesh_device)
+
+ # Capture the device-op-id range around the single forward. GLOBAL CALL COUNT in the C++
+ # device digest (cpp_device_perf_report.csv) == this counter, so [op0, op1) slices the digest
+ # to exactly this forward — no dependence on the fragile Tracy -r ops report or signpost rows.
+ from ttnn import _ttnn
+
+ op0 = _ttnn.get_device_operation_id()
+ signpost("start")
+ t0 = time.perf_counter()
+ out = pipeline.decode_audio(audio_latent, num_frames, fps=fps)
+ ttnn.synchronize_device(mesh_device)
+ host_wall_ms = (time.perf_counter() - t0) * 1000.0
+ ttnn.ReadDeviceProfiler(mesh_device) # flush the tail ops while still inside the region
+ signpost("stop")
+ op1 = _ttnn.get_device_operation_id()
+
+ assert out is not None
+ logger.info(f"Audio decode profiled: host_wall={host_wall_ms:.1f}ms wave={tuple(out.waveform.shape)}")
+ print(f"\nAUDIO_DECODE_HOST_WALL_MS={host_wall_ms:.2f}", flush=True)
+ print(f"AUDIO_DECODE_OPID_RANGE={op0},{op1}", flush=True)
+
+
+@pytest.mark.skipif(
+ os.environ.get("LTX_AUDIO_PROF") != "1",
+ reason="Tracy driver — run: LTX_AUDIO_PROF=1 LTX_CHECKPOINT= pytest "
+ "'::test_audio_decode_perf_table' -s",
+)
+@pytest.mark.timeout(3600)
+def test_audio_decode_perf_table():
+ """Profile ``test_audio_decode_profile`` and print the per-op device-time table.
+
+ Spawns ``python -m tracy`` on the workload (opening the device itself; this driver does not
+ take the mesh_device fixture, matching test_ring_joint_sdpa_perf_table), then reads the
+ signpost-sliced ops log. Prints one row per op (device FW/kernel duration) plus the summed
+ device time, and asserts the total lands within ``_PROF_TOL_MS`` of the ~0.49s traced target.
+ """
+ import pandas as pd
+ from tracy.process_model_log import post_process_ops_log, run_device_profiler
+
+ subdir = "ltx_audio_decode"
+ test_id = "models/tt_dit/tests/models/ltx/test_audio_ltx.py::test_audio_decode_profile"
+ run_device_profiler(
+ f"-m 'pytest {test_id} -s'",
+ subdir,
+ check_test_return_code=True,
+ is_command_binary_exe=True,
+ )
+
+ df = post_process_ops_log(subdir, has_signposts=True) # DataFrame sliced between start/stop
+ fw_col, k_col = "DEVICE FW DURATION [ns]", "DEVICE KERNEL DURATION [ns]"
+
+ def _num(series):
+ return pd.to_numeric(series[series != "-"], errors="coerce").fillna(0.0)
+
+ fw_ns = _num(df[fw_col])
+ k_ns = _num(df[k_col])
+ total_fw_ms = fw_ns.sum() / 1e6
+ total_k_ms = k_ns.sum() / 1e6
+
+ # Per-op table, ops in dispatch order.
+ print("\n" + "=" * 78, flush=True)
+ print(f"{'#':>3} {'OP CODE':<34}{'FW [µs]':>12}{'KERNEL [µs]':>14}", flush=True)
+ print("-" * 78, flush=True)
+ codes = df["OP CODE"].tolist()
+ fw_list = _num(df[fw_col]).tolist()
+ k_list = _num(df[k_col]).tolist()
+ for i, (code, fw, kd) in enumerate(zip(codes, fw_list, k_list)):
+ print(f"{i:>3} {str(code):<34}{fw / 1e3:>12.2f}{kd / 1e3:>14.2f}", flush=True)
+ print("-" * 78, flush=True)
+
+ # Per-op-code aggregate (sorted by total FW time).
+ agg = (
+ pd.DataFrame({"code": codes, "fw_ns": fw_list, "k_ns": k_list})
+ .groupby("code")
+ .agg(count=("fw_ns", "size"), fw_us=("fw_ns", lambda s: s.sum() / 1e3), k_us=("k_ns", lambda s: s.sum() / 1e3))
+ .sort_values("fw_us", ascending=False)
+ )
+ print("\nBy op code (FW-descending):", flush=True)
+ print(f"{'OP CODE':<34}{'count':>7}{'FW [µs]':>12}{'KERNEL [µs]':>14}", flush=True)
+ for code, row in agg.iterrows():
+ print(f"{str(code):<34}{int(row['count']):>7}{row['fw_us']:>12.1f}{row['k_us']:>14.1f}", flush=True)
+ print("=" * 78, flush=True)
+
+ print(
+ f"\nAUDIO_DECODE_DEVICE_FW_MS={total_fw_ms:.2f} "
+ f"AUDIO_DECODE_DEVICE_KERNEL_MS={total_k_ms:.2f} "
+ f"(target ~{_PROF_TARGET_MS:.0f}ms, tol ±{_PROF_TOL_MS:.0f}ms, n_ops={len(df)})",
+ flush=True,
+ )
+ logger.info(f"Audio decode device time: FW={total_fw_ms:.1f}ms kernel={total_k_ms:.1f}ms over {len(df)} ops")
+ assert len(df) > 0, "no ops captured between signposts — check flushing / signpost naming"
+ assert abs(total_fw_ms - _PROF_TARGET_MS) <= _PROF_TOL_MS, (
+ f"summed device FW {total_fw_ms:.1f}ms is >{_PROF_TOL_MS:.0f}ms from the ~{_PROF_TARGET_MS:.0f}ms "
+ f"traced target — device-bound audio-decode time drifted"
+ )
diff --git a/code/models/tt_dit/tests/models/ltx/test_embeddings_ltx.py b/code/models/tt_dit/tests/models/ltx/test_embeddings_ltx.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c0cd747928443de4d5dfc65cc3f1844c8cd28e5
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_embeddings_ltx.py
@@ -0,0 +1,74 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.tt_dit.layers.embeddings import LTXAdaLayerNormSingle
+from models.tt_dit.utils.check import assert_quality
+
+
+@pytest.mark.parametrize(
+ "mesh_device",
+ [(1, 1)],
+ ids=["1x1"],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize("embedding_dim", [4096], ids=["dim4096"])
+def test_ltx_adaln_single(mesh_device: ttnn.MeshDevice, embedding_dim: int):
+ """
+ Test LTXAdaLayerNormSingle: compare TT output vs diffusers LTX2AdaLayerNormSingle.
+ """
+ from diffusers.models.transformers.transformer_ltx2 import LTX2AdaLayerNormSingle as TorchAdaLayerNormSingle
+
+ B = 2
+
+ # Create PyTorch reference model
+ torch_model = TorchAdaLayerNormSingle(
+ embedding_dim=embedding_dim, num_mod_params=6, use_additional_conditions=False
+ )
+ torch_model.eval()
+ torch_state = torch_model.state_dict()
+ logger.info(f"PyTorch AdaLayerNormSingle state keys: {list(torch_state.keys())}")
+
+ # Create TT model and load weights
+ tt_model = LTXAdaLayerNormSingle(
+ embedding_dim=embedding_dim,
+ embedding_coefficient=6,
+ mesh_device=mesh_device,
+ dtype=ttnn.float32,
+ )
+ tt_model.load_torch_state_dict(torch_state)
+
+ # Create timestep input
+ torch.manual_seed(42)
+ timestep = torch.rand(B) * 1000 # random timesteps in [0, 1000)
+
+ # PyTorch reference forward
+ with torch.no_grad():
+ torch_modulation, torch_embedded = torch_model(timestep)
+ logger.info(f"PyTorch modulation shape: {torch_modulation.shape}, embedded: {torch_embedded.shape}")
+
+ # TT forward
+ # Timesteps module expects (1, 1, B, 1) so B broadcasts with the 128-dim factor.
+ # Use float32 for timestep input — sinusoidal embeddings are precision-sensitive.
+ tt_timestep = ttnn.from_torch(
+ timestep.reshape(1, 1, B, 1),
+ device=mesh_device,
+ layout=ttnn.TILE_LAYOUT,
+ dtype=ttnn.float32,
+ )
+ tt_modulation, tt_embedded = tt_model(tt_timestep)
+
+ # Convert back and compare — output is (1, 1, B, dim) in ttnn
+ tt_modulation_torch = ttnn.to_torch(tt_modulation).squeeze(0).squeeze(0)
+ tt_embedded_torch = ttnn.to_torch(tt_embedded).squeeze(0).squeeze(0)
+
+ logger.info(f"TT modulation shape: {tt_modulation_torch.shape}, embedded: {tt_embedded_torch.shape}")
+
+ assert_quality(torch_modulation, tt_modulation_torch, pcc=0.999)
+ assert_quality(torch_embedded, tt_embedded_torch, pcc=0.999)
+ logger.info("PASSED: LTXAdaLayerNormSingle matches PyTorch reference")
diff --git a/code/models/tt_dit/tests/models/ltx/test_latent_upsampler.py b/code/models/tt_dit/tests/models/ltx/test_latent_upsampler.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e14605dd16b4e28748a69248e1c372cd64a9664
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_latent_upsampler.py
@@ -0,0 +1,256 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""PCC tests for ``LTXLatentUpsampler`` vs the diffusers ``LTX2LatentUpsamplerModel`` reference.
+
+All shapes match the production LTX-2 Fast pipeline (mid_channels=1024, num_frames=19,
+2x4 BH mesh, half-res latent 17x30 / 18x32)."""
+
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.tt_dit.models.upsampler.latent_upsampler_ltx import LTXLatentUpsampler, LTXUpsamplerResBlock
+from models.tt_dit.parallel.config import ParallelFactor, VaeHWParallelConfig
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.conv3d import ConvDims, conv_pad_height, conv_pad_width
+from models.tt_dit.utils.tensor import fast_device_to_host, typed_tensor_2dshard
+from models.tt_dit.utils.test import skip_if_unsupported_num_links
+
+from .ltx_mesh_params import LTX_UPSAMPLER_MESH_PARAMS
+
+
+def _parallel_kwargs(
+ mesh_device: ttnn.MeshDevice, h_axis: int, w_axis: int, num_links: int, topology: ttnn.Topology
+) -> dict:
+ mesh_shape = tuple(mesh_device.shape)
+ parallel_config = VaeHWParallelConfig(
+ height_parallel=ParallelFactor(factor=mesh_shape[h_axis], mesh_axis=h_axis),
+ width_parallel=ParallelFactor(factor=mesh_shape[w_axis], mesh_axis=w_axis),
+ )
+ ccl_manager = CCLManager(mesh_device, num_links=num_links, topology=topology)
+ return {"parallel_config": parallel_config, "ccl_manager": ccl_manager}
+
+
+@pytest.mark.parametrize(
+ "H, W",
+ [(17, 30), (18, 32)],
+ ids=["nondiv_17x30", "div_18x32"],
+)
+@pytest.mark.parametrize(
+ ("mesh_device", "h_axis", "w_axis", "num_links", "device_params", "topology"),
+ LTX_UPSAMPLER_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_upsampler_resblock(
+ mesh_device: ttnn.MeshDevice, h_axis: int, w_axis: int, num_links: int, topology: ttnn.Topology, H: int, W: int
+):
+ """Single LTXUpsamplerResBlock at the production shape (channels=1024, 2x4 sharded,
+ T=19). ``nondiv_17x30`` exercises the sharded pad/mask/crop path; ``div_18x32`` is
+ the no-mask control."""
+ from diffusers.pipelines.ltx2.latent_upsampler import ResBlock as TorchResBlock
+
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ channels, T, B = 1024, 19, 1
+ torch.manual_seed(0xABBA)
+ pk = _parallel_kwargs(mesh_device, h_axis, w_axis, num_links, topology)
+ pc = pk["parallel_config"]
+ hf, wf = pc.height_parallel.factor, pc.width_parallel.factor
+ padded_h = ((H + hf - 1) // hf) * hf
+ padded_w = ((W + wf - 1) // wf) * wf
+
+ torch_block = TorchResBlock(channels=channels, dims=3)
+ torch_block.eval()
+
+ tt_block = LTXUpsamplerResBlock(
+ channels=channels,
+ gn_input_nhw=T * H * W,
+ mesh_device=mesh_device,
+ conv_dims=ConvDims(T=T + 2, H=padded_h // hf, W=padded_w // wf),
+ **pk,
+ )
+ tt_block.load_torch_state_dict(torch_block.state_dict())
+
+ x = torch.randn(B, channels, T, H, W, dtype=torch.float32)
+ with torch.no_grad():
+ torch_out = torch_block(x) # (B, C, T, H, W)
+
+ x_bthwc = x.permute(0, 2, 3, 4, 1).contiguous() # (B, T, H, W, C)
+ x_bthwc, logical_h = conv_pad_height(x_bthwc, hf)
+ x_bthwc, logical_w = conv_pad_width(x_bthwc, wf)
+ x_tt = typed_tensor_2dshard(
+ x_bthwc,
+ mesh_device,
+ shard_mapping={pc.height_parallel.mesh_axis: 2, pc.width_parallel.mesh_axis: 3},
+ layout=ttnn.ROW_MAJOR_LAYOUT,
+ dtype=ttnn.bfloat16,
+ )
+
+ out = tt_block(x_tt, logical_h, logical_w)
+
+ concat_dims = [None, None]
+ concat_dims[pc.height_parallel.mesh_axis] = 2
+ concat_dims[pc.width_parallel.mesh_axis] = 3
+ out_torch = fast_device_to_host(out, mesh_device, concat_dims, ccl_manager=pk["ccl_manager"])
+ out_torch = out_torch[:, :, :logical_h, :logical_w, :channels].permute(0, 4, 1, 2, 3)
+
+ assert out_torch.shape == torch_out.shape, f"shape mismatch: TT {out_torch.shape} vs torch {torch_out.shape}"
+ assert_quality(torch_out, out_torch, pcc=0.99)
+ logger.info(f"PASSED: LTXUpsamplerResBlock(1024 ch, {T}x{H}x{W}, 2x4) matches reference")
+
+
+@pytest.mark.parametrize(
+ "H, W",
+ [(17, 30), (18, 32)],
+ ids=["nondiv_17x30", "div_18x32"],
+)
+@pytest.mark.parametrize(
+ ("mesh_device", "h_axis", "w_axis", "num_links", "device_params", "topology"),
+ LTX_UPSAMPLER_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_latent_upsampler(
+ mesh_device: ttnn.MeshDevice, h_axis: int, w_axis: int, num_links: int, topology: ttnn.Topology, H: int, W: int
+):
+ """Full ``LTXLatentUpsampler`` PCC vs reference, random weights at production shape
+ (in=128, mid=1024, T=19, 2x4). No HF checkpoint required."""
+ from diffusers.pipelines.ltx2.latent_upsampler import LTX2LatentUpsamplerModel
+
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ in_c, mid_c, T, B = 128, 1024, 19, 1
+ torch.manual_seed(0xC0FFEE)
+
+ torch_model = LTX2LatentUpsamplerModel(
+ in_channels=in_c,
+ mid_channels=mid_c,
+ num_blocks_per_stage=4,
+ dims=3,
+ spatial_upsample=True,
+ temporal_upsample=False,
+ rational_spatial_scale=2.0,
+ use_rational_resampler=False,
+ )
+ torch_model.eval()
+
+ tt_model = LTXLatentUpsampler(
+ input_hw=(H, W),
+ in_channels=in_c,
+ mid_channels=mid_c,
+ num_blocks_per_stage=4,
+ spatial_upsample=True,
+ temporal_upsample=False,
+ spatial_scale=2.0,
+ rational_resampler=False,
+ mesh_device=mesh_device,
+ num_frames=T,
+ **_parallel_kwargs(mesh_device, h_axis, w_axis, num_links, topology),
+ )
+ tt_model.load_torch_state_dict(torch_model.state_dict())
+
+ latent = torch.randn(B, in_c, T, H, W, dtype=torch.float32)
+
+ with torch.no_grad():
+ torch_out = torch_model(latent)
+ tt_out = tt_model(latent)
+
+ assert tt_out.shape == torch_out.shape, f"shape mismatch: TT {tt_out.shape} vs torch {torch_out.shape}"
+ assert_quality(torch_out, tt_out, pcc=0.99)
+ logger.info(f"PASSED: LTXLatentUpsampler({in_c}->{mid_c}, {T}x{H}x{W} → {T}x{H * 2}x{W * 2}) matches reference")
+
+
+def _resolve_upsampler_checkpoint() -> str | None:
+ """Resolve the real spatial-upscaler safetensors (local HF cache, no download)."""
+ from huggingface_hub import hf_hub_download
+ from huggingface_hub.utils import LocalEntryNotFoundError
+
+ repo_id, filename = "Lightricks/LTX-2.3", "ltx-2.3-spatial-upscaler-x2-1.1.safetensors"
+ try:
+ return hf_hub_download(repo_id, filename, local_files_only=True)
+ except (LocalEntryNotFoundError, Exception): # noqa: BLE001
+ return None
+
+
+@pytest.mark.parametrize(
+ "T, H, W",
+ [(19, 17, 30), (19, 18, 32)],
+ ids=["nondiv_17x30", "div_18x32"],
+)
+@pytest.mark.parametrize(
+ ("mesh_device", "h_axis", "w_axis", "num_links", "device_params", "topology"),
+ LTX_UPSAMPLER_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_latent_upsampler_real_checkpoint(
+ mesh_device: ttnn.MeshDevice,
+ h_axis: int,
+ w_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+ T: int,
+ H: int,
+ W: int,
+):
+ """PCC vs reference with the real ``ltx-2.3-spatial-upscaler-x2`` weights (mid_channels=1024)
+ on the production 2x4 mesh. Input is a smooth, per-channel-scaled latent: white noise
+ is unrepresentative and lower-bounds bf16 conv PCC through the 8-block residual chain."""
+ import json
+
+ from diffusers.pipelines.ltx2.latent_upsampler import LTX2LatentUpsamplerModel
+ from safetensors import safe_open
+ from safetensors.torch import load_file
+
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ ckpt = _resolve_upsampler_checkpoint()
+ if ckpt is None:
+ pytest.skip("real upsampler checkpoint not in local HF cache")
+
+ with safe_open(ckpt, framework="pt") as f:
+ cfg = json.loads(f.metadata()["config"])
+ in_c, mid_c = cfg["in_channels"], cfg["mid_channels"]
+ n_blocks = cfg["num_blocks_per_stage"]
+ B = 1
+ torch.manual_seed(0xC0FFEE)
+
+ torch_model = LTX2LatentUpsamplerModel(
+ in_channels=in_c,
+ mid_channels=mid_c,
+ num_blocks_per_stage=n_blocks,
+ dims=3,
+ spatial_upsample=True,
+ temporal_upsample=False,
+ rational_spatial_scale=2.0,
+ use_rational_resampler=False,
+ )
+ torch_model.load_state_dict(load_file(ckpt))
+ torch_model.eval()
+
+ tt_model = LTXLatentUpsampler(
+ input_hw=(H, W),
+ in_channels=in_c,
+ mid_channels=mid_c,
+ num_blocks_per_stage=n_blocks,
+ spatial_upsample=True,
+ temporal_upsample=False,
+ spatial_scale=2.0,
+ rational_resampler=False,
+ mesh_device=mesh_device,
+ num_frames=T,
+ **_parallel_kwargs(mesh_device, h_axis, w_axis, num_links, topology),
+ )
+ tt_model.load_torch_state_dict(torch_model.state_dict())
+
+ # Smooth, spatially-correlated latent with per-channel scale (mimics real VAE latents).
+ noise = torch.randn(B, in_c, T, H, W, dtype=torch.float32)
+ latent = torch.nn.functional.avg_pool3d(noise, kernel_size=(1, 3, 3), stride=1, padding=(0, 1, 1))
+ latent = latent * (torch.rand(in_c) * 4 + 0.5).view(1, in_c, 1, 1, 1)
+
+ with torch.no_grad():
+ torch_out = torch_model(latent)
+ tt_out = tt_model(latent)
+
+ assert tt_out.shape == torch_out.shape, f"shape mismatch: TT {tt_out.shape} vs torch {torch_out.shape}"
+ assert_quality(torch_out, tt_out, pcc=0.99)
+ logger.info(f"PASSED: real-checkpoint LTXLatentUpsampler({in_c}->{mid_c}, {n_blocks} blocks) matches reference")
diff --git a/code/models/tt_dit/tests/models/ltx/test_ltx_adapter_loader.py b/code/models/tt_dit/tests/models/ltx/test_ltx_adapter_loader.py
new file mode 100644
index 0000000000000000000000000000000000000000..7321c3dedc4bdd9eeca800b9e0725c1511f133fb
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_ltx_adapter_loader.py
@@ -0,0 +1,39 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+"""Host-only unit tests for the LTX LoRA adapter loader's module resolution.
+
+No device, no checkpoint: these pin the loader's warn-and-skip contract for
+targets that do not exist on the transformer variant being loaded. A LoRA file
+authored against a deeper variant references block indices past this variant's
+depth; resolution must return "absent" so the caller skips, not raise IndexError.
+"""
+
+from types import SimpleNamespace
+
+from models.tt_dit.experimental.lora.ltx_adapter_loader import _get_attn, _resolve_singleton
+
+
+def _fake_transformer(num_blocks: int):
+ """A transformer stub with ``num_blocks`` blocks, each carrying an ``attn1``."""
+ blocks = [SimpleNamespace(attn1=SimpleNamespace(name=f"attn1_{i}")) for i in range(num_blocks)]
+ return SimpleNamespace(transformer_blocks=blocks)
+
+
+def test_get_attn_out_of_range_returns_none():
+ tf = _fake_transformer(2)
+ assert _get_attn(tf, 5, "attn1") is None
+ assert _get_attn(tf, -1, "attn1") is None
+
+
+def test_get_attn_in_range_resolves():
+ tf = _fake_transformer(2)
+ assert _get_attn(tf, 1, "attn1") is tf.transformer_blocks[1].attn1
+ # Missing attribute on an in-range block still degrades to None, not AttributeError.
+ assert _get_attn(tf, 1, "attn2") is None
+
+
+def test_resolve_singleton_out_of_range_returns_none_triple():
+ tf = _fake_transformer(2)
+ assert _resolve_singleton(tf, 9, "attn1", "to_out") == (None, None, None)
+ assert _resolve_singleton(tf, -1, "attn1", "ff1") == (None, None, None)
diff --git a/code/models/tt_dit/tests/models/ltx/test_ltx_lora_ondevice.py b/code/models/tt_dit/tests/models/ltx/test_ltx_lora_ondevice.py
new file mode 100644
index 0000000000000000000000000000000000000000..1099c141cf6ca78ae25fd1a601421aeeea50569e
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_ltx_lora_ondevice.py
@@ -0,0 +1,176 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+# SPDX-License-Identifier: Apache-2.0
+
+"""On-device fuse-mode LoRA swap for the LTX-2.3 pipeline.
+
+Unlike the host-fuse path (``fuse_loras_into`` + full weight reload), swapping here is a
+``bind_active`` on each LoRA Linear: ``weight.data += scale * A@B`` on device, no reload.
+
+Correctness anchor: the host-layout algebra (loader A/B == ``fuse_loras_into``) is already
+proven CPU-side. This test closes the remaining gap — that ``LoRAMixin._apply_delta`` applies
+that delta correctly on the *sharded device* weight — by comparing the on-device fused weight
+against a host recompute of the same registered A/B.
+
+The device cases need the real 22B checkpoint + a LoRA file + a mesh; they mirror the opt-in
+style of ``test_ltx_lora_runtime.py``. Set ``RUN_LORA_GEN=1`` for an end-to-end base/lora/unloaded
+generate A/B.
+"""
+
+import os
+
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.common.utility_functions import comp_pcc
+from models.tt_dit.experimental.lora.ltx_adapter_loader import iter_lora_modules
+from models.tt_dit.pipelines.ltx.pipeline_ltx import LTXPipeline
+from models.tt_dit.utils import tensor
+from models.tt_dit.utils.test import line_params
+
+
+def _default_checkpoint() -> str:
+ explicit = os.environ.get("LTX_CHECKPOINT")
+ if explicit:
+ return explicit
+ local = os.path.expanduser("~/.cache/ltx-checkpoints/ltx-2.3-22b-dev.safetensors")
+ if os.path.exists(local):
+ return local
+ return "Lightricks/LTX-2.3:ltx-2.3-22b-dev.safetensors"
+
+
+def _default_lora() -> str:
+ from models.tt_dit.utils.ltx_lora_asset import resolve_lora
+
+ return resolve_lora()
+
+
+def _read_weight(param) -> torch.Tensor:
+ """Reconstruct a sharded Parameter's full weight on host."""
+ return tensor.to_torch(param.data, mesh_axes=param.mesh_axes)
+
+
+def _first_qkv_lora_module(transformer):
+ """First self-attn to_qkv LoRA module (path, module) — the fused/permuted/interleaved
+ path, i.e. the one most sensitive to a layout bug."""
+ for path, mod in iter_lora_modules(transformer):
+ if path.endswith("attn1.to_qkv"):
+ return path, mod
+ return None, None
+
+
+@pytest.mark.timeout(3600)
+@pytest.mark.parametrize(
+ "mesh_device, mesh_shape, sp_axis, tp_axis, num_links, dynamic_load, device_params, topology, is_fsdp",
+ [
+ [(2, 2), (2, 2), 0, 1, 2, False, line_params, ttnn.Topology.Linear, True],
+ [(4, 8), (4, 8), 1, 0, 2, False, line_params, ttnn.Topology.Linear, False],
+ ],
+ ids=["2x2sp0tp1", "bh_4x8sp1tp0"],
+ indirect=["mesh_device", "device_params"],
+)
+def test_ondevice_lora_bind_matches_host_delta(
+ mesh_device,
+ mesh_shape,
+ sp_axis,
+ tp_axis,
+ num_links,
+ dynamic_load,
+ topology,
+ is_fsdp,
+):
+ """bind_active changes the weight to match base + host(scale*A@B); unbind restores the base
+ (within bf16 add/subtract drift); the transformer Module is reused throughout."""
+ ckpt = _default_checkpoint()
+ lora = _default_lora()
+ strength = float(os.environ.get("LORA_STRENGTH", "1.0"))
+
+ parent_mesh = mesh_device
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ pipeline = LTXPipeline.create_pipeline(
+ mesh_device=mesh_device,
+ checkpoint_name=ckpt,
+ gemma_path=None,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ dynamic_load=dynamic_load,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ mode="av",
+ num_frames=int(os.environ.get("NUM_FRAMES", "25")),
+ height=int(os.environ.get("HEIGHT", "256")),
+ width=int(os.environ.get("WIDTH", "256")),
+ lora_enabled=True,
+ )
+
+ if int(ttnn.distributed_context_get_rank()) != 0:
+ logger.info("Skipping assertions on non-zero rank")
+ return
+
+ transformer_id = id(pipeline.transformer)
+ assert pipeline._active_lora is None, "fresh pipeline must have no active LoRA"
+
+ path, module = _first_qkv_lora_module(pipeline.transformer)
+ assert module is not None, "no attn1.to_qkv LoRA module found — is lora_enabled wired?"
+ base_w = _read_weight(module.weight).clone()
+
+ # --- bind (on-device fuse) ---------------------------------------------
+ pipeline.load_lora_weights(lora, strength=strength)
+ assert id(pipeline.transformer) == transformer_id, "model object was rebuilt, not reused"
+ assert module.is_lora_active, f"{path} was not bound"
+ fused_w = _read_weight(module.weight)
+ assert not torch.allclose(fused_w, base_w), "bind did not change the weight"
+
+ # host recompute of the SAME registered delta; scale folded in exactly as _apply_delta does.
+ adapter = module.lora_bank[module.active_idx]
+ delta_oi = module.active_scale * (adapter.B.to(torch.float32) @ adapter.A.to(torch.float32)) # [out,in]
+ delta = delta_oi if delta_oi.shape == base_w.shape else delta_oi.T
+ assert delta.shape == base_w.shape, f"delta {tuple(delta.shape)} vs weight {tuple(base_w.shape)}"
+ expected = base_w.to(torch.float32) + delta
+
+ ok, pcc = comp_pcc(expected, fused_w.to(torch.float32), pcc=0.999)
+ logger.info(f"[{path}] on-device bind vs host delta: {pcc}")
+ assert ok, f"on-device fused weight disagrees with host delta recompute: {pcc}"
+
+ # --- unbind ------------------------------------------------------------
+ pipeline.unload_lora_weights()
+ assert pipeline._active_lora is None
+ assert id(pipeline.transformer) == transformer_id, "model object was rebuilt, not reused"
+ restored_w = _read_weight(module.weight)
+ # fuse-mode unbind subtracts in bf16 — close, not bit-exact (see lora.py drift note).
+ assert torch.allclose(
+ restored_w.to(torch.float32), base_w.to(torch.float32), atol=1e-2
+ ), "unbind did not restore the base weight within bf16 drift"
+ logger.info("On-device LoRA bind/unbind weight checks passed.")
+
+ if os.environ.get("RUN_LORA_GEN", "0") in ("1", "true", "True"):
+ steps = int(os.environ.get("STEPS", "6"))
+ num_frames = int(os.environ.get("NUM_FRAMES", "25"))
+ height = int(os.environ.get("HEIGHT", "256"))
+ width = int(os.environ.get("WIDTH", "256"))
+ seq = pipeline.gemma_encoder_pair.sequence_length
+ v_p = torch.zeros(1, seq, pipeline.gemma_encoder_pair.video_dim)
+ a_p = torch.zeros(1, seq, pipeline.gemma_encoder_pair.audio_dim)
+ for tag, spec in (("base", None), ("lora", lora), ("unloaded", None)):
+ if spec is not None:
+ pipeline.load_lora_weights(spec, strength=strength)
+ else:
+ pipeline.unload_lora_weights()
+ pipeline._prepare_transformer(0)
+ v_lat, a_lat = pipeline.call_av(
+ video_prompt_embeds=v_p,
+ audio_prompt_embeds=a_p,
+ neg_video_prompt_embeds=v_p,
+ neg_audio_prompt_embeds=a_p,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ num_inference_steps=steps,
+ seed=0,
+ ge_gamma=0.0,
+ )
+ logger.info(f"[{tag}] generate ran; video latent {tuple(v_lat.shape)}")
+ assert id(pipeline.transformer) == transformer_id, "model object was rebuilt during generation"
diff --git a/code/models/tt_dit/tests/models/ltx/test_pipeline_ltx_distilled.py b/code/models/tt_dit/tests/models/ltx/test_pipeline_ltx_distilled.py
new file mode 100644
index 0000000000000000000000000000000000000000..b2602d45116bbe31112bc5fa6d00f3568d845b10
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_pipeline_ltx_distilled.py
@@ -0,0 +1,757 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+import functools
+import itertools
+import math
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+
+import numpy as np
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.tt_dit.models.audio_vae.audio_decoder_ltx import LTXAudioDecoderAdapter
+from models.tt_dit.pipelines.ltx.pipeline_ltx_distilled import LTXDistilledPipeline
+from models.tt_dit.utils.ltx import (
+ DEFAULT_LTX_PROMPT,
+ STEADY_STATE_LTX_PROMPT,
+ STEADY_STATE_REPLAY_LTX_PROMPT,
+ default_ltx_checkpoint,
+ default_ltx_gemma,
+ print_ltx_timing_table,
+ traced_default,
+)
+from models.tt_dit.utils.patchifiers import AudioLatentShape, VideoPixelShape
+from models.tt_dit.utils.test import skip_if_unsupported_num_links
+from models.tt_dit.utils.vbench import assert_vbench_quality
+
+from .ltx_mesh_params import (
+ LTX_DISTILLED_AUDIO_MESH_PARAMS_DL,
+ LTX_DISTILLED_I2V_MESH_PARAMS_DL,
+ LTX_DISTILLED_MESH_PARAMS_DL,
+)
+
+
+def _ltx_checkpoint_cached(filename: str) -> bool:
+ """True if the LTX checkpoint is available -- either a real local file (LTX_CHECKPOINT / flat
+ ~/.cache/ltx-checkpoints) OR already in the HF hub cache. The old guard checked
+ os.path.exists(default_ltx_checkpoint(...)), but when the checkpoint resolves from the hub that
+ returns a "repo:file" string (never a path), so it always skipped in CI even with the weights
+ cached. Checking the hub cache too (local_files_only) lets the offline CI run actually execute."""
+ ref = default_ltx_checkpoint(filename)
+ if os.path.exists(ref):
+ return True
+ if ":" in ref: # "repo_id:filename" -> resolvable from the local hub cache without a network call
+ repo_id, fname = ref.split(":", 1)
+ from huggingface_hub import hf_hub_download
+ from huggingface_hub.utils import EntryNotFoundError, LocalEntryNotFoundError
+
+ try:
+ hf_hub_download(repo_id=repo_id, filename=fname, local_files_only=True)
+ return True
+ except (LocalEntryNotFoundError, EntryNotFoundError, FileNotFoundError):
+ return False
+ return False
+
+
+# Default-off: full AV gen needs the real LTX checkpoint + Gemma, so it skips without one.
+@pytest.mark.skipif(
+ not _ltx_checkpoint_cached("ltx-2.3-22b-distilled-1.1.safetensors"),
+ reason="needs the LTX checkpoint (set LTX_CHECKPOINT to a local .safetensors)",
+)
+@pytest.mark.parametrize(
+ "no_prompt",
+ [{"1": True, "0": False}.get(os.environ.get("NO_PROMPT"), True)],
+)
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis, num_links, device_params, topology, is_fsdp, dynamic_load",
+ LTX_DISTILLED_MESH_PARAMS_DL,
+ indirect=["mesh_device", "device_params"],
+)
+def test_pipeline_distilled(
+ mesh_device,
+ device_params,
+ sp_axis,
+ tp_axis,
+ num_links,
+ dynamic_load,
+ topology,
+ is_fsdp,
+ no_prompt,
+):
+ """LTX-2.3 distilled 2-stage AV pipeline."""
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ ckpt = default_ltx_checkpoint("ltx-2.3-22b-distilled-1.1.safetensors")
+ gemma = default_ltx_gemma()
+
+ parent_mesh = mesh_device
+ mesh_shape = tuple(parent_mesh.shape)
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ # Stage-2 output target: 1080p @ 24fps, ~6s. Constraints from the pipeline:
+ # - height/width must be divisible by 64 (asserted at the top of generate(),
+ # so half-res stage-1 is divisible by 32 for the VAE).
+ # 1920 is already % 64; 1080 is not → round up to 1088 (next multiple of 64).
+ # If you need exactly 1080 lines, post-crop the 8 extra rows.
+ # - num_frames must satisfy (num_frames - 1) % 8 == 0 so the VAE decoder maps
+ # latent_frames → num_frames frames exactly. 144 (== 6s × 24fps) doesn't
+ # satisfy this; 145 does (19 latent frames → 145 decoded frames ≈ 6.04s).
+ num_frames = int(os.environ.get("NUM_FRAMES", "145"))
+ height = int(os.environ.get("HEIGHT", "1088"))
+ width = int(os.environ.get("WIDTH", "1920"))
+
+ run_warmup = os.environ.get("RUN_WARMUP", "0") in ("1", "true", "True")
+ # Traced by default wherever the mesh param reserves a trace region; LTX_TRACED=0/1 overrides
+ # (rule + rationale: utils.ltx.traced_default, unit-tested in tests/unit/test_ltx_traced_default.py).
+ traced = traced_default(device_params, os.environ.get("LTX_TRACED"))
+
+ # Conditioning image (I2V). Its mere presence drives image_conditioning: with a path the
+ # transformer builds the per-token video-timestep (I2V) modulation; without one pure T2V keeps
+ # the fast scalar-AdaLN path. Resolved before create_pipeline so the bool can gate the build.
+ image_path = os.environ.get("LTX_I2V_IMAGE")
+ images = None
+ if image_path:
+ if not os.path.exists(image_path):
+ pytest.skip(f"LTX_I2V_IMAGE set but file not found: {image_path}")
+ strength = float(os.environ.get("LTX_I2V_STRENGTH", "1.0"))
+ images = [(image_path, 0, strength)]
+
+ pipeline = LTXDistilledPipeline.create_pipeline(
+ mesh_device=mesh_device,
+ checkpoint_name=ckpt,
+ gemma_path=gemma,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ dynamic_load=dynamic_load,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ run_warmup=run_warmup,
+ traced=traced,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ image_conditioning=bool(image_path),
+ )
+
+ prompt = os.environ.get("PROMPT", DEFAULT_LTX_PROMPT)
+ # Gen #1 is the steady-state measurement, and on console every request encodes a prompt the
+ # cache has never seen — so it gets its own prompt to keep the encoder in the measured path.
+ # Under dynamic_load the encoder is coresident-excluded with the DiT: a second encode would
+ # evict the DiT and clobber the captured traces, so that path reuses the cached embedding.
+ steady_state_prompt = prompt if dynamic_load else os.environ.get("PROMPT_STEADY_STATE", STEADY_STATE_LTX_PROMPT)
+ replay_prompt = prompt if dynamic_load else STEADY_STATE_REPLAY_LTX_PROMPT
+
+ def run(*, prompt, number, seed):
+ output_filename = os.environ.get("OUTPUT_PATH", f"ltx_av_fast_{width}x{height}_{number}.mp4")
+ logger.info(f"Running LTX AV Fast: '{prompt[:80]}...'")
+ logger.info(f"Config: {height}x{width}, {num_frames} frames")
+ if images:
+ logger.info(f"I2V: conditioning image {images[0][0]} (strength={images[0][2]})")
+
+ if int(ttnn.distributed_context_get_rank()) != 0:
+ logger.info(f"Skipping generation on rank {ttnn.distributed_context_get_rank()}")
+ return
+
+ pipeline.generate(
+ prompt,
+ output_path=output_filename,
+ images=images,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ seed=seed,
+ )
+ logger.info(f"Saved video to: {output_filename}")
+ print_ltx_timing_table(
+ pipeline,
+ label="LTX DISTILLED",
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ mesh_shape=mesh_shape,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ topology=topology,
+ output_path=output_filename,
+ prompt=prompt,
+ )
+
+ vbench_thresholds_by_height = {
+ 1088: {
+ "subject_consistency": 0.92,
+ "background_consistency": 0.93,
+ "motion_smoothness": 0.955,
+ # Averaged over VBENCH_SEEDS clips: dynamic_degree is near-binary per clip and this content
+ # sits at ~0.8-1.0, so the floor requires ~4/5 seeds dynamic (1.0 would demand every seed).
+ "dynamic_degree": 0.8,
+ "imaging_quality": 0.645,
+ },
+ }
+
+ # RUN_VBENCH=0 skips the quality gate (e.g. perf-only iteration); defaults on so CI gates.
+ run_vbench = os.environ.get("RUN_VBENCH", "1") in ("1", "true", "True")
+ # RUN_CLIP=0 skips the CLIP prompt-alignment gate; defaults on (mirrors the wan2.2 test).
+ run_clip = os.environ.get("RUN_CLIP", "1") in ("1", "true", "True")
+
+ export_dir = os.environ.get("VBENCH_EXPORT_DIR")
+ if export_dir and not run_vbench:
+ raise ValueError("VBENCH_EXPORT_DIR requires RUN_VBENCH=1")
+ # Missing dependencies must fail an enabled gate, including in CI.
+ if run_vbench and not export_dir:
+ __import__("vbench")
+ if run_clip:
+ __import__("decord")
+
+ def check_output_with_vbench(prompt, number, seed=None):
+ if not run_vbench:
+ logger.info("RUN_VBENCH=0, skipping VBench quality gate")
+ return
+ if int(ttnn.distributed_context_get_rank()) != 0:
+ return
+ thresholds = vbench_thresholds_by_height.get(height)
+ if thresholds is None:
+ pytest.skip(f"no VBench thresholds calibrated for height {height}")
+
+ # Gate on the average of VBENCH_SEEDS traced replays (default 5): a single clip's VBench score
+ # is too noisy to gate on — dynamic_degree is near-binary per clip — so score the set together
+ # (VBench averages over a directory). The primary clip (`number`, `seed`) is already rendered;
+ # the rest are cheap warm replays. A single-clip caller (no seed) or OUTPUT_PATH (one pinned
+ # filename) keeps the one-clip path.
+ num_seeds = int(os.environ.get("VBENCH_SEEDS", "5"))
+ if export_dir and (seed is None or num_seeds != 5 or os.environ.get("OUTPUT_PATH")):
+ raise ValueError("CI VBench export requires five distinct traced seed clips")
+ if seed is None or num_seeds <= 1 or os.environ.get("OUTPUT_PATH"):
+ output_filename = os.environ.get("OUTPUT_PATH", f"ltx_av_fast_{width}x{height}_{number}.mp4")
+ assert_vbench_quality(output_filename, prompt=prompt, thresholds=thresholds)
+ return
+
+ cwd = os.path.abspath(os.getcwd())
+
+ with tempfile.TemporaryDirectory() as vbench_dir:
+
+ def _link(idx: int, mp4_number: int) -> None:
+ # The render always lands directly in CWD; resolve the path and confirm it stays there so
+ # the number threaded into the filename can't escape it, and give the link a plain integer
+ # name so nothing dynamic reaches the destination path either.
+ src = os.path.abspath(os.path.join(cwd, f"ltx_av_fast_{width}x{height}_{mp4_number}.mp4"))
+ if os.path.dirname(src) != cwd:
+ raise ValueError(f"render path escaped the working dir: {src!r}")
+ os.symlink(src, os.path.join(vbench_dir, f"seed_{idx}.mp4"))
+
+ _link(0, number)
+ for k in range(1, num_seeds):
+ run(prompt=prompt, number=1000 + k, seed=seed + k)
+ _link(k, 1000 + k)
+ logger.info(f"VBench gate averaged over {num_seeds} seeds (base seed {seed})")
+ if export_dir:
+ from models.tt_dit.utils.vbench_bundle import export_bundle
+
+ export_bundle(
+ vbench_dir,
+ export_dir,
+ prompt=prompt,
+ thresholds=thresholds,
+ temporal_width=int(os.environ.get("VBENCH_TEMPORAL_WIDTH", "0")),
+ )
+ else:
+ assert_vbench_quality(vbench_dir, prompt=prompt, thresholds=thresholds)
+
+ def check_output_with_clip(prompt, number, clip_threshold=None):
+ # Mirrors wan2.2's check_output_with_clip: sample ~8 evenly-spaced frames, score each
+ # against the prompt with CLIP, assert the mean clears a floor. LTX writes the video to
+ # disk (generate() returns only the path), so frames are read back from the mp4.
+ if not run_clip:
+ logger.info("RUN_CLIP=0, skipping CLIP score check")
+ return
+ if int(ttnn.distributed_context_get_rank()) != 0:
+ return
+ try:
+ from decord import VideoReader
+ from PIL import Image
+
+ from models.tt_dit.tests.dataset_eval.clip_encoder import CLIPEncoder
+ except ImportError as e:
+ pytest.skip(f"CLIP deps unavailable ({e})")
+
+ # LTX baseline mean ~31.3 (vs wan2.2's ~37): the LTX prompt exceeds CLIP's 77-token limit
+ # so only its head is scored, and frames are read back re-encoded (CRF=25). 28.0 leaves
+ # ~3pt headroom for run-to-run variance. Override with CLIP_THRESHOLD.
+ threshold = float(os.environ.get("CLIP_THRESHOLD", "28.0")) if clip_threshold is None else clip_threshold
+ output_filename = os.environ.get("OUTPUT_PATH", f"ltx_av_fast_{width}x{height}_{number}.mp4")
+
+ vr = VideoReader(output_filename)
+ total_frames = len(vr)
+ indices = np.linspace(0, total_frames - 1, min(8, total_frames), dtype=int)
+
+ clip_encoder = CLIPEncoder()
+ scores = []
+ for i in indices:
+ frame = vr[int(i)].asnumpy() # (H, W, 3) uint8 RGB
+ pil_img = Image.fromarray(frame.astype(np.uint8))
+ score = clip_encoder.get_clip_score(prompt, pil_img).item() * 100.0
+ scores.append(score)
+
+ clip_min = min(scores)
+ clip_max = max(scores)
+ clip_mean = sum(scores) / len(scores)
+ logger.info(f"CLIP scores: min={clip_min:.2f}, max={clip_max:.2f}, mean={clip_mean:.2f}")
+
+ assert clip_mean >= threshold, (
+ f"Mean CLIP score {clip_mean:.2f} is below threshold {threshold:.2f}. "
+ f"Per-frame scores: {[f'{s:.2f}' for s in scores]}"
+ )
+
+ if no_prompt:
+ seed = int(os.environ.get("SEED", "10"))
+ run(prompt=prompt, number=0, seed=seed)
+ # Traced: gen #0 captures the denoise/VAE/audio traces (lazily, on first step of each stage).
+ if traced:
+ # Gen #1 captures the encode trace (the pipeline opens that gate once its own captures
+ # are done), so gen #2 is the first pass where every trace replays — the one whose
+ # timings are the steady state.
+ logger.info("=== gen #1: encode trace capture ===")
+ run(prompt=steady_state_prompt, number=1, seed=seed)
+ logger.info("=== traced steady-state pass (gen #2, pure replay) ===")
+ run(prompt=replay_prompt, number=2, seed=seed)
+ # Gate gen #2: a corrupted full-replay pass is the failure this structure exists to
+ # catch, and gen #1 still has a capture in it.
+ check_output_with_clip(replay_prompt, 2)
+ check_output_with_vbench(replay_prompt, 2, seed=seed)
+ else:
+ check_output_with_clip(prompt, 0)
+ check_output_with_vbench(prompt, 0)
+ else:
+ for i in itertools.count():
+ new_prompt = input("Enter the input prompt, or q to exit: ")
+ if new_prompt:
+ prompt = new_prompt
+ if prompt[0] == "q":
+ break
+ run(prompt=prompt, number=i, seed=i)
+ check_output_with_clip(prompt, i)
+ check_output_with_vbench(prompt, i)
+
+ if traced:
+ pipeline.release_traces()
+
+
+@functools.lru_cache(maxsize=1)
+def _ffmpeg():
+ """An ffmpeg executable. Prefer the system binary; fall back to imageio-ffmpeg's
+ bundled static build (pip-installed on demand) so a host without ffmpeg still runs
+ the frame-decode checks. The static build is real ffmpeg, so decoded pixels — and
+ thus the calibrated seam thresholds — are unchanged."""
+ exe = shutil.which("ffmpeg")
+ if exe:
+ return exe
+ try:
+ import imageio_ffmpeg
+ except ImportError:
+ pip_install = [sys.executable, "-m", "pip", "install", "-q", "imageio-ffmpeg"]
+ if subprocess.run(pip_install).returncode != 0:
+ # uv-managed venvs ship without pip; bootstrap it from the stdlib, then retry.
+ subprocess.run([sys.executable, "-m", "ensurepip", "--upgrade"], check=True)
+ subprocess.run(pip_install, check=True)
+ import imageio_ffmpeg
+ return imageio_ffmpeg.get_ffmpeg_exe()
+
+
+def _ffmpeg_frames(path, n):
+ """Sample ``n`` luma frames evenly across ``path`` (via ffmpeg) as float ndarrays."""
+ import io
+
+ from PIL import Image
+
+ frames = []
+ for t in np.linspace(0.1, 5.5, n):
+ raw = subprocess.run(
+ [
+ _ffmpeg(),
+ "-v",
+ "error",
+ "-ss",
+ f"{t}",
+ "-i",
+ path,
+ "-vframes",
+ "1",
+ "-f",
+ "image2pipe",
+ "-vcodec",
+ "png",
+ "-",
+ ],
+ capture_output=True,
+ ).stdout
+ if raw:
+ frames.append(np.asarray(Image.open(io.BytesIO(raw)).convert("L")).astype(float))
+ return frames
+
+
+def _temporal_seam_score(path):
+ """Grid-seam strength at the 2x4 mesh boundaries (W/4,W/2,3W/4 vertical; H/2 horizontal).
+ Per-column/row TEMPORAL MEDIAN of the gradient isolates a static seam from moving content.
+ Each boundary is scored against its LOCAL neighbour gradient (a ring +/-12 px, skipping
+ +/-1 px for seam width) — not the global median, which a centred subject over a dark,
+ low-gradient background inflates (the subject straddles W/2, not a mesh line). A baked seam
+ is a thin gradient spike pinned to the boundary, so it stands well above its flat local
+ ring; clean content scores ~1."""
+ fs = _ffmpeg_frames(path, 16)
+ assert fs, f"no frames decoded from {path}"
+ h, w = fs[0].shape
+ gx = np.median(np.stack([np.abs(np.diff(f, axis=1)).mean(0) for f in fs]), 0)
+ gy = np.median(np.stack([np.abs(np.diff(f, axis=0)).mean(1) for f in fs]), 0)
+
+ def _ring_ratio(g, idx, skip=1, span=12):
+ lo, hi = max(0, idx - span), min(len(g), idx + span + 1)
+ ring = np.concatenate([g[lo : idx - skip], g[idx + skip + 1 : hi]])
+ return float(g[idx] / (np.median(ring) + 1e-6))
+
+ v = float(np.mean([_ring_ratio(gx, round(w * i / 4)) for i in (1, 2, 3)]))
+ hh = _ring_ratio(gy, round(h / 2))
+ return v, hh
+
+
+@pytest.mark.skipif(
+ not _ltx_checkpoint_cached("ltx-2.3-22b-distilled-1.1.safetensors"),
+ reason="needs the LTX checkpoint (set LTX_CHECKPOINT to a local .safetensors)",
+)
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis, num_links, device_params, topology, is_fsdp, dynamic_load",
+ LTX_DISTILLED_I2V_MESH_PARAMS_DL,
+ indirect=["mesh_device", "device_params"],
+)
+def test_pipeline_distilled_i2v(mesh_device, sp_axis, tp_axis, num_links, dynamic_load, topology, is_fsdp, tmp_path):
+ """E2E chained t2v->i2v: generate a t2v clip, then i2v continuing from its LAST frame
+ (same DEFAULT_LTX_PROMPT), and splice both into one continuous ~12s clip. Asserts the
+ I2V output (a) reproduces the conditioning frame at frame-0 (conditioning works) and
+ (b) is free of the VAE 2x4 grid seam (guards the non-mesh-aligned i2v fix at 1088x1920,
+ whose s1 cond latent is the uneven 17x30 that used to seam). An external LTX_I2V_IMAGE
+ overrides the t2v pre-gen (single i2v, no splice)."""
+ import subprocess
+
+ from PIL import Image
+
+ skip_if_unsupported_num_links(mesh_device, num_links)
+
+ ckpt = default_ltx_checkpoint("ltx-2.3-22b-distilled-1.1.safetensors")
+ parent_mesh = mesh_device
+ mesh_shape = tuple(parent_mesh.shape)
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ num_frames = int(os.environ.get("NUM_FRAMES", "145"))
+ height = int(os.environ.get("HEIGHT", "1088")) # 1088x1920 -> s1 latent 17x30 (uneven, the fixed case)
+ width = int(os.environ.get("WIDTH", "1920"))
+ traced = os.environ.get("LTX_TRACED", "0") in ("1", "true", "True")
+ seed = int(os.environ.get("SEED", "10"))
+ prompt = os.environ.get("PROMPT", DEFAULT_LTX_PROMPT)
+
+ pipeline = LTXDistilledPipeline.create_pipeline(
+ mesh_device=mesh_device,
+ checkpoint_name=ckpt,
+ gemma_path=default_ltx_gemma(),
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ dynamic_load=dynamic_load,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ run_warmup=traced, # trace capture needs warmup; eager doesn't
+ traced=traced,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ # This test conditions on a frame produced in-process (t2v last frame), so there's no
+ # conditioning-image path in hand at construction — force the I2V path on explicitly.
+ image_conditioning=True,
+ )
+
+ if int(ttnn.distributed_context_get_rank()) != 0:
+ return
+
+ def _gen(out, images):
+ pipeline.generate(
+ prompt, output_path=str(out), images=images, num_frames=num_frames, height=height, width=width, seed=seed
+ )
+
+ # Conditioning image: an explicit LTX_I2V_IMAGE conditions directly on it and SKIPS the t2v
+ # pre-gen (an external/user image, or a t2v last frame produced by a prior process). Else this
+ # generates a t2v clip and conditions on its LAST FRAME so the i2v continues the shot.
+ cond_override = os.environ.get("LTX_I2V_IMAGE")
+ strength = float(os.environ.get("LTX_I2V_STRENGTH", "1.0"))
+ t2v = None
+ if cond_override:
+ cond = cond_override
+ else:
+ t2v = tmp_path / "t2v.mp4"
+ _gen(t2v, None)
+ # -sseof seeks near the end and -update rewrites the same file per decoded frame, so it
+ # lands on the true final frame without buffering the whole clip into memory.
+ cond = tmp_path / "cond_lastframe.png"
+ subprocess.run(
+ [_ffmpeg(), "-v", "error", "-sseof", "-3", "-i", str(t2v), "-update", "1", "-y", str(cond)],
+ check=True,
+ )
+ if os.environ.get("LTX_T2V_OUT"):
+ shutil.copy(str(t2v), os.environ["LTX_T2V_OUT"])
+ if os.environ.get("LTX_COND_OUT"):
+ shutil.copy(str(cond), os.environ["LTX_COND_OUT"])
+ # On a 4x8 mesh the image encoder and DiT coresident-evict, so a t2v->i2v chain in one
+ # process reloads the encoder over t2v's captured traces and wedges. LTX_T2V_ONLY stops
+ # here so a fresh process runs i2v on LTX_COND_OUT (via LTX_I2V_IMAGE) and host-splices.
+ if os.environ.get("LTX_T2V_ONLY"):
+ print(
+ f"I2V_E2E t2v-only -> clip={os.environ.get('LTX_T2V_OUT')} cond={os.environ.get('LTX_COND_OUT')}",
+ flush=True,
+ )
+ if traced:
+ pipeline.release_traces()
+ return
+
+ # i2v conditioned on that frame
+ i2v = tmp_path / "i2v.mp4"
+ _gen(i2v, [(str(cond), 0, strength)])
+ if os.environ.get("LTX_I2V_OUT"):
+ shutil.copy(str(i2v), os.environ["LTX_I2V_OUT"])
+
+ # In-process splice of t2v + i2v into one continuous clip (each gen is 145f@24fps -> ~12s).
+ # Only reached where encoder+DiT+traces fit one process (not the 4x8 mesh — use LTX_T2V_ONLY
+ # plus a second i2v process there). An external LTX_I2V_IMAGE has no preceding t2v to splice.
+ if t2v is not None:
+ chained = os.environ.get("LTX_CHAINED_OUT") or str(tmp_path / "chained.mp4")
+ subprocess.run(
+ [
+ _ffmpeg(),
+ "-v",
+ "error",
+ "-i",
+ str(t2v),
+ "-i",
+ str(i2v),
+ "-filter_complex",
+ "[0:v:0][0:a:0][1:v:0][1:a:0]concat=n=2:v=1:a=1[v][a]",
+ "-map",
+ "[v]",
+ "-map",
+ "[a]",
+ "-y",
+ chained,
+ ],
+ check=True,
+ )
+ print(f"I2V_E2E chained t2v+i2v -> {chained}", flush=True)
+
+ # (a) conditioning works: i2v frame-0 reproduces the conditioning frame (VAE roundtrip + CRF,
+ # so not identity — but a far tighter correlation than an unconditioned gen of the same prompt).
+ def _luma0(path):
+ raw = subprocess.run(
+ [_ffmpeg(), "-v", "error", "-i", path, "-vframes", "1", "-f", "image2pipe", "-vcodec", "png", "-"],
+ capture_output=True,
+ ).stdout
+ return Image.open(__import__("io").BytesIO(raw)).convert("L")
+
+ def _vec(im):
+ return torch.from_numpy(np.asarray(im).astype("float32")).flatten()
+
+ f0_img = _luma0(str(i2v))
+ cond_img = _luma0(str(cond))
+ # Compare a matched field of view: an external LTX_I2V_IMAGE can differ in resolution/aspect
+ # from the generated frame (the pipeline center-crops the conditioning image to the target
+ # aspect before encoding). Mirror that — center-crop cond to the frame's aspect, then resize.
+ # No-op when cond is a same-size/same-aspect frame (the self-conditioned t2v-last-frame case).
+ fw, fh = f0_img.size
+ cw, ch = cond_img.size
+ aspect = fw / fh
+ if cw / ch > aspect:
+ nw = round(ch * aspect)
+ x0 = (cw - nw) // 2
+ cond_img = cond_img.crop((x0, 0, x0 + nw, ch))
+ elif cw / ch < aspect:
+ nh = round(cw / aspect)
+ y0 = (ch - nh) // 2
+ cond_img = cond_img.crop((0, y0, cw, y0 + nh))
+ if cond_img.size != f0_img.size:
+ cond_img = cond_img.resize(f0_img.size)
+
+ c, f0 = _vec(cond_img), _vec(f0_img)
+ pcc = torch.corrcoef(torch.stack([c, f0]))[0, 1].item()
+ print(f"\nI2V_E2E frame0-vs-cond PCC={pcc:.4f}", flush=True)
+
+ # (b) seam-free at the uneven 1088x1920 (the i2v grid-seam fix). Local-ring ratios sit at
+ # ~1.0 on clean content (measured 0.85-1.16 across all boundaries); a baked seam spikes
+ # well above its flat neighbourhood. Threshold 1.5 clears the clean band with margin.
+ v, hh = _temporal_seam_score(str(i2v))
+ print(f"I2V_E2E seam V={v:.2f} H={hh:.2f} (clean~1.0, seam>>1)", flush=True)
+
+ if traced:
+ pipeline.release_traces()
+
+ assert pcc > 0.85, f"i2v frame-0 does not reproduce the conditioning frame (PCC={pcc:.4f}) — conditioning broken"
+ assert v < 1.5 and hh < 1.5, f"i2v grid seam present (V={v:.2f}, H={hh:.2f}) at 1088x1920"
+
+
+@pytest.mark.skipif(
+ not _ltx_checkpoint_cached("ltx-2.3-22b-distilled-1.1.safetensors"),
+ reason="needs the LTX checkpoint (set LTX_CHECKPOINT to a local .safetensors)",
+)
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis, num_links, device_params, topology, is_fsdp, dynamic_load",
+ LTX_DISTILLED_AUDIO_MESH_PARAMS_DL,
+ indirect=["mesh_device", "device_params"],
+)
+def test_audio_decode_girl(mesh_device, sp_axis, tp_axis, num_links, dynamic_load, topology, is_fsdp):
+ """Just the audio section of the AV pipeline: real checkpoint weights, real girl-clip
+ latent shape (num_frames -> als.frames), no transformer/gemma (decode_audio never
+ encodes prompts). Profiles cold (weight load + compile) vs warm (steady-state per-gen)
+ decode wall. LTX_TRACED=1 traces the main vocoder (cold/warm timing only — the eager
+ torch-oracle quality block is skipped under trace; run LTX_TRACED=0 for the stats)."""
+ skip_if_unsupported_num_links(mesh_device, num_links)
+
+ parent_mesh = mesh_device
+ mesh_shape = tuple(parent_mesh.shape)
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ num_frames = int(os.environ.get("NUM_FRAMES", "145")) # ~6.04s @ 24fps
+ height = int(os.environ.get("HEIGHT", "1088"))
+ width = int(os.environ.get("WIDTH", "1920"))
+ traced = os.environ.get("LTX_TRACED", "0") in ("1", "true", "True")
+
+ ckpt = default_ltx_checkpoint("ltx-2.3-22b-distilled-1.1.safetensors")
+ # Audio-only build: create the pipeline WITHOUT a checkpoint so __init__ builds/primes
+ # nothing (skipping the ~10-min video warmup and the 22B transformer / VAE / upsampler
+ # prime that dominate a cold run — decode_audio only touches the audio decoder + vocoder).
+ # Then point it at the checkpoint and construct + prime just the audio decode chain;
+ # decode_audio compiles + captures its own trace lazily on its first call.
+ pipeline = LTXDistilledPipeline.create_pipeline(
+ mesh_device=mesh_device,
+ checkpoint_name=None,
+ gemma_path=default_ltx_gemma(), # built as a lazy shell only; never loaded here
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ dynamic_load=dynamic_load,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ run_warmup=False,
+ traced=traced,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ )
+ pipeline.checkpoint_name = LTXDistilledPipeline._resolve_checkpoint_file(ckpt)
+ pipeline._audio_adapter = LTXAudioDecoderAdapter(
+ pipeline.checkpoint_name,
+ mesh_device=pipeline.mesh_device,
+ vae_ccl_manager=pipeline.vae_ccl_manager,
+ dit_parallel_config=pipeline.parallel_config,
+ traced=pipeline._traced,
+ )
+ pipeline._prepare_audio_decoder()
+
+ vps = VideoPixelShape(batch=1, frames=num_frames, height=height, width=width, fps=24)
+ als = AudioLatentShape.from_video_pixel_shape(vps)
+ # Seeded synthetic latent scaled to a real girl-clip audio latent's statistics (mean≈0.2,
+ # std≈0.92). The PCC gate below is TT-vs-torch with both legs fed the same mel, so the input
+ # need not be "real" content — only AUDIO_OUT .wav listening would differ. AUDIO_LATENT
+ # (.npy or .pt) still overrides for anyone who wants to decode a real dumped latent.
+ _lat = os.environ.get("AUDIO_LATENT")
+ if _lat and os.path.exists(_lat):
+ latent = (torch.from_numpy(np.load(_lat)) if _lat.endswith(".npy") else torch.load(_lat)).float()
+ else:
+ torch.manual_seed(0)
+ latent = torch.randn(1, als.frames, pipeline.in_channels, dtype=torch.float32) * 0.92 + 0.2
+
+ t0 = time.perf_counter()
+ audio = pipeline.decode_audio(latent, num_frames, fps=24.0) # cold: weight load + compile (+ capture)
+ cold_ms = (time.perf_counter() - t0) * 1000
+
+ # One untimed decode to absorb a late first-replay cost (the BWE trace captures on the
+ # first post-cold call, not during the cold decode), so warm_ms is true steady state.
+ pipeline.decode_audio(latent, num_frames, fps=24.0)
+ # WARM_REPS shrinks the steady-state loop for fast dev iteration (the PCC oracle below is
+ # unaffected); default 5 keeps warm_ms a stable average for reported timings.
+ N = int(os.environ.get("WARM_REPS", "5"))
+ t0 = time.perf_counter()
+ for _ in range(N):
+ audio = pipeline.decode_audio(latent, num_frames, fps=24.0)
+ warm_ms = (time.perf_counter() - t0) * 1000 / N
+ wav = audio.waveform
+ sr = audio.sampling_rate
+ out_dir = os.environ.get("AUDIO_OUT")
+
+ def _save(w, name):
+ if not out_dir:
+ return
+ import soundfile as sf
+
+ os.makedirs(out_dir, exist_ok=True)
+ a = w[0] if w.dim() == 3 else w # (2, T) -> (T, 2)
+ path = os.path.join(out_dir, name)
+ sf.write(path, a.transpose(0, 1).cpu().numpy(), int(sr))
+ logger.info(f"wrote {path}")
+
+ _save(wav, f"girl_audio_conv1d{'_traced' if traced else ''}.wav")
+
+ # Torch-oracle quality gate vs the diffusers vocoder+BWE (real checkpoint, CPU) — the ground
+ # truth. The conv1d path runs under BOTH eager and traced (gating the traced vocoder/BWE
+ # output, which is otherwise unverified — exactly the gap that let a broken trace ship).
+ # Per-1s-interval error-to-signal (rmse/σ, dB) + overall PCC; the absolute level (~−18 dB
+ # conv1d) is the fp32/bf16 floor, gated only against a gross spike.
+ from models.tt_dit.tests.models.ltx.test_audio_ltx import _build_torch_stage_c_real
+
+ z = pipeline.tt_mel_decoder.z_channels
+ audio_spatial = latent.reshape(1, latent.shape[1], z, latent.shape[2] // z).permute(0, 2, 1, 3).float()
+ mel = pipeline.tt_mel_decoder(audio_spatial) # TT mel, fed to both TT and torch
+ w_conv = pipeline.tt_vocoder_with_bwe(mel).squeeze(0).float()
+ with torch.no_grad():
+ w_torch = _build_torch_stage_c_real(ckpt)(mel.float()).squeeze(0).float()
+ _save(w_conv, f"girl_audio_conv1d{'_traced' if traced else ''}.wav")
+ _save(w_torch, "girl_audio_torch.wav")
+
+ nn = min(w_torch.shape[-1], w_conv.shape[-1])
+
+ def _vs_torch(test, name):
+ rows = []
+ for start in range(0, nn, sr):
+ r, t = w_torch[..., start : start + sr], test[..., start : start + sr]
+ sig = r.pow(2).mean().sqrt().item() + 1e-9
+ rr = ((r - t) ** 2).mean().sqrt().item() / sig
+ rows.append(rr)
+ logger.info(
+ f"[oracle] t={start//sr}s {name}-vs-torch rmse/σ={rr:.3e} ({20*math.log10(rr+1e-12):+.1f} dB) "
+ f"max|Δ|={(r - t).abs().max().item():.3e}"
+ )
+ pcc = torch.corrcoef(torch.stack([w_torch[..., :nn].flatten(), test[..., :nn].flatten()]))[0, 1].item()
+ worst_, med_ = max(rows), sorted(rows)[len(rows) // 2]
+ print(
+ f"\nAUDIO_GIRL {name}-vs-torch traced={traced} worst_rmse/σ={worst_:.3e} "
+ f"({20*math.log10(worst_+1e-12):+.1f} dB) median={med_:.3e} ratio={worst_/(med_+1e-9):.2f} PCC={pcc:.5f}",
+ flush=True,
+ )
+ return worst_, med_, pcc
+
+ cw, cm, c_pcc = _vs_torch(w_conv, "conv1d")
+ # Gross localized spike only: an interval >4x the median AND past a clearly-audible floor
+ # (rmse/σ 0.5 = −6 dB). Also gate overall PCC vs torch — a broken (e.g. mis-traced) vocoder
+ # drops PCC far below the ~0.99 fp32/bf16 floor.
+ assert cw < 0.5 and cw < 4 * cm + 1e-6, f"conv1d-vs-torch localized spike: worst {cw:.3e} vs median {cm:.3e}"
+ assert c_pcc > 0.95, f"conv1d-vs-torch PCC {c_pcc:.4f} below floor (traced={traced}) — vocoder output is wrong"
+
+ pipeline.release_traces()
+ dur = wav.shape[-1] / sr
+ print(
+ f"\nAUDIO_GIRL traced={traced} "
+ f"latent_frames={als.frames} out={tuple(wav.shape)} {dur:.2f}s@{sr}Hz cold={cold_ms:.1f}ms warm={warm_ms:.1f}ms",
+ flush=True,
+ )
+ assert torch.isfinite(wav).all(), "decoded waveform has non-finite samples"
+ assert abs(dur - num_frames / 24.0) < 0.2, f"duration {dur:.2f}s != ~{num_frames/24.0:.2f}s"
diff --git a/code/models/tt_dit/tests/models/ltx/test_pipeline_ltx_one_stage.py b/code/models/tt_dit/tests/models/ltx/test_pipeline_ltx_one_stage.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e3794a6424ec3d73354f5ed60ee4d6cde8df5b0
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_pipeline_ltx_one_stage.py
@@ -0,0 +1,173 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+LTX-2 one-stage pipeline tests:
+- Euler step scheduler primitive (no device needed)
+- End-to-end one-stage AV generation (LTXOneStagePipeline) on a multi-chip mesh
+"""
+
+import itertools
+import os
+
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.tt_dit.pipelines.ltx.pipeline_ltx import euler_step
+from models.tt_dit.pipelines.ltx.pipeline_ltx_one_stage import LTXOneStagePipeline
+from models.tt_dit.utils.ltx import (
+ DEFAULT_LTX_PROMPT,
+ default_ltx_checkpoint,
+ default_ltx_gemma,
+ print_ltx_timing_table,
+)
+from models.tt_dit.utils.test import skip_if_unsupported_num_links
+
+from .ltx_mesh_params import LTX_ONE_STAGE_MESH_PARAMS_DL
+
+
+def test_euler_step():
+ """Test Euler step: x_{t+1} = x_t + velocity * dt."""
+ torch.manual_seed(42)
+ sample = torch.randn(1, 256, 128)
+ denoised = torch.randn(1, 256, 128)
+ sigma, sigma_next = 0.8, 0.6
+
+ result = euler_step(sample, denoised, sigma=sigma, sigma_next=sigma_next)
+
+ # Manual computation
+ velocity = (sample.float() - denoised.float()) / sigma
+ dt = sigma_next - sigma
+ expected = (sample.float() + velocity * dt).to(sample.dtype)
+
+ assert torch.allclose(
+ result, expected, atol=1e-6
+ ), f"Euler step mismatch: max diff {(result - expected).abs().max():.2e}"
+ logger.info("Euler step matches manual computation")
+
+
+# ---------------------------------------------------------------------------
+# End-to-end one-stage AV generation (requires device + checkpoint)
+# ---------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize(
+ "no_prompt",
+ [{"1": True, "0": False}.get(os.environ.get("NO_PROMPT"), True)],
+)
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis, num_links, device_params, topology, is_fsdp, dynamic_load",
+ LTX_ONE_STAGE_MESH_PARAMS_DL,
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(
+ "width, height",
+ [
+ (1920, 1088),
+ ],
+ ids=[
+ "resolution_1080p",
+ ],
+)
+def test_pipeline_one_stage(
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ dynamic_load,
+ topology,
+ width,
+ height,
+ is_fsdp,
+ no_prompt,
+):
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ ckpt = default_ltx_checkpoint("ltx-2.3-22b-dev.safetensors")
+ gemma = default_ltx_gemma()
+ # ckpt / gemma always resolve (env var → local → HF repo string fallback). The pipeline's
+ # resolver downloads from HF if needed.
+
+ parent_mesh = mesh_device
+ mesh_shape = tuple(parent_mesh.shape)
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ num_frames = int(os.environ.get("NUM_FRAMES", "145"))
+ num_inference_steps = int(os.environ.get("NUM_STEPS", "30"))
+ width = int(os.environ.get("WIDTH", width))
+ height = int(os.environ.get("HEIGHT", height))
+
+ run_warmup = os.environ.get("RUN_WARMUP", "0") in ("1", "true", "True")
+
+ pipeline = LTXOneStagePipeline.create_pipeline(
+ mesh_device=mesh_device,
+ checkpoint_name=ckpt,
+ gemma_path=gemma,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ dynamic_load=dynamic_load,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ run_warmup=run_warmup,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ )
+
+ prompt = os.environ.get("PROMPT", DEFAULT_LTX_PROMPT)
+
+ def run(*, prompt, number, seed):
+ logger.info(f"Running inference with prompt: '{prompt}'")
+ logger.info(f"Parameters: {height}x{width}, {num_frames} frames, {num_inference_steps} steps")
+
+ output_filename = os.environ.get("OUTPUT_PATH", f"ltx_av_pro_{width}x{height}_{number}.mp4")
+
+ pipeline.generate(
+ prompt,
+ output_path=output_filename,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ num_inference_steps=num_inference_steps,
+ seed=seed,
+ )
+
+ if int(ttnn.distributed_context_get_rank()) == 0:
+ # Minimal artifact gate: the leg must not pass on missing/empty output. A calibrated
+ # VBench/CLIP quality check (like the distilled path) is the stronger follow-up.
+ assert os.path.exists(output_filename), f"pipeline produced no video at {output_filename}"
+ assert os.path.getsize(output_filename) > 0, f"pipeline produced an empty video at {output_filename}"
+ logger.info(f"Saved video to: {output_filename}")
+ print_ltx_timing_table(
+ pipeline,
+ label="LTX ONE-STAGE",
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ mesh_shape=mesh_shape,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ topology=topology,
+ output_path=output_filename,
+ prompt=prompt,
+ )
+ else:
+ logger.info(f"Skipping video export on rank {ttnn.distributed_context_get_rank()}")
+
+ if no_prompt:
+ run(prompt=prompt, number=0, seed=42)
+ else:
+ for i in itertools.count():
+ new_prompt = input("Enter the input prompt, or q to exit: ")
+ if new_prompt:
+ prompt = new_prompt
+ if prompt[0] == "q":
+ break
+ run(prompt=prompt, number=i, seed=i)
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
diff --git a/code/models/tt_dit/tests/models/ltx/test_pipeline_ltx_two_stages.py b/code/models/tt_dit/tests/models/ltx/test_pipeline_ltx_two_stages.py
new file mode 100644
index 0000000000000000000000000000000000000000..56eb790ef190f73eb0cfdd9d5125dfd3284a98ea
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_pipeline_ltx_two_stages.py
@@ -0,0 +1,237 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+import itertools
+import os
+
+import pytest
+from loguru import logger
+
+import ttnn
+from models.tt_dit.pipelines.ltx.pipeline_ltx_two_stages import LTXTwoStagesPipeline
+from models.tt_dit.utils.ltx import (
+ DEFAULT_LTX_PROMPT,
+ default_ltx_checkpoint,
+ default_ltx_gemma,
+ print_ltx_timing_table,
+)
+from models.tt_dit.utils.test import skip_if_unsupported_num_links
+
+from .ltx_mesh_params import LTX_TWO_STAGES_I2V_MESH_PARAMS_DL, LTX_TWO_STAGES_MESH_PARAMS_DL
+
+
+def _default_distilled_lora() -> str:
+ """Resolve distilled stage-2 LoRA: env var > local file > HF (auto-downloaded here)."""
+ explicit = os.environ.get("DISTILLED_LORA_PATH")
+ if explicit:
+ return explicit
+ local = os.path.expanduser("~/.cache/ltx-checkpoints/ltx-2.3-22b-distilled-lora-384-1.1.safetensors")
+ if os.path.exists(local):
+ return local
+ from huggingface_hub import hf_hub_download
+
+ logger.info(
+ "Resolving HuggingFace distilled LoRA Lightricks/LTX-2.3:ltx-2.3-22b-distilled-lora-384-1.1.safetensors"
+ )
+ return hf_hub_download(repo_id="Lightricks/LTX-2.3", filename="ltx-2.3-22b-distilled-lora-384-1.1.safetensors")
+
+
+@pytest.mark.parametrize(
+ "no_prompt",
+ [{"1": True, "0": False}.get(os.environ.get("NO_PROMPT"), True)],
+)
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis, num_links, device_params, topology, is_fsdp, dynamic_load",
+ LTX_TWO_STAGES_MESH_PARAMS_DL,
+ indirect=["mesh_device", "device_params"],
+)
+def test_pipeline_two_stages(
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ dynamic_load,
+ topology,
+ is_fsdp,
+ no_prompt,
+):
+ """LTX-2.3 22B 2-stage AV pipeline: full-guidance s1 + distilled-LoRA s2 refine."""
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ ckpt = default_ltx_checkpoint("ltx-2.3-22b-dev.safetensors")
+ distilled_lora = _default_distilled_lora()
+ gemma = default_ltx_gemma()
+
+ parent_mesh = mesh_device
+ mesh_shape = tuple(parent_mesh.shape)
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ num_frames = int(os.environ.get("NUM_FRAMES", "145"))
+ height = int(os.environ.get("HEIGHT", "1088"))
+ width = int(os.environ.get("WIDTH", "1920"))
+ num_inference_steps = int(os.environ.get("NUM_INFERENCE_STEPS", "30"))
+
+ run_warmup = os.environ.get("RUN_WARMUP", "0") in ("1", "true", "True")
+ pipeline = LTXTwoStagesPipeline.create_pipeline(
+ mesh_device=mesh_device,
+ checkpoint_name=ckpt,
+ gemma_path=gemma,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ dynamic_load=dynamic_load,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ distilled_lora_path=distilled_lora,
+ run_warmup=run_warmup,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ )
+
+ prompt = os.environ.get("PROMPT", DEFAULT_LTX_PROMPT)
+
+ def run(*, prompt, number, seed):
+ output_filename = os.environ.get("OUTPUT_PATH", f"ltx_av_two_stages_{width}x{height}_{number}.mp4")
+ logger.info(f"Running LTX AV Two-Stages: '{prompt[:80]}...'")
+ logger.info(f"Config: {height}x{width}, {num_frames} frames, {num_inference_steps} stage-1 steps")
+
+ if int(ttnn.distributed_context_get_rank()) != 0:
+ logger.info(f"Skipping generation on rank {ttnn.distributed_context_get_rank()}")
+ return
+
+ pipeline.generate(
+ prompt,
+ output_path=output_filename,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ num_inference_steps=num_inference_steps,
+ seed=seed,
+ )
+ logger.info(f"Saved video to: {output_filename}")
+ print_ltx_timing_table(
+ pipeline,
+ label="LTX TWO-STAGES",
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ mesh_shape=mesh_shape,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ topology=topology,
+ output_path=output_filename,
+ prompt=prompt,
+ )
+
+ if no_prompt:
+ run(prompt=prompt, number=0, seed=int(os.environ.get("SEED", "10")))
+ else:
+ for i in itertools.count():
+ new_prompt = input("Enter the input prompt, or q to exit: ")
+ if new_prompt:
+ prompt = new_prompt
+ if prompt[0] == "q":
+ break
+ run(prompt=prompt, number=i, seed=i)
+
+
+# ---------------------------------------------------------------------------
+# I2V smoke test (image + text -> video)
+# ---------------------------------------------------------------------------
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis, num_links, device_params, topology, is_fsdp, dynamic_load",
+ LTX_TWO_STAGES_I2V_MESH_PARAMS_DL,
+ indirect=["mesh_device", "device_params"],
+)
+def test_pipeline_two_stages_i2v_smoke(
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ dynamic_load,
+ topology,
+ is_fsdp,
+):
+ """End-to-end I2V smoke: generate(images=[(img, 0, 1.0)]) produces a non-empty MP4 whose
+ first frame correlates with the conditioning image.
+
+ Gated behind LTX_I2V_IMAGE (path to a conditioning image); skips otherwise so it never runs
+ in the default CI sweep without an input image / checkpoint present.
+ """
+ image_path = os.environ.get("LTX_I2V_IMAGE")
+ if not image_path or not os.path.exists(image_path):
+ pytest.skip("set LTX_I2V_IMAGE to a conditioning image path to run the I2V smoke test")
+
+ skip_if_unsupported_num_links(mesh_device, num_links)
+
+ ckpt = default_ltx_checkpoint("ltx-2.3-22b-dev.safetensors")
+ distilled_lora = _default_distilled_lora()
+ gemma = default_ltx_gemma()
+
+ parent_mesh = mesh_device
+ mesh_shape = tuple(parent_mesh.shape)
+ mesh_device = parent_mesh.create_submesh(ttnn.MeshShape(*mesh_shape))
+
+ num_frames = int(os.environ.get("NUM_FRAMES", "25"))
+ height = int(os.environ.get("HEIGHT", "512"))
+ width = int(os.environ.get("WIDTH", "768"))
+ num_inference_steps = int(os.environ.get("NUM_INFERENCE_STEPS", "8"))
+ strength = float(os.environ.get("LTX_I2V_STRENGTH", "1.0"))
+
+ pipeline = LTXTwoStagesPipeline.create_pipeline(
+ mesh_device=mesh_device,
+ checkpoint_name=ckpt,
+ gemma_path=gemma,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ dynamic_load=dynamic_load,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ distilled_lora_path=distilled_lora,
+ run_warmup=False,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ )
+
+ if int(ttnn.distributed_context_get_rank()) != 0:
+ logger.info(f"Skipping generation on rank {ttnn.distributed_context_get_rank()}")
+ return
+
+ output_filename = os.environ.get("OUTPUT_PATH", f"ltx_i2v_two_stages_{width}x{height}.mp4")
+ prompt = os.environ.get("PROMPT", DEFAULT_LTX_PROMPT)
+ pipeline.generate(
+ prompt,
+ output_path=output_filename,
+ images=[(image_path, 0, strength)],
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ num_inference_steps=num_inference_steps,
+ seed=int(os.environ.get("SEED", "10")),
+ )
+
+ assert os.path.exists(output_filename), f"no MP4 produced at {output_filename}"
+ assert os.path.getsize(output_filename) > 0, "produced MP4 is empty"
+ logger.info(f"I2V smoke: saved {output_filename} ({os.path.getsize(output_filename)} bytes)")
+
+ # Optional first-frame correlation check (only if imageio + PIL are available).
+ try:
+ import imageio.v3 as iio
+ import numpy as np
+ from PIL import Image
+
+ frames = iio.imread(output_filename, index=None, plugin="pyav") # (T, H, W, 3), in-process
+ first = frames[0].astype(np.float32) / 255.0
+ cond = Image.open(image_path).convert("RGB").resize((width, height))
+ cond_np = np.asarray(cond, dtype=np.float32) / 255.0
+ a = first.reshape(-1) - first.mean()
+ b = cond_np.reshape(-1) - cond_np.mean()
+ pcc = float((a @ b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8))
+ logger.info(f"I2V smoke: first-frame vs conditioning-image PCC = {pcc:.3f}")
+ if strength >= 1.0:
+ assert pcc > 0.3, f"first frame does not resemble the conditioning image (PCC={pcc:.3f})"
+ except ImportError:
+ logger.info("imageio/PIL unavailable — skipping first-frame correlation check")
diff --git a/code/models/tt_dit/tests/models/ltx/test_rope_ltx.py b/code/models/tt_dit/tests/models/ltx/test_rope_ltx.py
new file mode 100644
index 0000000000000000000000000000000000000000..2fa9718e8d7588efeca38973af0fcd1d1275d9f1
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_rope_ltx.py
@@ -0,0 +1,129 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.mochi import get_rot_transformation_mat
+from models.tt_dit.utils.tensor import bf16_tensor, bf16_tensor_2dshard
+
+
+def make_indices_grid(F: int, H: int, W: int) -> torch.Tensor:
+ """3D position indices grid (1, n_dims=3, seq_len) in the layout diffusers' RoPE expects."""
+ t_ids = torch.arange(F)
+ h_ids = torch.arange(H)
+ w_ids = torch.arange(W)
+ grid_t, grid_h, grid_w = torch.meshgrid(t_ids, h_ids, w_ids, indexing="ij")
+ indices_grid = torch.stack([grid_t.flatten(), grid_h.flatten(), grid_w.flatten()], dim=0).float()
+ return indices_grid.unsqueeze(0) # (1, n_dims=3, seq_len)
+
+
+@pytest.mark.parametrize(
+ "mesh_device, sp_axis, tp_axis",
+ [
+ [(1, 1), 0, 1],
+ [(1, 2), 0, 1],
+ [(2, 1), 0, 1],
+ [(2, 2), 0, 1],
+ [(2, 4), 0, 1],
+ [(2, 4), 1, 0],
+ ],
+ ids=[
+ "1x1sp0tp1",
+ "1x2sp0tp1",
+ "2x1sp0tp1",
+ "2x2sp0tp1",
+ "2x4sp0tp1",
+ "2x4sp1tp0",
+ ],
+ indirect=["mesh_device"],
+)
+@pytest.mark.parametrize(
+ "F, H, W",
+ [
+ (5, 16, 28), # Small: 2240 tokens
+ (5, 30, 52), # 480p-ish: 7800 tokens
+ (5, 34, 64), # 2K spatial grid (1088x2048 latent H×W): 10880 tokens
+ ],
+ ids=["small", "480p", "2k"],
+)
+def test_ltx_rope_interleaved(mesh_device: ttnn.MeshDevice, sp_axis: int, tp_axis: int, F: int, H: int, W: int):
+ """Test LTX-2 interleaved RoPE against the diffusers reference (mirrors test_wan_rotary_pos_embed)."""
+ # double_precision=False keeps the freq grid fp32 to match the device path.
+ from diffusers.models.transformers.transformer_ltx2 import (
+ LTX2AudioVideoRotaryPosEmbed,
+ apply_interleaved_rotary_emb,
+ )
+
+ dim = 4096
+ num_heads = 32
+ head_dim = dim // num_heads # 128
+ theta = 10000.0
+ max_pos = [20, 2048, 2048]
+ B = 1
+
+ indices_grid = make_indices_grid(F, H, W)
+ seq_len = F * H * W
+ logger.info(f"indices_grid shape: {indices_grid.shape}, seq_len: {seq_len}")
+
+ # Reference cos/sin on CPU; raw indices (ndim==3) normalize by base_{frames,height,width}.
+ rope = LTX2AudioVideoRotaryPosEmbed(
+ dim=dim,
+ base_num_frames=max_pos[0],
+ base_height=max_pos[1],
+ base_width=max_pos[2],
+ theta=theta,
+ modality="video",
+ double_precision=False,
+ rope_type="interleaved",
+ num_attention_heads=num_heads,
+ )
+ # Interleaved cos/sin: (B, seq_len, dim)
+ cos_freq, sin_freq = rope(indices_grid)
+ logger.info(f"cos_freq shape: {cos_freq.shape}, sin_freq shape: {sin_freq.shape}")
+
+ # Create random input on the flat (B, seq_len, dim) layout the diffusers apply expects.
+ torch.manual_seed(42)
+ input_flat = torch.randn(B, seq_len, dim, dtype=torch.float32)
+
+ # PyTorch reference: diffusers applies interleaved RoPE on (B, N, dim).
+ output_ref_flat = apply_interleaved_rotary_emb(input_flat, (cos_freq, sin_freq))
+
+ # Reshape into (B, seq_len, num_heads, head_dim) for the device kernel + comparison.
+ input_tensor = input_flat.reshape(B, seq_len, num_heads, head_dim)
+ output_ref = output_ref_flat.reshape(B, seq_len, num_heads, head_dim)
+ cos_for_apply = cos_freq.reshape(B, seq_len, num_heads, head_dim)
+ sin_for_apply = sin_freq.reshape(B, seq_len, num_heads, head_dim)
+ logger.info(f"Reference output shape: {output_ref.shape}")
+
+ # rotary_embedding_llama expects (B, num_heads, seq_len, head_dim)
+ input_bhnd = input_tensor.permute(0, 2, 1, 3) # (B, H, N, D)
+ cos_bhnd = cos_for_apply.permute(0, 2, 1, 3) # (B, H, N, D)
+ sin_bhnd = sin_for_apply.permute(0, 2, 1, 3) # (B, H, N, D)
+
+ tt_input = bf16_tensor_2dshard(input_bhnd, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ # cos/sin sharded same as input: SP axis on sequence, TP axis on heads
+ tt_cos = bf16_tensor_2dshard(cos_bhnd, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ tt_sin = bf16_tensor_2dshard(sin_bhnd, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ tt_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+
+ # Apply RoPE on device
+ tt_output = ttnn.experimental.rotary_embedding_llama(tt_input, tt_cos, tt_sin, tt_trans_mat)
+
+ # Gather output back to host
+ concat_dims = [None, None]
+ concat_dims[sp_axis] = 2
+ concat_dims[tp_axis] = 1
+ tt_output_torch = ttnn.to_torch(
+ tt_output,
+ mesh_composer=ttnn.ConcatMesh2dToTensor(mesh_device, dims=concat_dims, mesh_shape=tuple(mesh_device.shape)),
+ )
+ tt_output_torch = tt_output_torch.permute(0, 2, 1, 3) # Back to (B, N, H, D)
+
+ # Compare
+ assert_quality(output_ref, tt_output_torch, pcc=0.99)
+ logger.info("PASSED: LTX interleaved RoPE matches PyTorch reference")
diff --git a/code/models/tt_dit/tests/models/ltx/test_transformer_ltx.py b/code/models/tt_dit/tests/models/ltx/test_transformer_ltx.py
new file mode 100644
index 0000000000000000000000000000000000000000..c811a9407c01211b4ad0ff53a002be861dfbaab6
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_transformer_ltx.py
@@ -0,0 +1,2113 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""LTX-2.3 transformer unit tests; structure mirrors test_transformer_wan.py.
+
+Torch reference is diffusers' LTX2VideoTransformer{Block,3DModel}. The TT runtime fuses RoPE
+into rotary_embedding_llama (INTERLEAVED + trans_mat) with a load-time Q/K permute, so every
+TT freq feed is INTERLEAVED. Modality axis selects video vs av; set LTX_SKIP_PCC=1 to assert
+shape/finiteness only.
+"""
+
+import os
+import time
+
+import pytest
+import torch
+from loguru import logger
+from safetensors.torch import load_file
+from tracy import signpost
+
+import ttnn
+from models.tt_dit.models.transformers.ltx.quant_config import LtxQuantProfile
+from models.tt_dit.models.transformers.ltx.rope_ltx import LTXRopeType, precompute_freqs_cis
+from models.tt_dit.models.transformers.ltx.transformer_ltx import (
+ LTXTransformerBlock,
+ LTXTransformerModel,
+ build_audio_masks,
+ build_video_pad_mask,
+)
+from models.tt_dit.parallel.config import DiTParallelConfig, ParallelFactor
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.mochi import get_rot_transformation_mat
+from models.tt_dit.utils.patchifiers import AudioLatentShape, VideoPixelShape
+from models.tt_dit.utils.tensor import bf16_tensor, bf16_tensor_2dshard
+from models.tt_dit.utils.test import (
+ line_params_req_exact_devices,
+ ring_params_4k,
+ ring_params_8k,
+ skip_if_unsupported_num_links,
+)
+from models.tt_dit.utils.tracing import Tracer
+
+from .ltx_mesh_params import LTX_TRANSFORMER_MESH_PARAMS, _4x8sp1tp0nl2_ring_is_fsdp0
+
+# ---------------------------------------------------------------------------
+# LTX-2.3-22B distilled transformer configuration
+# ---------------------------------------------------------------------------
+DIM = 4096
+NUM_HEADS = 32
+HEAD_DIM = DIM // NUM_HEADS # 128
+IN_CHANNELS = 128
+OUT_CHANNELS = 128
+CTX_DIM = 4096
+AUDIO_DIM = 2048
+AUDIO_HEAD_DIM = AUDIO_DIM // NUM_HEADS # 64
+AUDIO_IN_CHANNELS = 128
+AUDIO_CTX_DIM = 2048
+EPS = 1e-6
+PROMPT_LEN = 32
+
+# RoPE
+ROPE_THETA = 10000.0
+VIDEO_ROPE_MAX_POS = [20, 2048, 2048]
+AUDIO_ROPE_MAX_POS = [20]
+CROSS_PE_MAX_POS = [20]
+
+# AV-mode auxiliary
+AUDIO_N = 256 # sp-aligned for sp ∈ {1, 2, 4, 8}
+# 22B checkpoint variants (AV mode only): "fast" = distilled-1.1 (production), "dev" = base 22B.
+_CHECKPOINT_22B_VARIANTS = {
+ "fast": os.path.expanduser("~/.cache/ltx-checkpoints/ltx-2.3-22b-distilled-1.1.safetensors"),
+ "dev": os.path.expanduser("~/.cache/ltx-checkpoints/ltx-2.3-22b-dev.safetensors"),
+}
+
+
+# Fallback: the same checkpoints as cached by huggingface_hub (Lightricks/LTX-2.3 repo).
+_CHECKPOINT_22B_HF_FILENAMES = {
+ "fast": "ltx-2.3-22b-distilled-1.1.safetensors",
+ "dev": "ltx-2.3-22b-dev.safetensors",
+}
+
+
+def _resolve_checkpoint_22b(variant: str) -> str:
+ """Path for the 22B variant: LTX_CHECKPOINT env, then local cache, then huggingface_hub snapshot."""
+ env = os.environ.get("LTX_CHECKPOINT")
+ if env:
+ return env
+ local = _CHECKPOINT_22B_VARIANTS[variant]
+ if os.path.exists(local):
+ return local
+ snap_root = os.path.expanduser("~/.cache/huggingface/hub/models--Lightricks--LTX-2.3/snapshots")
+ if os.path.isdir(snap_root):
+ for snap in sorted(os.listdir(snap_root)):
+ cand = os.path.join(snap_root, snap, _CHECKPOINT_22B_HF_FILENAMES[variant])
+ if os.path.exists(cand):
+ return cand
+ return local
+
+
+# Block-test reference uses small scaled random weights; default init blows up the adaln chains.
+WEIGHT_SEED = 1234
+INPUT_SEED = 42
+TIMESTEP_VAL = 0.01
+
+# Toggle PCC verification via env (mirrors Wan's `dit_unit_test`). Default ON.
+_RUN_PCC_DEFAULT = {"1": False, "0": True}.get(os.environ.get("LTX_SKIP_PCC"), True)
+
+
+# ---------------------------------------------------------------------------
+# Parametrize lists
+# ---------------------------------------------------------------------------
+
+# 1080p fast-pipeline real latent grids (F, H_lat, W_lat) — the exact (latent_frames, h//32,
+# w//32) production feeds RoPE. These are NOT tile/SP-aligned, so the test SP-pads the sequence
+# (round F·H·W up to TILE * sp_factor) exactly like LTXPipeline._sp_pad_len, then crops back.
+_LTX_TRANSFORMER_SHAPE_PARAMS = [
+ pytest.param(19, 17, 30, id="stage_1"), # F·H·W = 9690 (real); SP-padded on device
+ pytest.param(19, 34, 60, id="stage_2"), # F·H·W = 38760 (real); SP-padded on device
+]
+
+_LTX_TRANSFORMER_MODALITY_PARAMS = [
+ pytest.param(False, id="video"),
+ pytest.param(True, id="av"),
+]
+
+_LTX_TRANSFORMER_RUN_PCC_PARAMS = [pytest.param(_RUN_PCC_DEFAULT, id="pcc" if _RUN_PCC_DEFAULT else "nopcc")]
+
+# Checkpoint variant for AV mode ("fast" = production default); ignored in video mode.
+_LTX_TRANSFORMER_CKPT_PARAMS = [
+ pytest.param("fast", id="ckpt_fast"),
+ pytest.param("dev", id="ckpt_dev"),
+]
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+def _interleaved_to_bhnd(t: torch.Tensor, num_heads: int) -> torch.Tensor:
+ """(B, N, dim) INTERLEAVED freqs → (B, num_heads, N, head_dim) for rotary_embedding_llama."""
+ B, N, dim = t.shape
+ head_dim = dim // num_heads
+ return t.reshape(B, N, num_heads, head_dim).permute(0, 2, 1, 3).contiguous()
+
+
+def _sp_pad_len(n_real: int, sp_factor: int) -> int:
+ """Round a seq len up to ttnn.TILE_SIZE * sp_factor (mirrors LTXPipeline._sp_pad_len)."""
+ divisor = ttnn.TILE_SIZE * sp_factor
+ return ((n_real + divisor - 1) // divisor) * divisor
+
+
+def _pad_seq_dim(t: torch.Tensor, n_pad: int, dim: int = -2) -> torch.Tensor:
+ """Zero-pad a torch tensor's sequence dim up to n_pad (no-op if already ≥ n_pad)."""
+ n = t.shape[dim]
+ if n_pad <= n:
+ return t
+ shape = list(t.shape)
+ shape[dim] = n_pad
+ out = torch.zeros(shape, dtype=t.dtype)
+ idx = [slice(None)] * t.ndim
+ idx[dim] = slice(0, n)
+ out[tuple(idx)] = t
+ return out
+
+
+def _pad_rope_bhnd(cos_i: torch.Tensor, sin_i: torch.Tensor, pad_to: int | None):
+ """Right-pad BHND INTERLEAVED freqs on the seq dim (2) with identity rotation (cos=1, sin=0).
+
+ Same convention as rope_ltx.pad_video_rope_sp: padded slots are no-op rotations; SDPA
+ still masks them out via logical_n / padding masks.
+ """
+ if pad_to is None or pad_to <= cos_i.shape[2]:
+ return cos_i, sin_i
+ pad = pad_to - cos_i.shape[2]
+ cos_i = torch.nn.functional.pad(cos_i, (0, 0, 0, pad), value=1.0)
+ sin_i = torch.nn.functional.pad(sin_i, (0, 0, 0, pad), value=0.0)
+ return cos_i, sin_i
+
+
+def _diffusers_qk_to_split(t: torch.Tensor, num_heads: int, head_dim: int) -> torch.Tensor:
+ """diffusers (interleaved-rotation) Q/K → Lightricks SPLIT convention the TT loader expects."""
+ D = head_dim
+ D_half = D // 2
+ inv = torch.empty(D, dtype=torch.long)
+ inv[:D_half] = torch.arange(0, D, 2)
+ inv[D_half:] = torch.arange(1, D, 2)
+ rest = t.shape[1:]
+ return t.reshape(num_heads, D, *rest).index_select(1, inv).reshape(num_heads * D, *rest)
+
+
+def _convert_diffusers_video_block_to_tt(state: dict, *, num_heads: int, head_dim: int) -> dict:
+ """diffusers LTX2VideoTransformerBlock state_dict → TT video block loader input (video subset, split Q/K)."""
+ keep_exact = {"scale_shift_table", "prompt_scale_shift_table"}
+ out = {k: v for k, v in state.items() if k in keep_exact or k.startswith(("attn1.", "attn2.", "ff."))}
+ for base in ("attn1", "attn2"):
+ for proj in ("to_q", "to_k"):
+ for suf in ("weight", "bias"):
+ kk = f"{base}.{proj}.{suf}"
+ if kk in out:
+ out[kk] = _diffusers_qk_to_split(out[kk], num_heads, head_dim)
+ for nk in ("norm_q.weight", "norm_k.weight"):
+ kk = f"{base}.{nk}"
+ if kk in out:
+ out[kk] = _diffusers_qk_to_split(out[kk], num_heads, head_dim)
+ return out
+
+
+def _make_diffusers_video_block():
+ """Build the diffusers LTX-2 block in the video config used as the PCC reference."""
+ from diffusers.models.transformers.transformer_ltx2 import LTX2VideoTransformerBlock
+
+ return LTX2VideoTransformerBlock(
+ dim=DIM,
+ num_attention_heads=NUM_HEADS,
+ attention_head_dim=HEAD_DIM,
+ cross_attention_dim=CTX_DIM,
+ audio_dim=AUDIO_DIM,
+ audio_num_attention_heads=NUM_HEADS,
+ audio_attention_head_dim=AUDIO_HEAD_DIM,
+ audio_cross_attention_dim=AUDIO_CTX_DIM,
+ video_gated_attn=False,
+ video_cross_attn_adaln=True,
+ audio_gated_attn=False,
+ audio_cross_attn_adaln=False,
+ eps=EPS,
+ rope_type="interleaved",
+ )
+
+
+def _diffusers_video_block_ref(block, *, x, context, temb, prompt_temb, cos_i, sin_i):
+ """Run the diffusers block video-only (a2v/v2a disabled) and return the video output."""
+ B, N, _ = x.shape
+ a_N = 32
+ audio_hidden = torch.zeros(B, a_N, AUDIO_DIM)
+ audio_enc = torch.zeros(B, context.shape[1], AUDIO_CTX_DIM)
+ temb_audio = torch.zeros(B, 1, 6 * AUDIO_DIM) # audio_cross_attn_adaln=False → 6 mod params
+ temb_prompt_audio = torch.zeros(B, 1, 2 * AUDIO_DIM)
+ with torch.no_grad():
+ out_v, _ = block(
+ hidden_states=x,
+ audio_hidden_states=audio_hidden,
+ encoder_hidden_states=context,
+ audio_encoder_hidden_states=audio_enc,
+ temb=temb,
+ temb_audio=temb_audio,
+ temb_ca_scale_shift=None,
+ temb_ca_audio_scale_shift=None,
+ temb_ca_gate=None,
+ temb_ca_audio_gate=None,
+ temb_prompt=prompt_temb,
+ temb_prompt_audio=temb_prompt_audio,
+ video_rotary_emb=(cos_i, sin_i),
+ audio_rotary_emb=None,
+ use_a2v_cross_attention=False,
+ use_v2a_cross_attention=False,
+ )
+ return out_v
+
+
+def _make_diffusers_video_model(num_layers: int = 1):
+ """diffusers LTX2VideoTransformer3DModel configured to match the TT video model."""
+ from diffusers.models.transformers.transformer_ltx2 import LTX2VideoTransformer3DModel
+
+ return LTX2VideoTransformer3DModel(
+ in_channels=IN_CHANNELS,
+ out_channels=OUT_CHANNELS,
+ num_attention_heads=NUM_HEADS,
+ attention_head_dim=HEAD_DIM,
+ cross_attention_dim=CTX_DIM,
+ audio_in_channels=AUDIO_IN_CHANNELS,
+ audio_out_channels=AUDIO_IN_CHANNELS,
+ audio_num_attention_heads=NUM_HEADS,
+ audio_attention_head_dim=AUDIO_HEAD_DIM,
+ audio_cross_attention_dim=AUDIO_CTX_DIM,
+ num_layers=num_layers,
+ cross_attn_mod=True,
+ audio_cross_attn_mod=False,
+ norm_eps=EPS,
+ rope_theta=10000.0,
+ rope_double_precision=False,
+ use_prompt_embeddings=False,
+ rope_type="interleaved",
+ )
+
+
+def _convert_diffusers_video_model_to_tt(state: dict, *, num_heads: int, head_dim: int) -> dict:
+ """diffusers LTX2VideoTransformer3DModel state_dict → TT LTXTransformerModel loader input."""
+ drop_top = ("audio_", "av_cross_attn_", "caption_projection")
+ rename_top = {"proj_in.": "patchify_proj.", "time_embed.": "adaln_single.", "prompt_adaln.": "prompt_adaln_single."}
+ out: dict = {}
+ for k, v in state.items():
+ if k.startswith("transformer_blocks."):
+ _, _idx, rest = k.split(".", 2)
+ if rest in ("scale_shift_table", "prompt_scale_shift_table") or rest.startswith(
+ ("attn1.", "attn2.", "ff.")
+ ):
+ out[k] = v
+ continue
+ if k.startswith(drop_top):
+ continue
+ nk = k
+ for a, b in rename_top.items():
+ if nk.startswith(a):
+ nk = b + nk[len(a) :]
+ break
+ out[nk] = v
+ qk_suffixes = ("to_q.weight", "to_q.bias", "to_k.weight", "to_k.bias", "norm_q.weight", "norm_k.weight")
+ for k in list(out.keys()):
+ if (".attn1." in k or ".attn2." in k) and k.endswith(qk_suffixes):
+ out[k] = _diffusers_qk_to_split(out[k], num_heads, head_dim)
+ return out
+
+
+def _diffusers_video_model_ref(model, *, video_lat, video_prompt, sigma_val, F, H, W):
+ """Run the diffusers 3D model video-only (isolate_modalities=True) and return video output."""
+ B, N, _ = video_lat.shape
+ gt, gh, gw, _ = _video_grid(F, H, W)
+ video_coords = torch.stack([gt, gh, gw], dim=0).float().unsqueeze(0) # (1, 3, N)
+ a_N = 64
+ audio_lat = torch.zeros(B, a_N, AUDIO_IN_CHANNELS)
+ audio_prompt = torch.zeros(B, video_prompt.shape[1], AUDIO_CTX_DIM)
+ audio_coords = torch.arange(a_N).reshape(1, 1, a_N).float()
+ ts = torch.tensor([sigma_val * 1000.0])
+ with torch.no_grad():
+ out = model(
+ hidden_states=video_lat,
+ audio_hidden_states=audio_lat,
+ encoder_hidden_states=video_prompt,
+ audio_encoder_hidden_states=audio_prompt,
+ timestep=ts,
+ audio_timestep=ts,
+ sigma=ts,
+ audio_sigma=ts,
+ video_coords=video_coords,
+ audio_coords=audio_coords,
+ isolate_modalities=True,
+ return_dict=False,
+ )
+ return out[0] if isinstance(out, (tuple, list)) else out
+
+
+def _diffusers_video_model_ref_pertoken(model, *, video_lat, video_prompt, video_ts_real, sigma_scalar, F, H, W):
+ """Per-token oracle: same as ``_diffusers_video_model_ref`` but ``timestep`` is ``(1, N)``.
+
+ The diffusers LTX2 model flattens ``timestep`` through ``time_embed`` then reshapes to
+ ``(B, N, ...)`` modulation, so a per-token timestep yields genuine per-token modulation.
+ ``sigma`` (prompt/cross modulation) stays scalar, matching the TT scalar-timestep path.
+ """
+ B, N, _ = video_lat.shape
+ gt, gh, gw, _ = _video_grid(F, H, W)
+ video_coords = torch.stack([gt, gh, gw], dim=0).float().unsqueeze(0) # (1, 3, N)
+ a_N = 64
+ audio_lat = torch.zeros(B, a_N, AUDIO_IN_CHANNELS)
+ audio_prompt = torch.zeros(B, video_prompt.shape[1], AUDIO_CTX_DIM)
+ audio_coords = torch.arange(a_N).reshape(1, 1, a_N).float()
+ ts_pertoken = video_ts_real.reshape(1, -1) * 1000.0 # (1, N)
+ ts_scalar = torch.tensor([sigma_scalar * 1000.0])
+ with torch.no_grad():
+ out = model(
+ hidden_states=video_lat,
+ audio_hidden_states=audio_lat,
+ encoder_hidden_states=video_prompt,
+ audio_encoder_hidden_states=audio_prompt,
+ timestep=ts_pertoken,
+ audio_timestep=ts_scalar,
+ sigma=ts_scalar,
+ audio_sigma=ts_scalar,
+ video_coords=video_coords,
+ audio_coords=audio_coords,
+ isolate_modalities=True,
+ return_dict=False,
+ )
+ return out[0] if isinstance(out, (tuple, list)) else out
+
+
+def _lightricks_qk_to_interleaved(t: torch.Tensor, num_heads: int, head_dim: int) -> torch.Tensor:
+ """Forward per-head SPLIT→INTERLEAVED Q/K permute (same as LTXAttention._permute_qk)."""
+ D = head_dim
+ D_half = D // 2
+ perm = torch.empty(D, dtype=torch.long)
+ perm[0::2] = torch.arange(D_half)
+ perm[1::2] = torch.arange(D_half, D)
+ rest = t.shape[1:]
+ return t.reshape(num_heads, D, *rest).index_select(1, perm).reshape(num_heads * D, *rest)
+
+
+# Per-attention head_dim for the AV block (video self/text-cross use 128; audio + cross-modal use 64).
+_AV_ATTN_HEAD_DIM = {
+ "attn1": HEAD_DIM,
+ "attn2": HEAD_DIM,
+ "audio_attn1": AUDIO_HEAD_DIM,
+ "audio_attn2": AUDIO_HEAD_DIM,
+ "audio_to_video_attn": AUDIO_HEAD_DIM,
+ "video_to_audio_attn": AUDIO_HEAD_DIM,
+}
+
+
+def _convert_lightricks_av_to_diffusers(state: dict, *, num_heads: int) -> dict:
+ """Raw Lightricks AV state_dict → diffusers LTX2VideoTransformer3DModel (AV) names."""
+ rename_top = {
+ "patchify_proj.": "proj_in.",
+ "adaln_single.": "time_embed.",
+ "prompt_adaln_single.": "prompt_adaln.",
+ "audio_patchify_proj.": "audio_proj_in.",
+ "audio_adaln_single.": "audio_time_embed.",
+ "audio_prompt_adaln_single.": "audio_prompt_adaln.",
+ "av_ca_video_scale_shift_adaln_single.": "av_cross_attn_video_scale_shift.",
+ "av_ca_audio_scale_shift_adaln_single.": "av_cross_attn_audio_scale_shift.",
+ "av_ca_a2v_gate_adaln_single.": "av_cross_attn_video_a2v_gate.",
+ "av_ca_v2a_gate_adaln_single.": "av_cross_attn_audio_v2a_gate.",
+ }
+ block_table_rename = {
+ "scale_shift_table_a2v_ca_video": "video_a2v_cross_attn_scale_shift_table",
+ "scale_shift_table_a2v_ca_audio": "audio_a2v_cross_attn_scale_shift_table",
+ }
+ out: dict = {}
+ for k, v in state.items():
+ if k.startswith("transformer_blocks."):
+ prefix, idx, rest = k.split(".", 2)
+ if rest in block_table_rename:
+ rest = block_table_rename[rest]
+ rest = rest.replace("q_norm", "norm_q").replace("k_norm", "norm_k")
+ out[f"{prefix}.{idx}.{rest}"] = v
+ else:
+ nk = k
+ for a, b in rename_top.items():
+ if nk.startswith(a):
+ nk = b + nk[len(a) :]
+ break
+ out[nk] = v
+ qk_suffixes = ("to_q.weight", "to_q.bias", "to_k.weight", "to_k.bias", "norm_q.weight", "norm_k.weight")
+ for k in list(out.keys()):
+ if not k.startswith("transformer_blocks."):
+ continue
+ attn = k.split(".")[2]
+ if attn in _AV_ATTN_HEAD_DIM and k.endswith(qk_suffixes):
+ out[k] = _lightricks_qk_to_interleaved(out[k], num_heads, _AV_ATTN_HEAD_DIM[attn])
+ return out
+
+
+def _make_diffusers_av_model(num_layers: int = 1):
+ """diffusers ``LTX2VideoTransformer3DModel`` configured to match the TT AV model (gated
+ attention, 9-param video+audio modulation, cross-modal a2v/v2a, no caption projection)."""
+ from diffusers.models.transformers.transformer_ltx2 import LTX2VideoTransformer3DModel
+
+ return LTX2VideoTransformer3DModel(
+ in_channels=IN_CHANNELS,
+ out_channels=OUT_CHANNELS,
+ num_attention_heads=NUM_HEADS,
+ attention_head_dim=HEAD_DIM,
+ cross_attention_dim=CTX_DIM,
+ audio_in_channels=AUDIO_IN_CHANNELS,
+ audio_out_channels=AUDIO_IN_CHANNELS,
+ audio_num_attention_heads=NUM_HEADS,
+ audio_attention_head_dim=AUDIO_HEAD_DIM,
+ audio_cross_attention_dim=AUDIO_CTX_DIM,
+ num_layers=num_layers,
+ gated_attn=True,
+ audio_gated_attn=True,
+ cross_attn_mod=True,
+ audio_cross_attn_mod=True,
+ norm_eps=EPS,
+ rope_theta=ROPE_THETA,
+ rope_double_precision=False,
+ use_prompt_embeddings=False,
+ rope_type="interleaved",
+ # =1 matches Lightricks' av_ca_timestep_scale_multiplier (a2v/v2a gate adaln sees raw sigma).
+ cross_attn_timestep_scale_multiplier=1,
+ )
+
+
+def _diffusers_av_model_ref(model, *, video_lat, video_prompt, audio_lat, audio_prompt, sigma_val, F, H, W, audio_N):
+ """Run the diffusers AV model (cross-modal enabled) and return (video, audio) outputs."""
+ gt, gh, gw, _ = _video_grid(F, H, W)
+ video_coords = torch.stack([gt, gh, gw], dim=0).float().unsqueeze(0) # (1, 3, N)
+ audio_coords = torch.arange(audio_N).reshape(1, 1, audio_N).float()
+ ts = torch.tensor([sigma_val * 1000.0])
+ with torch.no_grad():
+ out = model(
+ hidden_states=video_lat,
+ audio_hidden_states=audio_lat,
+ encoder_hidden_states=video_prompt,
+ audio_encoder_hidden_states=audio_prompt,
+ timestep=ts,
+ audio_timestep=ts,
+ sigma=ts,
+ audio_sigma=ts,
+ video_coords=video_coords,
+ audio_coords=audio_coords,
+ isolate_modalities=False,
+ return_dict=False,
+ )
+ return out[0], out[1]
+
+
+def _make_parallel_config(mesh_device, sp_axis, tp_axis):
+ return DiTParallelConfig(
+ cfg_parallel=ParallelFactor(factor=1, mesh_axis=0),
+ sequence_parallel=ParallelFactor(factor=tuple(mesh_device.shape)[sp_axis], mesh_axis=sp_axis),
+ tensor_parallel=ParallelFactor(factor=tuple(mesh_device.shape)[tp_axis], mesh_axis=tp_axis),
+ )
+
+
+def _make_ccl_manager(mesh_device, num_links, topology):
+ return CCLManager(mesh_device=mesh_device, num_links=num_links, topology=topology)
+
+
+def _video_grid(F, H, W):
+ """Return (grid_t, grid_h, grid_w) flat tensors and stacked 3D positions (1, N, 3)."""
+ t_ids, h_ids, w_ids = torch.arange(F), torch.arange(H), torch.arange(W)
+ gt, gh, gw = torch.meshgrid(t_ids, h_ids, w_ids, indexing="ij")
+ gt_f, gh_f, gw_f = gt.flatten(), gh.flatten(), gw.flatten()
+ positions = torch.stack([gt_f, gh_f, gw_f], dim=-1).float().unsqueeze(0)
+ return gt_f, gh_f, gw_f, positions
+
+
+def _video_rope_freqs(F, H, W, rope_type=LTXRopeType.SPLIT):
+ _, _, _, positions = _video_grid(F, H, W)
+ return precompute_freqs_cis(
+ positions,
+ dim=DIM,
+ out_dtype=torch.float32,
+ theta=ROPE_THETA,
+ max_pos=VIDEO_ROPE_MAX_POS,
+ num_attention_heads=NUM_HEADS,
+ rope_type=rope_type,
+ )
+
+
+def _video_cross_pe_freqs(F, H, W, rope_type=LTXRopeType.SPLIT):
+ """T-only cross-PE for V↔A, dim matches audio (AUDIO_DIM)."""
+ _, _, _, positions = _video_grid(F, H, W)
+ return precompute_freqs_cis(
+ positions[..., 0:1],
+ dim=AUDIO_DIM,
+ out_dtype=torch.float32,
+ theta=ROPE_THETA,
+ max_pos=CROSS_PE_MAX_POS,
+ num_attention_heads=NUM_HEADS,
+ rope_type=rope_type,
+ )
+
+
+def _audio_rope_freqs(audio_N, rope_type=LTXRopeType.SPLIT):
+ a_pos = torch.arange(audio_N).float().unsqueeze(0).unsqueeze(-1)
+ return precompute_freqs_cis(
+ a_pos,
+ dim=AUDIO_DIM,
+ out_dtype=torch.float32,
+ theta=ROPE_THETA,
+ max_pos=AUDIO_ROPE_MAX_POS,
+ num_attention_heads=NUM_HEADS,
+ rope_type=rope_type,
+ )
+
+
+def _audio_cross_pe_freqs(audio_N, rope_type=LTXRopeType.SPLIT):
+ a_pos = torch.arange(audio_N).float().unsqueeze(0).unsqueeze(-1)
+ return precompute_freqs_cis(
+ a_pos,
+ dim=AUDIO_DIM,
+ out_dtype=torch.float32,
+ theta=ROPE_THETA,
+ max_pos=CROSS_PE_MAX_POS,
+ num_attention_heads=NUM_HEADS,
+ rope_type=rope_type,
+ )
+
+
+def _audio_seq_lens(F: int, sp_factor: int) -> tuple[int, int]:
+ """Fast-pipeline audio seq lengths from the latent frame count (audio_N_real, padded to 32*sp)."""
+ num_frames = (F - 1) * 8 + 1
+ vps = VideoPixelShape(batch=1, frames=num_frames, height=64, width=64, fps=24)
+ audio_N_real = AudioLatentShape.from_video_pixel_shape(vps).frames
+ audio_N = ((audio_N_real + 32 * sp_factor - 1) // (32 * sp_factor)) * (32 * sp_factor)
+ return audio_N, audio_N_real
+
+
+def _tt_rope(freqs_fn, *args, mesh_device, sp_axis, tp_axis, pad_to=None):
+ """Build INTERLEAVED freqs for the TT runtime: precompute → BHND reshape → SP-pad → 2D shard."""
+ cos_i, sin_i = freqs_fn(*args, rope_type=LTXRopeType.INTERLEAVED)
+ cos_i = _interleaved_to_bhnd(cos_i, NUM_HEADS)
+ sin_i = _interleaved_to_bhnd(sin_i, NUM_HEADS)
+ cos_i, sin_i = _pad_rope_bhnd(cos_i, sin_i, pad_to)
+ tt_cos = bf16_tensor_2dshard(cos_i, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ tt_sin = bf16_tensor_2dshard(sin_i, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 1})
+ return tt_cos, tt_sin
+
+
+def _tt_rope_full(freqs_fn, *args, mesh_device, tp_axis, pad_to=None):
+ """INTERLEAVED freqs replicated full-seq (cross-attn K side): BHND reshape, SP-pad, no SP shard."""
+ cos_i, sin_i = freqs_fn(*args, rope_type=LTXRopeType.INTERLEAVED)
+ cos_i = _interleaved_to_bhnd(cos_i, NUM_HEADS)
+ sin_i = _interleaved_to_bhnd(sin_i, NUM_HEADS)
+ cos_i, sin_i = _pad_rope_bhnd(cos_i, sin_i, pad_to)
+ tt_cos = bf16_tensor(cos_i, device=mesh_device, mesh_axis=tp_axis, shard_dim=1)
+ tt_sin = bf16_tensor(sin_i, device=mesh_device, mesh_axis=tp_axis, shard_dim=1)
+ return tt_cos, tt_sin
+
+
+def _make_tt_block(*, mesh_device, ccl_manager, parallel_config, is_fsdp, has_audio, quant_config=None):
+ return LTXTransformerBlock(
+ video_dim=DIM,
+ video_ffn_dim=DIM * 4,
+ video_num_heads=NUM_HEADS,
+ video_cross_attention_dim=CTX_DIM,
+ audio_dim=AUDIO_DIM,
+ audio_ffn_dim=AUDIO_DIM * 4,
+ audio_num_heads=NUM_HEADS,
+ audio_cross_attention_dim=AUDIO_CTX_DIM,
+ eps=EPS,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=has_audio,
+ apply_gated_attention=has_audio,
+ cross_attention_adaln=True,
+ quant_config=quant_config,
+ )
+
+
+def _make_tt_model(
+ *, mesh_device, ccl_manager, parallel_config, is_fsdp, has_audio, num_layers, image_conditioning=False
+):
+ return LTXTransformerModel(
+ num_attention_heads=NUM_HEADS,
+ attention_head_dim=HEAD_DIM,
+ in_channels=IN_CHANNELS,
+ out_channels=OUT_CHANNELS,
+ num_layers=num_layers,
+ cross_attention_dim=CTX_DIM,
+ audio_num_attention_heads=NUM_HEADS,
+ audio_attention_head_dim=AUDIO_HEAD_DIM,
+ audio_in_channels=AUDIO_IN_CHANNELS,
+ audio_out_channels=AUDIO_IN_CHANNELS,
+ audio_cross_attention_dim=AUDIO_CTX_DIM,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=has_audio,
+ apply_gated_attention=has_audio,
+ cross_attention_adaln=True,
+ image_conditioning=image_conditioning,
+ )
+
+
+def _load_22b_state_dict(num_layers: int, checkpoint_path: str) -> dict | None:
+ """Load LTX-2.3-22B weights filtered to the first num_layers blocks; None if checkpoint missing."""
+ if not os.path.exists(checkpoint_path):
+ return None
+ raw = load_file(checkpoint_path)
+ prefix = "model.diffusion_model."
+ sd = {k[len(prefix) :]: v for k, v in raw.items() if k.startswith(prefix)}
+ return {k: v for k, v in sd.items() if not k.startswith("transformer_blocks.") or int(k.split(".")[1]) < num_layers}
+
+
+def _scale_init_(module: torch.nn.Module, seed: int = WEIGHT_SEED) -> None:
+ """Deterministic N(0, 0.1²) reinit in-place. See block test comment for why."""
+ torch.manual_seed(seed)
+ with torch.no_grad():
+ for p in module.parameters():
+ p.copy_(torch.randn(p.shape, dtype=p.dtype) * 0.1)
+
+
+# ---------------------------------------------------------------------------
+# Tests
+# ---------------------------------------------------------------------------
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ LTX_TRANSFORMER_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(("F", "H", "W"), _LTX_TRANSFORMER_SHAPE_PARAMS)
+@pytest.mark.parametrize("has_audio", _LTX_TRANSFORMER_MODALITY_PARAMS)
+@pytest.mark.parametrize("run_pcc", _LTX_TRANSFORMER_RUN_PCC_PARAMS)
+@pytest.mark.parametrize("checkpoint_variant", _LTX_TRANSFORMER_CKPT_PARAMS)
+def test_ltx_transformer_block(
+ mesh_device: ttnn.MeshDevice,
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+ is_fsdp: bool,
+ F: int,
+ H: int,
+ W: int,
+ has_audio: bool,
+ run_pcc: bool,
+ checkpoint_variant: str,
+ reset_seeds,
+) -> None:
+ """Test LTXTransformerBlock: TT forward, with optional PCC vs the diffusers LTX-2 block."""
+ # Checkpoint variant only affects AV weight loading; skip the redundant copy in video mode.
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ if not has_audio and checkpoint_variant != "fast":
+ pytest.skip("checkpoint_variant only affects AV mode (video uses random scaled weights)")
+ checkpoint_22b = _resolve_checkpoint_22b(checkpoint_variant)
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ # Real latent grid → SP-padded sequence (mirrors LTXPipeline). video_N is the padded device
+ # length; video_N_real is the logical token count fed to SDPA's logical_n / padding masks.
+ video_N_real = F * H * W
+ video_N = _sp_pad_len(video_N_real, sp_factor)
+ # Audio seq length mirrors the fast pipeline so op sizes / padding match production.
+ audio_N, audio_N_real = _audio_seq_lens(F, sp_factor)
+ assert video_N % (32 * sp_factor) == 0, f"video_N={video_N} not sp/tile-aligned for sp={sp_factor}"
+ assert audio_N % (32 * sp_factor) == 0, f"audio_N={audio_N} not sp/tile-aligned for sp={sp_factor}"
+
+ # AV mode lacks the LTXModel-level cross-PE plumbing, so it asserts shape/finiteness only.
+ do_pcc = run_pcc and not has_audio
+ torch_block = None
+ torch_out = None
+ if do_pcc:
+ # Reference is the diffusers LTX-2 block run video-only (audio + a2v/v2a disabled).
+ torch_block = _make_diffusers_video_block()
+ torch_block.eval()
+ _scale_init_(torch_block)
+
+ # TT block
+ ccl_manager = _make_ccl_manager(mesh_device, num_links, topology)
+ parallel_config = _make_parallel_config(mesh_device, sp_axis, tp_axis)
+ tt_block = _make_tt_block(
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=has_audio,
+ )
+
+ if do_pcc:
+ # diffusers→TT: keep video subset, convert attn Q/K to split. Clone to avoid aliasing.
+ conv = _convert_diffusers_video_block_to_tt(torch_block.state_dict(), num_heads=NUM_HEADS, head_dim=HEAD_DIM)
+ tt_block.load_torch_state_dict({k: v.detach().clone() for k, v in conv.items()})
+ elif has_audio:
+ sd = _load_22b_state_dict(num_layers=1, checkpoint_path=checkpoint_22b)
+ if sd is None:
+ pytest.skip(f"22B checkpoint not found at {checkpoint_22b}")
+ block_sd = {
+ k[len("transformer_blocks.0.") :]: v for k, v in sd.items() if k.startswith("transformer_blocks.0.")
+ }
+ tt_block.load_torch_state_dict(block_sd, strict=False)
+ else:
+ # video mode, PCC disabled — harvest a shape-matching state dict from a throwaway block.
+ dummy = _make_diffusers_video_block()
+ _scale_init_(dummy)
+ conv = _convert_diffusers_video_block_to_tt(dummy.state_dict(), num_heads=NUM_HEADS, head_dim=HEAD_DIM)
+ tt_block.load_torch_state_dict({k: v.detach().clone() for k, v in conv.items()})
+ del dummy
+
+ # Inputs (real-grid token count; the TT side gets a zero-padded copy below).
+ torch.manual_seed(INPUT_SEED)
+ x = torch.randn(1, video_N_real, DIM, dtype=torch.float32)
+ context = torch.randn(1, PROMPT_LEN, CTX_DIM, dtype=torch.float32)
+ temb = torch.randn(1, 1, 9 * DIM, dtype=torch.float32) # 9 adaln params
+ prompt_temb = torch.randn(1, 1, 2 * DIM, dtype=torch.float32) # 2 adaln params for prompt
+
+ # Torch forward (video-only branch): diffusers block consumes INTERLEAVED cos/sin, same
+ # convention as the TT runtime (tt_dit interleaved freqs ≈ diffusers rope, PCC ~1.0).
+ if do_pcc:
+ cos_int, sin_int = _video_rope_freqs(F, H, W, rope_type=LTXRopeType.INTERLEAVED)
+ torch_out = _diffusers_video_block_ref(
+ torch_block, x=x, context=context, temb=temb, prompt_temb=prompt_temb, cos_i=cos_int, sin_i=sin_int
+ )
+ logger.info(f"torch block output {tuple(torch_out.shape)}")
+
+ # TT video tensors. Runtime rope is INTERLEAVED + trans_mat. Sequence is SP-padded to video_N;
+ # rope pads with identity rotation; video_N (logical) goes to the block for SDPA masking.
+ spatial = _pad_seq_dim(x, video_N, dim=1).unsqueeze(0)
+ tt_spatial = bf16_tensor_2dshard(spatial, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 3})
+ tt_prompt = bf16_tensor(context.unsqueeze(0), device=mesh_device)
+ # Outer-param layout (coeff, B=1, 1, D) so the block's chunk(dim=0) is a free tile-aligned slice.
+ tt_temb = bf16_tensor(
+ temb.reshape(9, DIM).unsqueeze(1).unsqueeze(1), device=mesh_device, mesh_axis=tp_axis, shard_dim=3
+ )
+ tt_prompt_temb = bf16_tensor(prompt_temb.reshape(2, DIM).unsqueeze(1).unsqueeze(1), device=mesh_device)
+ tt_cos, tt_sin = _tt_rope(
+ _video_rope_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ tt_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+
+ forward_kwargs = dict(
+ video_1BND=tt_spatial,
+ video_prompt=tt_prompt,
+ video_temb=tt_temb,
+ video_N=video_N_real,
+ video_rope_cos=tt_cos,
+ video_rope_sin=tt_sin,
+ trans_mat=tt_trans_mat,
+ video_prompt_temb=tt_prompt_temb,
+ )
+ if has_audio:
+ # Real tokens in [:audio_N_real], zeros in the padded tail (matches the padded audio latent).
+ a_x = torch.zeros(1, audio_N, AUDIO_DIM, dtype=torch.float32)
+ a_x[:, :audio_N_real, :] = torch.randn(1, audio_N_real, AUDIO_DIM, dtype=torch.float32)
+ a_ctx = torch.randn(1, PROMPT_LEN, AUDIO_CTX_DIM, dtype=torch.float32)
+ a_temb = torch.randn(1, 1, 9 * AUDIO_DIM, dtype=torch.float32)
+ a_prompt_temb = torch.randn(1, 1, 2 * AUDIO_DIM, dtype=torch.float32)
+ av_ca_v = torch.randn(1, 1, 5 * DIM, dtype=torch.float32) # 4 scale-shift + 1 gate
+ av_ca_a = torch.randn(1, 1, 5 * AUDIO_DIM, dtype=torch.float32)
+
+ a_cos, a_sin = _tt_rope(_audio_rope_freqs, audio_N, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis)
+ vx_cos, vx_sin = _tt_rope(
+ _video_cross_pe_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ ax_cos, ax_sin = _tt_rope(
+ _audio_cross_pe_freqs, audio_N, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis
+ )
+ ax_cos_full, ax_sin_full = _tt_rope_full(
+ _audio_cross_pe_freqs, audio_N, mesh_device=mesh_device, tp_axis=tp_axis
+ )
+
+ # Padding masks, same construction as the fast pipeline (audio padded, video aligned).
+ a_attn_mask, a_pad_sp, a_pad_full = build_audio_masks(
+ audio_N, audio_N_real, mesh_device=mesh_device, sp_axis=sp_axis
+ )
+ v_pad_sp = build_video_pad_mask(video_N, video_N_real, mesh_device=mesh_device, sp_axis=sp_axis)
+
+ forward_kwargs.update(
+ audio_1BND=bf16_tensor_2dshard(
+ a_x.unsqueeze(0), device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 3}
+ ),
+ audio_prompt=bf16_tensor(a_ctx.unsqueeze(0), device=mesh_device),
+ audio_temb=bf16_tensor(
+ a_temb.reshape(9, AUDIO_DIM).unsqueeze(1).unsqueeze(1),
+ device=mesh_device,
+ mesh_axis=tp_axis,
+ shard_dim=3,
+ ),
+ audio_prompt_temb=bf16_tensor(
+ a_prompt_temb.reshape(2, AUDIO_DIM).unsqueeze(1).unsqueeze(1), device=mesh_device
+ ),
+ av_ca_temb=bf16_tensor(
+ av_ca_v.reshape(5, DIM).unsqueeze(1).unsqueeze(1), device=mesh_device, mesh_axis=tp_axis, shard_dim=3
+ ),
+ av_ca_audio_temb=bf16_tensor(
+ av_ca_a.reshape(5, AUDIO_DIM).unsqueeze(1).unsqueeze(1),
+ device=mesh_device,
+ mesh_axis=tp_axis,
+ shard_dim=3,
+ ),
+ audio_N=audio_N,
+ audio_rope_cos=a_cos,
+ audio_rope_sin=a_sin,
+ video_cross_pe_cos=vx_cos,
+ video_cross_pe_sin=vx_sin,
+ audio_cross_pe_cos=ax_cos,
+ audio_cross_pe_sin=ax_sin,
+ audio_cross_pe_cos_full=ax_cos_full,
+ audio_cross_pe_sin_full=ax_sin_full,
+ audio_attn_mask=a_attn_mask,
+ audio_padding_mask=a_pad_sp,
+ audio_padding_mask_full=a_pad_full,
+ video_padding_mask=v_pad_sp,
+ )
+
+ # Signpost-bracket one warm forward for tt-perf-report --start/end-signpost.
+ for _ in range(2):
+ tt_out = tt_block(**forward_kwargs)
+ ttnn.synchronize_device(mesh_device)
+ signpost("start")
+ tt_out = tt_block(**forward_kwargs)
+ ttnn.synchronize_device(mesh_device)
+ signpost("stop")
+
+ if has_audio:
+ tt_v, tt_a = tt_out
+ else:
+ tt_v = tt_out
+
+ # Gather video output
+ concat_dims = [None, None]
+ concat_dims[sp_axis] = 2
+ concat_dims[tp_axis] = 3
+ tt_v_torch = ttnn.to_torch(
+ tt_v,
+ mesh_composer=ttnn.ConcatMesh2dToTensor(mesh_device, dims=concat_dims, mesh_shape=tuple(mesh_device.shape)),
+ ).squeeze(0)
+
+ # Crop the SP-padding tail off the gathered video before checking/comparing real tokens.
+ tt_v_torch = tt_v_torch[:, :video_N_real, :]
+ assert tt_v_torch.shape == (1, video_N_real, DIM), f"video shape {tt_v_torch.shape}"
+ assert torch.isfinite(tt_v_torch).all(), "video output NaN/Inf"
+
+ if has_audio:
+ tt_a_torch = ttnn.to_torch(
+ tt_a,
+ mesh_composer=ttnn.ConcatMesh2dToTensor(mesh_device, dims=concat_dims, mesh_shape=tuple(mesh_device.shape)),
+ ).squeeze(0)[:, :audio_N_real, :]
+ assert tt_a_torch.shape == (1, audio_N_real, AUDIO_DIM), f"audio shape {tt_a_torch.shape}"
+ assert torch.isfinite(tt_a_torch).all(), "audio output NaN/Inf"
+
+ if do_pcc:
+ # 4x8 mesh has 8-way SP ring all-gathers — looser tolerance.
+ pcc = 0.988 if mesh_device.get_num_devices() > 8 else 0.999
+ rmse = 0.10 if mesh_device.get_num_devices() > 8 else 0.032
+ assert_quality(torch_out, tt_v_torch, pcc=pcc, relative_rmse=rmse)
+ logger.info(f"PASSED block PCC: video {tuple(tt_v_torch.shape)}")
+ else:
+ logger.info(f"PASSED block (no PCC): video {tuple(tt_v_torch.shape)}")
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ [_4x8sp1tp0nl2_ring_is_fsdp0],
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_transformer_block_bf8(
+ mesh_device: ttnn.MeshDevice,
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+ is_fsdp: bool,
+ reset_seeds,
+) -> None:
+ """Shipped bf8 DiT-linear quant path (``all_bf8_lofi``) on the production 4x8 ring.
+
+ Covers two things a bf16-only run cannot: (1) the profile actually produced bf8 device weights with
+ the ``to_out`` carve-out kept bf16 — a mis-wired role or a silent no-quant fails the dtype asserts;
+ (2) the bf8 forward still tracks the fp32 diffusers oracle. Video-only, matching the block test's
+ PCC path; the gate-role carve-out only exists under gated (audio) attention, so it is out of scope.
+ """
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ F, H, W = 19, 17, 30 # stage_1 latent grid
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ video_N_real = F * H * W
+ video_N = _sp_pad_len(video_N_real, sp_factor)
+
+ torch_block = _make_diffusers_video_block()
+ torch_block.eval()
+ _scale_init_(torch_block)
+
+ ccl_manager = _make_ccl_manager(mesh_device, num_links, topology)
+ parallel_config = _make_parallel_config(mesh_device, sp_axis, tp_axis)
+ tt_block = _make_tt_block(
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=False,
+ quant_config=LtxQuantProfile.all_bf8_lofi(),
+ )
+ conv = _convert_diffusers_video_block_to_tt(torch_block.state_dict(), num_heads=NUM_HEADS, head_dim=HEAD_DIM)
+ tt_block.load_torch_state_dict({k: v.detach().clone() for k, v in conv.items()})
+
+ # Assert the loaded device-tensor dtypes the profile is supposed to map: projections bf8, the
+ # fused-addcmul to_out carve-out bf16. A wrong role or a silent no-quant (bf16 everywhere) fails here.
+ def wdtype(linear):
+ return linear.weight.data.dtype
+
+ assert wdtype(tt_block.attn1.to_qkv) == ttnn.bfloat8_b, "self-attn qkv should be bf8"
+ assert wdtype(tt_block.attn1.to_out) == ttnn.bfloat16, "self-attn to_out carve-out should stay bf16"
+ assert wdtype(tt_block.attn2.to_q) == ttnn.bfloat8_b, "cross-attn q should be bf8"
+ assert wdtype(tt_block.attn2.to_kv) == ttnn.bfloat8_b, "cross-attn kv should be bf8"
+ assert wdtype(tt_block.attn2.to_out) == ttnn.bfloat16, "cross-attn to_out carve-out should stay bf16"
+ assert wdtype(tt_block.ffn.ff1) == ttnn.bfloat8_b, "ff1 should be bf8"
+ assert wdtype(tt_block.ffn.ff2) == ttnn.bfloat8_b, "ff2 should be bf8"
+
+ # Activation casts ride LTX_QUANT_ACTIVATIONS (default on): the projections and the bf16-weight
+ # to_out both narrow their activation to bf8. PCC can't catch a missing cast (bf16 activations only
+ # raise it), so assert it directly.
+ assert tt_block.attn1.to_qkv.activation_dtype == ttnn.bfloat8_b, "self-attn qkv activation should be bf8"
+ assert tt_block.attn1.to_out.activation_dtype == ttnn.bfloat8_b, "to_out consumes the shared bf8 activation"
+ assert tt_block.attn2.to_q.activation_dtype == ttnn.bfloat8_b, "cross-attn q activation should be bf8"
+
+ torch.manual_seed(INPUT_SEED)
+ x = torch.randn(1, video_N_real, DIM, dtype=torch.float32)
+ context = torch.randn(1, PROMPT_LEN, CTX_DIM, dtype=torch.float32)
+ temb = torch.randn(1, 1, 9 * DIM, dtype=torch.float32)
+ prompt_temb = torch.randn(1, 1, 2 * DIM, dtype=torch.float32)
+ cos_int, sin_int = _video_rope_freqs(F, H, W, rope_type=LTXRopeType.INTERLEAVED)
+ torch_out = _diffusers_video_block_ref(
+ torch_block, x=x, context=context, temb=temb, prompt_temb=prompt_temb, cos_i=cos_int, sin_i=sin_int
+ )
+
+ spatial = _pad_seq_dim(x, video_N, dim=1).unsqueeze(0)
+ tt_spatial = bf16_tensor_2dshard(spatial, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 3})
+ tt_prompt = bf16_tensor(context.unsqueeze(0), device=mesh_device)
+ tt_temb = bf16_tensor(
+ temb.reshape(9, DIM).unsqueeze(1).unsqueeze(1), device=mesh_device, mesh_axis=tp_axis, shard_dim=3
+ )
+ tt_prompt_temb = bf16_tensor(prompt_temb.reshape(2, DIM).unsqueeze(1).unsqueeze(1), device=mesh_device)
+ tt_cos, tt_sin = _tt_rope(
+ _video_rope_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ tt_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+ tt_out = tt_block(
+ video_1BND=tt_spatial,
+ video_prompt=tt_prompt,
+ video_temb=tt_temb,
+ video_N=video_N_real,
+ video_rope_cos=tt_cos,
+ video_rope_sin=tt_sin,
+ trans_mat=tt_trans_mat,
+ video_prompt_temb=tt_prompt_temb,
+ )
+
+ concat_dims = [None, None]
+ concat_dims[sp_axis] = 2
+ concat_dims[tp_axis] = 3
+ tt_v = ttnn.to_torch(
+ tt_out,
+ mesh_composer=ttnn.ConcatMesh2dToTensor(mesh_device, dims=concat_dims, mesh_shape=tuple(mesh_device.shape)),
+ ).squeeze(0)[:, :video_N_real, :]
+ assert tt_v.shape == (1, video_N_real, DIM), f"video shape {tt_v.shape}"
+ assert torch.isfinite(tt_v).all(), "bf8 video output NaN/Inf"
+
+ # bf8 measures PCC 99.93% / RMSE 4.2% on BH 4x8. Floor PCC at the bf16 block test's ring-noise
+ # tolerance (0.988), not tighter — the RMSE gate (0.08 vs observed 0.042) is the real quant-
+ # regression catch; a broken quant path craters PCC or blows up RMSE and trips here regardless.
+ assert_quality(torch_out, tt_v, pcc=0.988, relative_rmse=0.08)
+ logger.info(f"PASSED bf8 block PCC: video {tuple(tt_v.shape)}")
+
+
+def _run_inner_step(
+ *,
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ topology,
+ is_fsdp,
+ F,
+ H,
+ W,
+ has_audio,
+ run_pcc,
+ checkpoint_variant: str,
+ use_forward_alias: bool,
+):
+ """Shared body for the model forward tests; use_forward_alias picks tt_model(...) vs .forward(...)."""
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ # Checkpoint variant only affects AV weight loading; skip the redundant copy in video mode.
+ if not has_audio and checkpoint_variant != "fast":
+ pytest.skip("checkpoint_variant only affects AV mode (video uses random scaled weights)")
+ checkpoint_22b = _resolve_checkpoint_22b(checkpoint_variant)
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ # Real latent grid → SP-padded sequence (mirrors LTXPipeline). video_N_real is the logical
+ # token count (SDPA logical_n / pad masks); video_N is the padded on-device length.
+ video_N_real = F * H * W
+ video_N = _sp_pad_len(video_N_real, sp_factor)
+ audio_N = AUDIO_N
+ assert video_N % (32 * sp_factor) == 0
+ if has_audio:
+ assert audio_N % (32 * sp_factor) == 0
+
+ # === Reference model ===
+ do_pcc = run_pcc
+ ref_video = None
+ ref_audio = None
+ state_dict = None
+
+ if has_audio:
+ # AV PCC path: 22B with strict=False.
+ state_dict = _load_22b_state_dict(num_layers=1, checkpoint_path=checkpoint_22b)
+ if state_dict is None:
+ pytest.skip(f"22B checkpoint not found at {checkpoint_22b}")
+ else:
+ # Video PCC path: random scaled weights (1 layer) from the diffusers 3D model.
+ torch_model = _make_diffusers_video_model(num_layers=1)
+ torch_model.eval()
+ _scale_init_(torch_model)
+ state_dict = _convert_diffusers_video_model_to_tt(
+ torch_model.state_dict(), num_heads=NUM_HEADS, head_dim=HEAD_DIM
+ )
+ state_dict = {k: v.detach().clone() for k, v in state_dict.items()}
+
+ # === Inputs ===
+ # Real-grid latent for the torch reference; the TT side gets a zero-padded copy (video_N).
+ torch.manual_seed(INPUT_SEED)
+ video_lat_real = torch.randn(1, video_N_real, IN_CHANNELS, dtype=torch.float32)
+ video_lat = _pad_seq_dim(video_lat_real, video_N, dim=1)
+ video_prompt = torch.randn(1, PROMPT_LEN, CTX_DIM, dtype=torch.float32)
+ sigma_val = TIMESTEP_VAL if not has_audio else 0.5 # AV path uses production sigma=0.5
+ timestep_torch = torch.tensor([sigma_val])
+
+ audio_lat = None
+ audio_prompt = None
+ if has_audio:
+ audio_lat = torch.randn(1, audio_N, AUDIO_IN_CHANNELS, dtype=torch.float32)
+ audio_prompt = torch.randn(1, PROMPT_LEN, AUDIO_CTX_DIM, dtype=torch.float32)
+
+ # === Reference forward (before TT to avoid weight aliasing) ===
+ # The torch LTXModel computes rope internally from positions; only the TT side builds freqs.
+ if do_pcc and not has_audio:
+ ref_video = _diffusers_video_model_ref(
+ torch_model, video_lat=video_lat_real, video_prompt=video_prompt, sigma_val=sigma_val, F=F, H=H, W=W
+ )
+ logger.info(f"ref video {tuple(ref_video.shape)} range=[{ref_video.min():.3f}, {ref_video.max():.3f}]")
+ del torch_model
+
+ if do_pcc and has_audio:
+ # AV reference: load the raw 22B into the diffusers AV model and run the cross-modal forward.
+ ref_model = _make_diffusers_av_model(num_layers=1)
+ ref_model.load_state_dict(_convert_lightricks_av_to_diffusers(state_dict, num_heads=NUM_HEADS), strict=False)
+ ref_model.eval()
+ t0 = time.time()
+ ref_video, ref_audio = _diffusers_av_model_ref(
+ ref_model,
+ video_lat=video_lat_real,
+ video_prompt=video_prompt,
+ audio_lat=audio_lat,
+ audio_prompt=audio_prompt,
+ sigma_val=sigma_val,
+ F=F,
+ H=H,
+ W=W,
+ audio_N=audio_N,
+ )
+ logger.info(
+ f"AV ref forward: {time.time() - t0:.1f}s — video {tuple(ref_video.shape)} audio {tuple(ref_audio.shape)}"
+ )
+ del ref_model
+ import gc
+
+ gc.collect()
+
+ # === TT model ===
+ ccl_manager = _make_ccl_manager(mesh_device, num_links, topology)
+ parallel_config = _make_parallel_config(mesh_device, sp_axis, tp_axis)
+ tt_model = _make_tt_model(
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=has_audio,
+ num_layers=1,
+ )
+ t0 = time.time()
+ tt_model.load_torch_state_dict(state_dict, strict=not has_audio)
+ logger.info(f"state-dict load: {time.time() - t0:.1f}s")
+
+ # === Build TT-side RoPE / cross-PE / prompt tensors (INTERLEAVED + trans_mat) ===
+ tt_video_prompt = bf16_tensor(video_prompt.unsqueeze(0), device=mesh_device)
+ tt_vc, tt_vs = _tt_rope(
+ _video_rope_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ tt_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+
+ call_kwargs = dict(
+ video_1BNI_torch=video_lat.unsqueeze(0),
+ video_prompt_1BLP=tt_video_prompt,
+ video_rope_cos=tt_vc,
+ video_rope_sin=tt_vs,
+ video_N=video_N_real,
+ trans_mat=tt_trans_mat,
+ timestep_torch=timestep_torch,
+ )
+ if has_audio:
+ a_cos, a_sin = _tt_rope(_audio_rope_freqs, audio_N, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis)
+ vx_cos, vx_sin = _tt_rope(
+ _video_cross_pe_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ ax_cos, ax_sin = _tt_rope(
+ _audio_cross_pe_freqs, audio_N, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis
+ )
+ ax_cos_full, ax_sin_full = _tt_rope_full(
+ _audio_cross_pe_freqs, audio_N, mesh_device=mesh_device, tp_axis=tp_axis
+ )
+ # Zero padded video tokens as V→A cross-attention keys (matches build_video_pad_mask).
+ v_pad_sp = build_video_pad_mask(video_N, video_N_real, mesh_device=mesh_device, sp_axis=sp_axis)
+ call_kwargs.update(
+ audio_1BNI_torch=audio_lat.unsqueeze(0),
+ audio_prompt_1BLP=bf16_tensor(audio_prompt.unsqueeze(0), device=mesh_device),
+ audio_rope_cos=a_cos,
+ audio_rope_sin=a_sin,
+ audio_N=audio_N,
+ video_cross_pe_cos=vx_cos,
+ video_cross_pe_sin=vx_sin,
+ audio_cross_pe_cos=ax_cos,
+ audio_cross_pe_sin=ax_sin,
+ audio_cross_pe_cos_full=ax_cos_full,
+ audio_cross_pe_sin_full=ax_sin_full,
+ video_padding_mask=v_pad_sp,
+ )
+
+ # === Forward ===
+ t0 = time.time()
+ result = tt_model(**call_kwargs) if use_forward_alias else tt_model.forward(**call_kwargs)
+ logger.info(f"TT forward: {time.time() - t0:.1f}s")
+
+ # Crop the SP-padding tail off the gathered video output before checking/comparing real tokens.
+ if has_audio:
+ tt_v_dev, tt_a_dev = result
+ tt_video = LTXTransformerModel.device_to_host(tt_v_dev).squeeze(0)[:, :video_N_real, :]
+ tt_audio = LTXTransformerModel.device_to_host(tt_a_dev).squeeze(0)
+ assert tt_video.shape == (1, video_N_real, OUT_CHANNELS), f"video shape {tt_video.shape}"
+ assert tt_audio.shape == (1, audio_N, AUDIO_IN_CHANNELS), f"audio shape {tt_audio.shape}"
+ assert torch.isfinite(tt_video).all() and torch.isfinite(tt_audio).all(), "NaN/Inf in TT output"
+ else:
+ tt_video = LTXTransformerModel.device_to_host(result).squeeze(0)[:, :video_N_real, :]
+ assert tt_video.shape == (1, video_N_real, OUT_CHANNELS), f"video shape {tt_video.shape}"
+ assert torch.isfinite(tt_video).all(), "NaN/Inf in TT output"
+
+ del tt_model
+
+ if do_pcc:
+ assert_quality(ref_video, tt_video, pcc=0.992, relative_rmse=0.15)
+ if has_audio:
+ assert_quality(ref_audio, tt_audio, pcc=0.992, relative_rmse=0.15)
+ logger.info("PASSED PCC")
+ else:
+ logger.info("PASSED (no PCC)")
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ LTX_TRANSFORMER_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(("F", "H", "W"), _LTX_TRANSFORMER_SHAPE_PARAMS)
+@pytest.mark.parametrize("has_audio", _LTX_TRANSFORMER_MODALITY_PARAMS)
+@pytest.mark.parametrize("run_pcc", _LTX_TRANSFORMER_RUN_PCC_PARAMS)
+@pytest.mark.parametrize("checkpoint_variant", _LTX_TRANSFORMER_CKPT_PARAMS)
+def test_ltx_transformer_model(
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ topology,
+ is_fsdp,
+ F,
+ H,
+ W,
+ has_audio,
+ run_pcc,
+ checkpoint_variant,
+ reset_seeds,
+) -> None:
+ """Full LTXTransformerModel forward via ``__call__``."""
+ _run_inner_step(
+ mesh_device=mesh_device,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ F=F,
+ H=H,
+ W=W,
+ has_audio=has_audio,
+ run_pcc=run_pcc,
+ checkpoint_variant=checkpoint_variant,
+ use_forward_alias=True,
+ )
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ LTX_TRANSFORMER_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(("F", "H", "W"), _LTX_TRANSFORMER_SHAPE_PARAMS)
+@pytest.mark.parametrize("has_audio", _LTX_TRANSFORMER_MODALITY_PARAMS)
+@pytest.mark.parametrize("run_pcc", _LTX_TRANSFORMER_RUN_PCC_PARAMS)
+@pytest.mark.parametrize("checkpoint_variant", _LTX_TRANSFORMER_CKPT_PARAMS)
+def test_ltx_transformer_inner_step(
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ topology,
+ is_fsdp,
+ F,
+ H,
+ W,
+ has_audio,
+ run_pcc,
+ checkpoint_variant,
+ reset_seeds,
+) -> None:
+ """LTXTransformerModel.forward — explicit call, denoising-loop path."""
+ _run_inner_step(
+ mesh_device=mesh_device,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ F=F,
+ H=H,
+ W=W,
+ has_audio=has_audio,
+ run_pcc=run_pcc,
+ checkpoint_variant=checkpoint_variant,
+ use_forward_alias=False,
+ )
+
+
+# ---------------------------------------------------------------------------
+# Per-token video timestep (I2V) — equivalence to the scalar path
+# ---------------------------------------------------------------------------
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ [pytest.param((2, 4), 1, 0, 2, line_params_req_exact_devices, ttnn.Topology.Linear, False, id="2x4sp1tp0")],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(("F", "H", "W"), [pytest.param(19, 17, 30, id="stage_1")])
+def test_ltx_per_token_timestep_equivalence(
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ topology,
+ is_fsdp,
+ F,
+ H,
+ W,
+ reset_seeds,
+) -> None:
+ """A uniform per-token video timestep must reproduce the scalar-timestep path.
+
+ This is the weight-agnostic invariant behind I2V: with ``image_conditioning=True`` and a
+ per-token ``video_timestep = sigma`` for every token, the model output must match the
+ ``image_conditioning=False`` scalar path (T2V/AV stays bit-similar). The frame-0 pinning
+ behaviour itself is covered end-to-end by the pipeline; here we isolate the DiT plumbing.
+ """
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ video_N_real = F * H * W
+ video_N = _sp_pad_len(video_N_real, sp_factor)
+
+ # Shared random-scaled weights (1 layer) from the diffusers 3D video model.
+ torch_model = _make_diffusers_video_model(num_layers=1)
+ torch_model.eval()
+ _scale_init_(torch_model)
+ state_dict = _convert_diffusers_video_model_to_tt(torch_model.state_dict(), num_heads=NUM_HEADS, head_dim=HEAD_DIM)
+ state_dict = {k: v.detach().clone() for k, v in state_dict.items()}
+ del torch_model
+
+ torch.manual_seed(INPUT_SEED)
+ video_lat_real = torch.randn(1, video_N_real, IN_CHANNELS, dtype=torch.float32)
+ video_lat = _pad_seq_dim(video_lat_real, video_N, dim=1)
+ video_prompt = torch.randn(1, PROMPT_LEN, CTX_DIM, dtype=torch.float32)
+ sigma_val = TIMESTEP_VAL
+ timestep_torch = torch.tensor([sigma_val])
+
+ ccl_manager = _make_ccl_manager(mesh_device, num_links, topology)
+ parallel_config = _make_parallel_config(mesh_device, sp_axis, tp_axis)
+
+ # Shared TT-side RoPE / prompt tensors.
+ tt_video_prompt = bf16_tensor(video_prompt.unsqueeze(0), device=mesh_device)
+ tt_vc, tt_vs = _tt_rope(
+ _video_rope_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ tt_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+ base_kwargs = dict(
+ video_1BNI_torch=video_lat.unsqueeze(0),
+ video_prompt_1BLP=tt_video_prompt,
+ video_rope_cos=tt_vc,
+ video_rope_sin=tt_vs,
+ video_N=video_N_real,
+ trans_mat=tt_trans_mat,
+ timestep_torch=timestep_torch,
+ )
+
+ # Scalar path (image_conditioning=False).
+ scalar_model = _make_tt_model(
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=False,
+ num_layers=1,
+ image_conditioning=False,
+ )
+ scalar_model.load_torch_state_dict(state_dict, strict=True)
+ out_scalar = LTXTransformerModel.device_to_host(scalar_model.forward(**base_kwargs)).squeeze(0)[:, :video_N_real, :]
+ del scalar_model
+
+ # Per-token path (image_conditioning=True) with a uniform timestep = sigma over the padded grid.
+ pertoken_model = _make_tt_model(
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=False,
+ num_layers=1,
+ image_conditioning=True,
+ )
+ pertoken_model.load_torch_state_dict(state_dict, strict=True)
+ video_timestep_torch = torch.full((video_N,), sigma_val, dtype=torch.float32)
+ out_pertoken = LTXTransformerModel.device_to_host(
+ pertoken_model.forward(video_timestep_torch=video_timestep_torch, **base_kwargs)
+ ).squeeze(0)[:, :video_N_real, :]
+ del pertoken_model
+
+ assert out_pertoken.shape == out_scalar.shape, f"{out_pertoken.shape} vs {out_scalar.shape}"
+ assert torch.isfinite(out_pertoken).all(), "NaN/Inf in per-token output"
+ # Same weights + uniform sigma: the per-token MLP collapses to the scalar broadcast (bf16-equal).
+ assert_quality(out_scalar, out_pertoken, pcc=0.999, relative_rmse=0.02)
+ logger.info("PASSED: uniform per-token timestep reproduces the scalar path")
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ [pytest.param((2, 4), 1, 0, 2, line_params_req_exact_devices, ttnn.Topology.Linear, False, id="2x4sp1tp0")],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(("F", "H", "W"), [pytest.param(19, 17, 30, id="stage_1")])
+def test_ltx_per_token_timestep_nonuniform(
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ topology,
+ is_fsdp,
+ F,
+ H,
+ W,
+ reset_seeds,
+) -> None:
+ """NON-uniform per-token timestep (the real I2V case) must match the diffusers per-token oracle.
+
+ This is the case the existing equivalence test does NOT cover: frame-0 tokens at sigma=0
+ (the pinned image anchor) and every other token at a high sigma. If the per-token gate does
+ not reach the fused attn/FFN epilogues, non-frame-0 tokens get the wrong (frame-0) gate and
+ the output diverges from the oracle — the DiT-level signature of "frame 0 ok, rest collapses".
+ """
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ video_N_real = F * H * W
+ video_N = _sp_pad_len(video_N_real, sp_factor)
+ frame0_tokens = H * W # token order is f*H*W + h*W + w, so frame 0 == first H*W tokens
+ sigma_high = float(os.environ.get("LTX_TEST_SIGMA", "0.7"))
+
+ # Shared random-scaled weights (1 layer) from the diffusers 3D video model.
+ torch_model = _make_diffusers_video_model(num_layers=1)
+ torch_model.eval()
+ _scale_init_(torch_model)
+ state_dict = _convert_diffusers_video_model_to_tt(torch_model.state_dict(), num_heads=NUM_HEADS, head_dim=HEAD_DIM)
+ state_dict = {k: v.detach().clone() for k, v in state_dict.items()}
+
+ torch.manual_seed(INPUT_SEED)
+ video_lat_real = torch.randn(1, video_N_real, IN_CHANNELS, dtype=torch.float32)
+ video_lat = _pad_seq_dim(video_lat_real, video_N, dim=1)
+ video_prompt = torch.randn(1, PROMPT_LEN, CTX_DIM, dtype=torch.float32)
+
+ # Per-token timestep: frame-0 tokens pinned at sigma=0, everything else (incl. SP padding) at sigma_high.
+ video_ts_real = torch.full((video_N_real,), sigma_high, dtype=torch.float32)
+ video_ts_real[:frame0_tokens] = 0.0
+ video_timestep_torch = torch.full((video_N,), sigma_high, dtype=torch.float32)
+ video_timestep_torch[:frame0_tokens] = 0.0
+
+ # === Reference (per-token oracle) — before TT to avoid weight aliasing ===
+ ref_video = _diffusers_video_model_ref_pertoken(
+ torch_model,
+ video_lat=video_lat_real,
+ video_prompt=video_prompt,
+ video_ts_real=video_ts_real,
+ sigma_scalar=sigma_high,
+ F=F,
+ H=H,
+ W=W,
+ )
+ del torch_model
+
+ ccl_manager = _make_ccl_manager(mesh_device, num_links, topology)
+ parallel_config = _make_parallel_config(mesh_device, sp_axis, tp_axis)
+ tt_video_prompt = bf16_tensor(video_prompt.unsqueeze(0), device=mesh_device)
+ tt_vc, tt_vs = _tt_rope(
+ _video_rope_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ tt_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+
+ model = _make_tt_model(
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=False,
+ num_layers=1,
+ image_conditioning=True,
+ )
+ model.load_torch_state_dict(state_dict, strict=True)
+ out_tt = LTXTransformerModel.device_to_host(
+ model.forward(
+ video_1BNI_torch=video_lat.unsqueeze(0),
+ video_prompt_1BLP=tt_video_prompt,
+ video_rope_cos=tt_vc,
+ video_rope_sin=tt_vs,
+ video_N=video_N_real,
+ trans_mat=tt_trans_mat,
+ timestep_torch=torch.tensor([sigma_high]),
+ video_timestep_torch=video_timestep_torch,
+ )
+ ).squeeze(0)[:, :video_N_real, :]
+ del model
+
+ assert out_tt.shape == ref_video.shape, f"{out_tt.shape} vs {ref_video.shape}"
+ assert torch.isfinite(out_tt).all(), "NaN/Inf in per-token output"
+
+ # Localize frame-0 (pinned, sigma=0) vs the rest (sigma_high): a per-token gate that doesn't
+ # reach the fused epilogues makes the "rest" track the oracle far worse than frame 0.
+ def _pcc(a, b):
+ a32, b32 = a.float().flatten(), b.float().flatten()
+ return torch.corrcoef(torch.stack([a32, b32]))[0, 1].item()
+
+ pcc_all = _pcc(ref_video, out_tt)
+ pcc_f0 = _pcc(ref_video[:, :frame0_tokens, :], out_tt[:, :frame0_tokens, :])
+ pcc_rest = _pcc(ref_video[:, frame0_tokens:, :], out_tt[:, frame0_tokens:, :])
+ logger.info(f"non-uniform per-token (TT vs oracle): all={pcc_all:.5f} frame0={pcc_f0:.5f} rest={pcc_rest:.5f}")
+
+ assert_quality(ref_video, out_tt, pcc=0.99, relative_rmse=0.03)
+
+
+# --------------------------------------------------------------------------- Trace-based perf: VIDEO stage-2 ring
+def _build_video_trace_setup(
+ *,
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ topology,
+ is_fsdp,
+ F,
+ H,
+ W,
+ has_audio=False,
+ checkpoint_variant="fast",
+ num_layers=1,
+):
+ """Build the TT model + device-only ``inner_step`` kwargs for the trace-perf test.
+
+ Mirrors the TT setup in ``_run_inner_step`` and reproduces ``LTXTransformerModel.forward``'s
+ host→device upload preamble (which ``inner_step`` normally receives), so the returned ttnn
+ kwargs can be captured/replayed directly under ttnn trace. Returns
+ ``(tt_model, inner_kwargs, video_N_real)``. Perf-only; no PCC.
+
+ Video mode uses random scaled weights. AV mode (``has_audio=True``) loads the 22B checkpoint
+ (``strict=False``) like the AV PCC path and adds the audio device tensors (audio latent,
+ audio/cross-modal rope, video pad mask) so a2v / audio shapes actually fire under the trace.
+ """
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ video_N_real = F * H * W
+ video_N = _sp_pad_len(video_N_real, sp_factor)
+ assert video_N % (32 * sp_factor) == 0
+ audio_N = AUDIO_N
+ if has_audio:
+ assert audio_N % (32 * sp_factor) == 0
+
+ if has_audio:
+ # AV weights come from the 22B checkpoint (strict=False), same as the AV PCC path.
+ checkpoint_22b = _resolve_checkpoint_22b(checkpoint_variant)
+ state_dict = _load_22b_state_dict(num_layers=num_layers, checkpoint_path=checkpoint_22b)
+ if state_dict is None:
+ pytest.skip(f"22B checkpoint not found at {checkpoint_22b}")
+ else:
+ # Random scaled weights from the diffusers 3D video model — the video PCC path.
+ torch_model = _make_diffusers_video_model(num_layers=num_layers)
+ torch_model.eval()
+ _scale_init_(torch_model)
+ state_dict = _convert_diffusers_video_model_to_tt(
+ torch_model.state_dict(), num_heads=NUM_HEADS, head_dim=HEAD_DIM
+ )
+ state_dict = {k: v.detach().clone() for k, v in state_dict.items()}
+ del torch_model
+
+ # Real-grid latent; the TT side gets a zero-padded copy (video_N).
+ torch.manual_seed(INPUT_SEED)
+ video_lat_real = torch.randn(1, video_N_real, IN_CHANNELS, dtype=torch.float32)
+ video_lat = _pad_seq_dim(video_lat_real, video_N, dim=1)
+ video_prompt = torch.randn(1, PROMPT_LEN, CTX_DIM, dtype=torch.float32)
+ sigma_val = 0.5 if has_audio else TIMESTEP_VAL # AV path uses production sigma=0.5
+ timestep_torch = torch.tensor([sigma_val])
+
+ audio_lat = None
+ audio_prompt = None
+ if has_audio:
+ audio_lat = torch.randn(1, audio_N, AUDIO_IN_CHANNELS, dtype=torch.float32)
+ audio_prompt = torch.randn(1, PROMPT_LEN, AUDIO_CTX_DIM, dtype=torch.float32)
+
+ ccl_manager = _make_ccl_manager(mesh_device, num_links, topology)
+ parallel_config = _make_parallel_config(mesh_device, sp_axis, tp_axis)
+ tt_model = _make_tt_model(
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=has_audio,
+ num_layers=num_layers,
+ )
+ t0 = time.time()
+ tt_model.load_torch_state_dict(state_dict, strict=not has_audio)
+ logger.info(f"state-dict load: {time.time() - t0:.1f}s")
+
+ # Stage-constant device tensors (INTERLEAVED rope + trans_mat + prompt).
+ tt_video_prompt = bf16_tensor(video_prompt.unsqueeze(0), device=mesh_device)
+ tt_vc, tt_vs = _tt_rope(
+ _video_rope_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ tt_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+
+ # Per-step device inputs — reproduce LTXTransformerModel.forward's upload for the video path.
+ video_1BNI_torch = video_lat.unsqueeze(0) # (1, B=1, video_N, IN_CHANNELS)
+ B_size = video_1BNI_torch.shape[1]
+ video_1BNI = bf16_tensor(video_1BNI_torch, device=mesh_device, mesh_axis=sp_axis, shard_dim=-2)
+ timestep = bf16_tensor(timestep_torch.reshape(1, 1, B_size, 1) * 1000.0, device=mesh_device)
+
+ # inner_step is keyword-only; these are exactly the device tensors it consumes.
+ inner_kwargs = dict(
+ video_1BNI=video_1BNI,
+ timestep=timestep,
+ video_prompt_1BLP=tt_video_prompt,
+ video_rope_cos=tt_vc,
+ video_rope_sin=tt_vs,
+ video_N=video_N_real,
+ trans_mat=tt_trans_mat,
+ )
+
+ if has_audio:
+ # Audio + cross-modal device tensors, mirroring the AV branch of _run_inner_step.
+ a_cos, a_sin = _tt_rope(_audio_rope_freqs, audio_N, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis)
+ vx_cos, vx_sin = _tt_rope(
+ _video_cross_pe_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ ax_cos, ax_sin = _tt_rope(
+ _audio_cross_pe_freqs, audio_N, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis
+ )
+ ax_cos_full, ax_sin_full = _tt_rope_full(
+ _audio_cross_pe_freqs, audio_N, mesh_device=mesh_device, tp_axis=tp_axis
+ )
+ v_pad_sp = build_video_pad_mask(video_N, video_N_real, mesh_device=mesh_device, sp_axis=sp_axis)
+ audio_1BNI = bf16_tensor(audio_lat.unsqueeze(0), device=mesh_device, mesh_axis=sp_axis, shard_dim=-2)
+ inner_kwargs.update(
+ audio_1BNI=audio_1BNI,
+ audio_prompt_1BLP=bf16_tensor(audio_prompt.unsqueeze(0), device=mesh_device),
+ audio_rope_cos=a_cos,
+ audio_rope_sin=a_sin,
+ audio_N=audio_N,
+ video_cross_pe_cos=vx_cos,
+ video_cross_pe_sin=vx_sin,
+ audio_cross_pe_cos=ax_cos,
+ audio_cross_pe_sin=ax_sin,
+ audio_cross_pe_cos_full=ax_cos_full,
+ audio_cross_pe_sin_full=ax_sin_full,
+ video_padding_mask=v_pad_sp,
+ )
+ return tt_model, inner_kwargs, video_N_real
+
+
+def _build_trace_inner_kwargs(*, mesh_device, sp_axis, tp_axis, F, H, W, has_audio):
+ """Build ONLY the device inner_step kwargs for a given (F,H,W) — no model / CCLManager.
+
+ Split out so ONE model + CCLManager can be driven at multiple shapes (e.g. stage-1 then
+ stage-2) to reproduce the e2e's cross-stage device/semaphore-pool state. Mirrors the per-shape
+ tensor setup inside _build_video_trace_setup. Returns (inner_kwargs, video_N_real).
+ """
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ video_N_real = F * H * W
+ video_N = _sp_pad_len(video_N_real, sp_factor)
+ assert video_N % (32 * sp_factor) == 0
+ audio_N = AUDIO_N
+ if has_audio:
+ assert audio_N % (32 * sp_factor) == 0
+
+ torch.manual_seed(INPUT_SEED)
+ video_lat_real = torch.randn(1, video_N_real, IN_CHANNELS, dtype=torch.float32)
+ video_lat = _pad_seq_dim(video_lat_real, video_N, dim=1)
+ video_prompt = torch.randn(1, PROMPT_LEN, CTX_DIM, dtype=torch.float32)
+ sigma_val = 0.5 if has_audio else TIMESTEP_VAL
+ timestep_torch = torch.tensor([sigma_val])
+
+ audio_lat = None
+ audio_prompt = None
+ if has_audio:
+ audio_lat = torch.randn(1, audio_N, AUDIO_IN_CHANNELS, dtype=torch.float32)
+ audio_prompt = torch.randn(1, PROMPT_LEN, AUDIO_CTX_DIM, dtype=torch.float32)
+
+ tt_video_prompt = bf16_tensor(video_prompt.unsqueeze(0), device=mesh_device)
+ tt_vc, tt_vs = _tt_rope(
+ _video_rope_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ tt_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+
+ video_1BNI_torch = video_lat.unsqueeze(0)
+ B_size = video_1BNI_torch.shape[1]
+ video_1BNI = bf16_tensor(video_1BNI_torch, device=mesh_device, mesh_axis=sp_axis, shard_dim=-2)
+ timestep = bf16_tensor(timestep_torch.reshape(1, 1, B_size, 1) * 1000.0, device=mesh_device)
+
+ inner_kwargs = dict(
+ video_1BNI=video_1BNI,
+ timestep=timestep,
+ video_prompt_1BLP=tt_video_prompt,
+ video_rope_cos=tt_vc,
+ video_rope_sin=tt_vs,
+ video_N=video_N_real,
+ trans_mat=tt_trans_mat,
+ )
+ if has_audio:
+ a_cos, a_sin = _tt_rope(_audio_rope_freqs, audio_N, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis)
+ vx_cos, vx_sin = _tt_rope(
+ _video_cross_pe_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ ax_cos, ax_sin = _tt_rope(
+ _audio_cross_pe_freqs, audio_N, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis
+ )
+ ax_cos_full, ax_sin_full = _tt_rope_full(
+ _audio_cross_pe_freqs, audio_N, mesh_device=mesh_device, tp_axis=tp_axis
+ )
+ v_pad_sp = build_video_pad_mask(video_N, video_N_real, mesh_device=mesh_device, sp_axis=sp_axis)
+ audio_1BNI = bf16_tensor(audio_lat.unsqueeze(0), device=mesh_device, mesh_axis=sp_axis, shard_dim=-2)
+ inner_kwargs.update(
+ audio_1BNI=audio_1BNI,
+ audio_prompt_1BLP=bf16_tensor(audio_prompt.unsqueeze(0), device=mesh_device),
+ audio_rope_cos=a_cos,
+ audio_rope_sin=a_sin,
+ audio_N=audio_N,
+ video_cross_pe_cos=vx_cos,
+ video_cross_pe_sin=vx_sin,
+ audio_cross_pe_cos=ax_cos,
+ audio_cross_pe_sin=ax_sin,
+ audio_cross_pe_cos_full=ax_cos_full,
+ audio_cross_pe_sin_full=ax_sin_full,
+ video_padding_mask=v_pad_sp,
+ )
+ return inner_kwargs, video_N_real
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ [
+ pytest.param((4, 8), 1, 0, 2, ring_params_4k, ttnn.Topology.Ring, False, id="ring_bh_4x8sp1tp0_4k"),
+ # 8k fabric payload variant (where stage-2 wedges at depth).
+ pytest.param((4, 8), 1, 0, 2, ring_params_8k, ttnn.Topology.Ring, False, id="ring_bh_4x8sp1tp0_8k"),
+ ],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize("has_audio", [pytest.param(False, id="video"), pytest.param(True, id="av")])
+@pytest.mark.parametrize("checkpoint_variant", [pytest.param("fast", id="ckpt_fast")])
+def test_ltx_transformer_crossstage_hang(
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ topology,
+ is_fsdp,
+ has_audio,
+ checkpoint_variant,
+ reset_seeds,
+) -> None:
+ """FAST cross-stage repro of the e2e stage-2 deadlock: drive ONE model+CCLManager through
+ stage-1 then stage-2 (eager, no VAE/Gemma), so stage-2 reuses the shared strided-AGMM pool
+ that stage-1 left mid-rotation. PASSES -> not cross-stage; HANGS -> confirmed.
+ """
+ # Stage-2 shape; full depth (env-overridable) to stack many back-to-back strided ops per forward.
+ tt_model, kwargs_s2, vN2 = _build_video_trace_setup(
+ mesh_device=mesh_device,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ F=19,
+ H=34,
+ W=60,
+ has_audio=has_audio,
+ checkpoint_variant=checkpoint_variant,
+ num_layers=int(os.environ.get("CROSSSTAGE_NUM_LAYERS", "48")),
+ )
+ # Stage-1 shape (half spatial res, like the e2e's height//2,width//2) on the SAME model.
+ kwargs_s1, vN1 = _build_trace_inner_kwargs(
+ mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, F=19, H=17, W=30, has_audio=has_audio
+ )
+ logger.info(f"cross-stage: stage-1 vN={vN1} then stage-2 vN={vN2} (shared CCLManager)")
+
+ # CROSSSTAGE_SKIP_S1=1 -> run only stage-2, isolating the stage transition from full-depth big-M.
+ skip_s1 = os.environ.get("CROSSSTAGE_SKIP_S1", "0") != "0"
+ if not skip_s1:
+ # Stage 1 first (eager) — populate/rotate the shared strided-AGMM semaphore pool at stage-1 M.
+ for i in range(3):
+ _ = tt_model.inner_step(**kwargs_s1)
+ ttnn.synchronize_device(mesh_device)
+ logger.info(f"stage-1 forward {i} done")
+ logger.info("stage-1 complete; entering STAGE-2 (the e2e hang point) ...")
+ else:
+ logger.info("SKIP_S1: entering STAGE-2 directly (48 layers, no stage-1) ...")
+
+ # Stage 2 (eager) — reuses the same pool. This is where the e2e deadlocks.
+ for i in range(3):
+ _ = tt_model.inner_step(**kwargs_s2)
+ ttnn.synchronize_device(mesh_device)
+ logger.info(f"stage-2 forward {i} done")
+
+ logger.info("PASSED cross-stage: stage-1 -> stage-2 completed without deadlock")
+ del tt_model
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ # VIDEO stage-2 ring config only (8k fabric payload); do NOT add AV/other-mesh params here.
+ [pytest.param((4, 8), 1, 0, 2, ring_params_8k, ttnn.Topology.Ring, False, id="ring_bh_4x8sp1tp0_8k")],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(("F", "H", "W"), [pytest.param(19, 34, 60, id="stage_2")])
+@pytest.mark.parametrize("has_audio", [pytest.param(False, id="video"), pytest.param(True, id="av")])
+@pytest.mark.parametrize("checkpoint_variant", [pytest.param("fast", id="ckpt_fast")])
+def test_ltx_transformer_trace_perf(
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ topology,
+ is_fsdp,
+ F,
+ H,
+ W,
+ has_audio,
+ checkpoint_variant,
+ reset_seeds,
+) -> None:
+ """Trace-based perf for LTXTransformerModel.inner_step (VIDEO stage-2 ring).
+
+ Measures StridedAllGatherMinimalMatmulAsync device time and block wall-clock at steady state
+ under a ttnn trace (gap-free replay), which the eager op-by-op path can't expose. Perf-only:
+ asserts shape/finiteness, no PCC. inner_step is @traced_function(prep_run=False), so kernels
+ are precompiled with a couple of eager warm calls before trace capture.
+ """
+ tt_model, inner_kwargs, video_N_real = _build_video_trace_setup(
+ mesh_device=mesh_device,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ F=F,
+ H=H,
+ W=W,
+ has_audio=has_audio,
+ checkpoint_variant=checkpoint_variant,
+ )
+
+ # AV emits far more markers than video
+ n_warm = int(os.environ.get("LTX_PERF_N_WARM", "1" if has_audio else "2"))
+ n_replay = int(os.environ.get("LTX_PERF_N_REPLAY", "3" if has_audio else "5"))
+
+ # Warm / compile: eager (untraced) calls precompile kernels (prep_run=False on inner_step).
+ t0 = time.time()
+ for _ in range(n_warm):
+ _ = tt_model.inner_step(**inner_kwargs)
+ ttnn.synchronize_device(mesh_device)
+ logger.info(f"eager warm/compile ({n_warm}x): {time.time() - t0:.1f}s")
+
+ # Capture: first traced call captures the trace (and executes it once).
+ t0 = time.time()
+ _ = tt_model.inner_step(**inner_kwargs, traced=True)
+ ttnn.synchronize_device(mesh_device)
+ logger.info(f"trace capture: {time.time() - t0:.1f}s")
+
+ # Drain warm+capture markers so only the signposted replays occupy the profiler buffer.
+ ttnn.ReadDeviceProfiler(mesh_device)
+
+ # Steady-state replay under the signpost bracket for tt-perf-report --start/end-signpost.
+ signpost("start")
+ t0 = time.time()
+ result = None
+ for _ in range(n_replay):
+ result = tt_model.inner_step(**inner_kwargs, traced=True)
+ ttnn.synchronize_device(mesh_device)
+ replay_s = time.time() - t0
+ signpost("stop")
+ logger.info(f"trace replay ({n_replay}x): {replay_s:.3f}s → {replay_s / n_replay * 1e3:.2f} ms/step")
+
+ # Perf-only: crop SP-padding tail, assert shape + finiteness (no PCC)
+ video_out = result[0] if isinstance(result, tuple) else result
+ tt_video = LTXTransformerModel.device_to_host(video_out).squeeze(0)[:, :video_N_real, :]
+ assert tt_video.shape == (1, video_N_real, OUT_CHANNELS), f"video shape {tt_video.shape}"
+ assert torch.isfinite(tt_video).all(), "NaN/Inf in traced video output"
+ logger.info(f"PASSED trace-perf: video {tuple(tt_video.shape)} has_audio={has_audio}")
+
+ del tt_model
+
+
+# Trace-based perf for a SINGLE transformer block (isolates one block's program order).
+def _build_block_trace_setup(
+ *, mesh_device, sp_axis, tp_axis, num_links, topology, is_fsdp, F, H, W, has_audio, checkpoint_variant="fast"
+):
+ """Build an ``LTXTransformerBlock`` + device ``forward`` kwargs for the block trace-perf test.
+
+ Mirrors the non-PCC setup of ``test_ltx_transformer_block``: AV loads the 22B checkpoint
+ (``strict=False``) and builds the full video+audio+cross-modal device tensors; video harvests
+ random scaled weights from a throwaway diffusers block. Returns
+ ``(tt_block, forward_kwargs, video_N_real, audio_N_real)``. Perf-only; no PCC.
+ """
+ checkpoint_22b = _resolve_checkpoint_22b(checkpoint_variant)
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ # Real latent grid → SP-padded sequence (mirrors LTXPipeline)
+ video_N_real = F * H * W
+ video_N = _sp_pad_len(video_N_real, sp_factor)
+ audio_N, audio_N_real = _audio_seq_lens(F, sp_factor)
+ assert video_N % (32 * sp_factor) == 0, f"video_N={video_N} not sp/tile-aligned for sp={sp_factor}"
+ assert audio_N % (32 * sp_factor) == 0, f"audio_N={audio_N} not sp/tile-aligned for sp={sp_factor}"
+
+ ccl_manager = _make_ccl_manager(mesh_device, num_links, topology)
+ parallel_config = _make_parallel_config(mesh_device, sp_axis, tp_axis)
+ tt_block = _make_tt_block(
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ has_audio=has_audio,
+ )
+
+ if has_audio:
+ sd = _load_22b_state_dict(num_layers=1, checkpoint_path=checkpoint_22b)
+ if sd is None:
+ pytest.skip(f"22B checkpoint not found at {checkpoint_22b}")
+ block_sd = {
+ k[len("transformer_blocks.0.") :]: v for k, v in sd.items() if k.startswith("transformer_blocks.0.")
+ }
+ tt_block.load_torch_state_dict(block_sd, strict=False)
+ else:
+ # video mode — harvest a shape-matching state dict from a throwaway block (perf-only).
+ dummy = _make_diffusers_video_block()
+ _scale_init_(dummy)
+ conv = _convert_diffusers_video_block_to_tt(dummy.state_dict(), num_heads=NUM_HEADS, head_dim=HEAD_DIM)
+ tt_block.load_torch_state_dict({k: v.detach().clone() for k, v in conv.items()})
+ del dummy
+
+ # Inputs (real-grid token count; the TT side gets a zero-padded copy below).
+ torch.manual_seed(INPUT_SEED)
+ x = torch.randn(1, video_N_real, DIM, dtype=torch.float32)
+ context = torch.randn(1, PROMPT_LEN, CTX_DIM, dtype=torch.float32)
+ temb = torch.randn(1, 1, 9 * DIM, dtype=torch.float32) # 9 adaln params
+ prompt_temb = torch.randn(1, 1, 2 * DIM, dtype=torch.float32) # 2 adaln params for prompt
+
+ # TT video tensors
+ spatial = _pad_seq_dim(x, video_N, dim=1).unsqueeze(0)
+ tt_spatial = bf16_tensor_2dshard(spatial, device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 3})
+ tt_prompt = bf16_tensor(context.unsqueeze(0), device=mesh_device)
+ tt_temb = bf16_tensor(
+ temb.reshape(9, DIM).unsqueeze(1).unsqueeze(1), device=mesh_device, mesh_axis=tp_axis, shard_dim=3
+ )
+ tt_prompt_temb = bf16_tensor(prompt_temb.reshape(2, DIM).unsqueeze(1).unsqueeze(1), device=mesh_device)
+ tt_cos, tt_sin = _tt_rope(
+ _video_rope_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ tt_trans_mat = bf16_tensor(get_rot_transformation_mat(), device=mesh_device)
+
+ forward_kwargs = dict(
+ video_1BND=tt_spatial,
+ video_prompt=tt_prompt,
+ video_temb=tt_temb,
+ video_N=video_N_real,
+ video_rope_cos=tt_cos,
+ video_rope_sin=tt_sin,
+ trans_mat=tt_trans_mat,
+ video_prompt_temb=tt_prompt_temb,
+ )
+ if has_audio:
+ # Real tokens in [:audio_N_real], zeros in the padded tail (matches the padded audio latent).
+ a_x = torch.zeros(1, audio_N, AUDIO_DIM, dtype=torch.float32)
+ a_x[:, :audio_N_real, :] = torch.randn(1, audio_N_real, AUDIO_DIM, dtype=torch.float32)
+ a_ctx = torch.randn(1, PROMPT_LEN, AUDIO_CTX_DIM, dtype=torch.float32)
+ a_temb = torch.randn(1, 1, 9 * AUDIO_DIM, dtype=torch.float32)
+ a_prompt_temb = torch.randn(1, 1, 2 * AUDIO_DIM, dtype=torch.float32)
+ av_ca_v = torch.randn(1, 1, 5 * DIM, dtype=torch.float32) # 4 scale-shift + 1 gate
+ av_ca_a = torch.randn(1, 1, 5 * AUDIO_DIM, dtype=torch.float32)
+
+ a_cos, a_sin = _tt_rope(_audio_rope_freqs, audio_N, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis)
+ vx_cos, vx_sin = _tt_rope(
+ _video_cross_pe_freqs, F, H, W, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis, pad_to=video_N
+ )
+ ax_cos, ax_sin = _tt_rope(
+ _audio_cross_pe_freqs, audio_N, mesh_device=mesh_device, sp_axis=sp_axis, tp_axis=tp_axis
+ )
+ ax_cos_full, ax_sin_full = _tt_rope_full(
+ _audio_cross_pe_freqs, audio_N, mesh_device=mesh_device, tp_axis=tp_axis
+ )
+
+ # Padding masks, same construction as the fast pipeline (audio padded, video aligned).
+ a_attn_mask, a_pad_sp, a_pad_full = build_audio_masks(
+ audio_N, audio_N_real, mesh_device=mesh_device, sp_axis=sp_axis
+ )
+ v_pad_sp = build_video_pad_mask(video_N, video_N_real, mesh_device=mesh_device, sp_axis=sp_axis)
+
+ forward_kwargs.update(
+ audio_1BND=bf16_tensor_2dshard(
+ a_x.unsqueeze(0), device=mesh_device, shard_mapping={sp_axis: 2, tp_axis: 3}
+ ),
+ audio_prompt=bf16_tensor(a_ctx.unsqueeze(0), device=mesh_device),
+ audio_temb=bf16_tensor(
+ a_temb.reshape(9, AUDIO_DIM).unsqueeze(1).unsqueeze(1),
+ device=mesh_device,
+ mesh_axis=tp_axis,
+ shard_dim=3,
+ ),
+ audio_prompt_temb=bf16_tensor(
+ a_prompt_temb.reshape(2, AUDIO_DIM).unsqueeze(1).unsqueeze(1), device=mesh_device
+ ),
+ av_ca_temb=bf16_tensor(
+ av_ca_v.reshape(5, DIM).unsqueeze(1).unsqueeze(1), device=mesh_device, mesh_axis=tp_axis, shard_dim=3
+ ),
+ av_ca_audio_temb=bf16_tensor(
+ av_ca_a.reshape(5, AUDIO_DIM).unsqueeze(1).unsqueeze(1),
+ device=mesh_device,
+ mesh_axis=tp_axis,
+ shard_dim=3,
+ ),
+ audio_N=audio_N,
+ audio_rope_cos=a_cos,
+ audio_rope_sin=a_sin,
+ video_cross_pe_cos=vx_cos,
+ video_cross_pe_sin=vx_sin,
+ audio_cross_pe_cos=ax_cos,
+ audio_cross_pe_sin=ax_sin,
+ audio_cross_pe_cos_full=ax_cos_full,
+ audio_cross_pe_sin_full=ax_sin_full,
+ audio_attn_mask=a_attn_mask,
+ audio_padding_mask=a_pad_sp,
+ audio_padding_mask_full=a_pad_full,
+ video_padding_mask=v_pad_sp,
+ )
+ return tt_block, forward_kwargs, video_N_real, audio_N_real
+
+
+@pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ # Same ring stage-2 8k config as test_ltx_transformer_trace_perf
+ [pytest.param((4, 8), 1, 0, 2, ring_params_8k, ttnn.Topology.Ring, False, id="ring_bh_4x8sp1tp0_8k")],
+ indirect=["mesh_device", "device_params"],
+)
+@pytest.mark.parametrize(("F", "H", "W"), [pytest.param(19, 34, 60, id="stage_2")])
+@pytest.mark.parametrize("has_audio", [pytest.param(False, id="video"), pytest.param(True, id="av")])
+@pytest.mark.parametrize("checkpoint_variant", [pytest.param("fast", id="ckpt_fast")])
+def test_ltx_transformer_block_trace_perf(
+ mesh_device,
+ sp_axis,
+ tp_axis,
+ num_links,
+ topology,
+ is_fsdp,
+ F,
+ H,
+ W,
+ has_audio,
+ checkpoint_variant,
+ reset_seeds,
+) -> None:
+ """Trace-based perf for a SINGLE ``LTXTransformerBlock`` forward (AV stage-2 ring).
+
+ Isolates one block's program order — no model-level patch-embed / proj-out / tilize /
+ untilize / mesh-partition ops, and no multi-block scrambling — so tt-perf-report shows a clean
+ one-block, program-ordered view. Captures the block forward under a ttnn trace (via the same
+ ``Tracer`` machinery that backs ``inner_step``'s ``traced=True`` path) and replays it under the
+ same "start"/"stop" signpost bracket used by ``test_ltx_transformer_trace_perf``. Perf-only:
+ asserts shape/finiteness, no PCC.
+ """
+ tt_block, forward_kwargs, video_N_real, _audio_N_real = _build_block_trace_setup(
+ mesh_device=mesh_device,
+ sp_axis=sp_axis,
+ tp_axis=tp_axis,
+ num_links=num_links,
+ topology=topology,
+ is_fsdp=is_fsdp,
+ F=F,
+ H=H,
+ W=W,
+ has_audio=has_audio,
+ checkpoint_variant=checkpoint_variant,
+ )
+
+ # Trace the block forward with the same Tracer settings inner_step uses
+ tracer = Tracer(tt_block.forward, device=mesh_device, prep_run=False, clone_prep_inputs=False)
+
+ # AV runs many more ops than video (audio self/cross + a2v/v2a + audio ff/RS)
+ n_warm = 1 if has_audio else 2
+ n_replay = 3 if has_audio else 5
+
+ # Warm / compile: eager (untraced) calls precompile kernels before trace capture.
+ t0 = time.time()
+ for _ in range(n_warm):
+ _ = tt_block(**forward_kwargs)
+ ttnn.synchronize_device(mesh_device)
+ logger.info(f"eager warm/compile ({n_warm}x): {time.time() - t0:.1f}s")
+
+ # Capture: first traced call captures the trace (and executes it once).
+ t0 = time.time()
+ _ = tracer(**forward_kwargs, traced=True)
+ ttnn.synchronize_device(mesh_device)
+ logger.info(f"trace capture: {time.time() - t0:.1f}s")
+
+ # Drain warm+capture markers so ONLY the signposted replays occupy the profiler DRAM buffer
+ ttnn.ReadDeviceProfiler(mesh_device)
+
+ # Steady-state replay under the signpost bracket for tt-perf-report --start/end-signpost.
+ signpost("start")
+ t0 = time.time()
+ result = None
+ for _ in range(n_replay):
+ result = tracer(**forward_kwargs, traced=True)
+ ttnn.synchronize_device(mesh_device)
+ replay_s = time.time() - t0
+ signpost("stop")
+ logger.info(f"trace replay ({n_replay}x): {replay_s:.3f}s → {replay_s / n_replay * 1e3:.2f} ms/step")
+
+ # Perf-only: crop SP-padding tail, assert shape + finiteness (no PCC)
+ tt_v = result[0] if isinstance(result, tuple) else result
+ concat_dims = [None, None]
+ concat_dims[sp_axis] = 2
+ concat_dims[tp_axis] = 3
+ tt_v_torch = ttnn.to_torch(
+ tt_v,
+ mesh_composer=ttnn.ConcatMesh2dToTensor(mesh_device, dims=concat_dims, mesh_shape=tuple(mesh_device.shape)),
+ ).squeeze(0)[:, :video_N_real, :]
+ assert tt_v_torch.shape == (1, video_N_real, DIM), f"video shape {tt_v_torch.shape}"
+ assert torch.isfinite(tt_v_torch).all(), "NaN/Inf in traced video output"
+ logger.info(f"PASSED block trace-perf: video {tuple(tt_v_torch.shape)} has_audio={has_audio}")
+
+ tracer.release_trace()
+ del tt_block
diff --git a/code/models/tt_dit/tests/models/ltx/test_vae_ltx.py b/code/models/tt_dit/tests/models/ltx/test_vae_ltx.py
new file mode 100644
index 0000000000000000000000000000000000000000..edbd3ba341923daf12e1da77ecdd034058384123
--- /dev/null
+++ b/code/models/tt_dit/tests/models/ltx/test_vae_ltx.py
@@ -0,0 +1,1151 @@
+# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Tests for LTX-2 Video VAE components.
+
+Torch references use diffusers LTX-2 VAE modules (mirrors test_vae_wan2_1.py / test_audio_ltx.py).
+"""
+
+from __future__ import annotations
+
+import os
+from typing import Any
+
+import pytest
+import torch
+import torch.nn as nn
+from loguru import logger
+from tracy import signpost
+
+import ttnn
+from models.tt_dit.models.vae.vae_ltx import (
+ LTXCausalConv3d,
+ LTXDepthToSpaceUpsample,
+ LTXResnetBlock3D,
+ LTXVideoDecoder,
+ LTXVideoEncoder,
+)
+from models.tt_dit.parallel.config import ParallelFactor, VaeHWParallelConfig
+from models.tt_dit.parallel.manager import CCLManager
+from models.tt_dit.utils.check import assert_quality
+from models.tt_dit.utils.conv3d import conv_pad_in_channels
+
+from .ltx_mesh_params import (
+ LTX_VAE_DECODER_MESH_PARAMS,
+ LTX_VAE_DECODER_MULTI_ONLY_MESH_PARAMS,
+ LTX_VAE_ENCODER_PROD_MESH_PARAMS,
+)
+
+
+def _require_diffusers_ltx_vae():
+ """Return diffusers LTX-2 VAE building blocks (preferred) or LTX-Video fallbacks."""
+ pytest.importorskip("diffusers")
+ try:
+ from diffusers.models.autoencoders.autoencoder_kl_ltx2 import (
+ LTX2VideoCausalConv3d,
+ LTX2VideoDownsampler3d,
+ LTX2VideoResnetBlock3d,
+ LTX2VideoUpsampler3d,
+ PerChannelRMSNorm,
+ )
+
+ return {
+ "causal_conv": LTX2VideoCausalConv3d,
+ "resnet": LTX2VideoResnetBlock3d,
+ "upsample": LTX2VideoUpsampler3d,
+ "downsample": LTX2VideoDownsampler3d,
+ "pixel_norm": PerChannelRMSNorm,
+ "ltx2": True,
+ }
+ except ImportError:
+ from diffusers.models.autoencoders.autoencoder_kl_ltx import (
+ LTXVideoCausalConv3d,
+ LTXVideoDownsampler3d,
+ LTXVideoResnetBlock3d,
+ LTXVideoUpsampler3d,
+ )
+ from diffusers.models.normalization import RMSNorm
+
+ return {
+ "causal_conv": LTXVideoCausalConv3d,
+ "resnet": LTXVideoResnetBlock3d,
+ "upsample": LTXVideoUpsampler3d,
+ "downsample": LTXVideoDownsampler3d,
+ "pixel_norm": RMSNorm,
+ "ltx2": False,
+ }
+
+
+def _diffusers_resnet_state_to_tt(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """Remap diffusers LTX-2 resnet keys to the TT/Lightricks layout."""
+ out = dict(state)
+ if "conv_shortcut.weight" in out:
+ out["conv_shortcut.conv.weight"] = out.pop("conv_shortcut.weight")
+ if "conv_shortcut.bias" in out:
+ out["conv_shortcut.conv.bias"] = out.pop("conv_shortcut.bias")
+ return out
+
+
+def _run_diffusers_causal_conv(conv: nn.Module, x: torch.Tensor, *, causal: bool, ltx2: bool) -> torch.Tensor:
+ if ltx2:
+ return conv(x, causal=causal)
+ return conv(x)
+
+
+def _run_diffusers_resnet(block: nn.Module, x: torch.Tensor, *, causal: bool, ltx2: bool) -> torch.Tensor:
+ if ltx2:
+ return block(x, causal=causal)
+ return block(x)
+
+
+def _run_diffusers_upsample(block: nn.Module, x: torch.Tensor, *, causal: bool, ltx2: bool) -> torch.Tensor:
+ if ltx2:
+ return block(x, causal=causal)
+ return block(x)
+
+
+def _run_diffusers_downsample(block: nn.Module, x: torch.Tensor, *, causal: bool, ltx2: bool) -> torch.Tensor:
+ if ltx2:
+ return block(x, causal=causal)
+ return block(x)
+
+
+class _PerChannelStatistics(nn.Module):
+ """Minimal per-channel latent stats (Lightricks key names for TT weight load)."""
+
+ def __init__(self, latent_channels: int) -> None:
+ super().__init__()
+ self.register_buffer("mean-of-means", torch.zeros(latent_channels))
+ self.register_buffer("std-of-means", torch.ones(latent_channels))
+
+ def un_normalize(self, x: torch.Tensor) -> torch.Tensor:
+ mean = self.get_buffer("mean-of-means").view(1, -1, 1, 1, 1)
+ std = self.get_buffer("std-of-means").view(1, -1, 1, 1, 1)
+ return x * std + mean
+
+ def normalize(self, x: torch.Tensor) -> torch.Tensor:
+ mean = self.get_buffer("mean-of-means").view(1, -1, 1, 1, 1)
+ std = self.get_buffer("std-of-means").view(1, -1, 1, 1, 1)
+ return (x - mean) / std
+
+
+def _diffusers_decoder_state_to_tt(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """Remap diffusers LTX-2 decoder keys (incl. nested resnets) to TT layout."""
+ out: dict[str, torch.Tensor] = {}
+ for key, value in state.items():
+ if key.endswith("conv_shortcut.weight"):
+ out[key.replace("conv_shortcut.weight", "conv_shortcut.conv.weight")] = value
+ elif key.endswith("conv_shortcut.bias"):
+ out[key.replace("conv_shortcut.bias", "conv_shortcut.conv.bias")] = value
+ else:
+ out[key] = value
+ return out
+
+
+class _TorchUNetMidBlock3D(nn.Module):
+ def __init__(
+ self,
+ *,
+ in_channels: int,
+ num_layers: int,
+ resnet_cls: type,
+ ltx2: bool,
+ spatial_padding_mode: str = "zeros",
+ ) -> None:
+ super().__init__()
+ kwargs: dict[str, Any] = {"in_channels": in_channels, "out_channels": in_channels}
+ if ltx2 and spatial_padding_mode != "zeros":
+ kwargs["spatial_padding_mode"] = spatial_padding_mode
+ self.res_blocks = nn.ModuleList([resnet_cls(**kwargs) for _ in range(num_layers)])
+
+ def forward(self, x: torch.Tensor, *, causal: bool, ltx2: bool) -> torch.Tensor:
+ for block in self.res_blocks:
+ x = _run_diffusers_resnet(block, x, causal=causal, ltx2=ltx2)
+ return x
+
+
+def _unpatchify(x: torch.Tensor, *, patch_size_hw: int = 4, patch_size_t: int = 1) -> torch.Tensor:
+ batch_size, num_channels, num_frames, height, width = x.shape
+ p = patch_size_hw
+ p_t = patch_size_t
+ x = x.reshape(batch_size, -1, p_t, p, p, num_frames, height, width)
+ return x.permute(0, 1, 5, 2, 6, 4, 7, 3).flatten(6, 7).flatten(4, 5).flatten(2, 3)
+
+
+def _patchify(x: torch.Tensor, *, patch_size_hw: int = 4, patch_size_t: int = 1) -> torch.Tensor:
+ """Space-to-depth patchify (exact inverse of ``_unpatchify``): folds a
+ ``(p_t, p_hw, p_hw)`` patch into channels with order ``(c, p_t, p_width, p_height)``.
+ Mirrors the Lightricks/TT encoder patchify so both references agree.
+ """
+ batch_size, num_channels, num_frames, height, width = x.shape
+ p = patch_size_hw
+ p_t = patch_size_t
+ x = x.reshape(batch_size, num_channels, num_frames // p_t, p_t, height // p, p, width // p, p)
+ # (b, c, f', p_t, h', p_h, w', p_w) -> (b, c, p_t, p_w, p_h, f', h', w')
+ x = x.permute(0, 1, 3, 7, 5, 2, 4, 6)
+ return x.reshape(batch_size, num_channels * p_t * p * p, num_frames // p_t, height // p, width // p)
+
+
+class _TorchLTXVideoDecoder(nn.Module):
+ """Block-list LTX decoder built from diffusers VAE primitives (matches TT/Lightricks layout)."""
+
+ def __init__(
+ self,
+ *,
+ decoder_blocks: list[tuple[str, dict | int]],
+ in_channels: int = 128,
+ out_channels: int = 3,
+ patch_size: int = 4,
+ base_channels: int = 128,
+ causal: bool = False,
+ spatial_padding_mode: str = "zeros",
+ vae_mods: dict,
+ ) -> None:
+ super().__init__()
+ self.patch_size = patch_size
+ self.causal = causal
+ self.ltx2 = vae_mods["ltx2"]
+ causal_conv = vae_mods["causal_conv"]
+ resnet_cls = vae_mods["resnet"]
+ upsample_cls = vae_mods["upsample"]
+ pixel_norm = vae_mods["pixel_norm"]
+
+ out_channels_patched = out_channels * patch_size**2
+ feature_channels = base_channels * 8
+
+ conv_kwargs: dict[str, Any] = {}
+ if self.ltx2:
+ if spatial_padding_mode != "zeros":
+ conv_kwargs["spatial_padding_mode"] = spatial_padding_mode
+ else:
+ conv_kwargs["is_causal"] = causal
+
+ self.per_channel_statistics = _PerChannelStatistics(in_channels)
+ self.conv_in = causal_conv(
+ in_channels=in_channels,
+ out_channels=feature_channels,
+ kernel_size=3,
+ stride=1,
+ **conv_kwargs,
+ )
+
+ stride_map = {
+ "compress_all": (2, 2, 2),
+ "compress_space": (1, 2, 2),
+ "compress_time": (2, 1, 1),
+ }
+
+ self.up_blocks = nn.ModuleList()
+ ch = feature_channels
+ for block_name, block_params in reversed(decoder_blocks):
+ block_config = {"num_layers": block_params} if isinstance(block_params, int) else block_params
+ if block_name == "res_x":
+ self.up_blocks.append(
+ _TorchUNetMidBlock3D(
+ in_channels=ch,
+ num_layers=block_config["num_layers"],
+ resnet_cls=resnet_cls,
+ ltx2=self.ltx2,
+ spatial_padding_mode=spatial_padding_mode,
+ )
+ )
+ elif block_name in stride_map:
+ multiplier = block_config.get("multiplier", 1)
+ upsample_kwargs: dict[str, Any] = {
+ "in_channels": ch,
+ "stride": stride_map[block_name],
+ "residual": block_config.get("residual", False),
+ "upscale_factor": multiplier,
+ }
+ self.up_blocks.append(upsample_cls(**upsample_kwargs))
+ ch = ch // multiplier
+ else:
+ raise ValueError(f"Unknown decoder block: {block_name}")
+
+ self.conv_norm_out = pixel_norm()
+ self.conv_act = nn.SiLU()
+ self.conv_out = causal_conv(
+ in_channels=ch,
+ out_channels=out_channels_patched,
+ kernel_size=3,
+ stride=1,
+ **conv_kwargs,
+ )
+
+ def forward(self, sample: torch.Tensor) -> torch.Tensor:
+ sample = self.per_channel_statistics.un_normalize(sample)
+ sample = _run_diffusers_causal_conv(self.conv_in, sample, causal=self.causal, ltx2=self.ltx2)
+ for up_block in self.up_blocks:
+ if isinstance(up_block, _TorchUNetMidBlock3D):
+ sample = up_block(sample, causal=self.causal, ltx2=self.ltx2)
+ else:
+ sample = _run_diffusers_upsample(up_block, sample, causal=self.causal, ltx2=self.ltx2)
+ if self.ltx2:
+ sample = self.conv_norm_out(sample)
+ else:
+ sample = self.conv_norm_out(sample.movedim(1, -1)).movedim(-1, 1)
+ sample = self.conv_act(sample)
+ sample = _run_diffusers_causal_conv(self.conv_out, sample, causal=self.causal, ltx2=self.ltx2)
+ return _unpatchify(sample, patch_size_hw=self.patch_size)
+
+
+def _vae_parallel_kwargs(mesh_device: ttnn.MeshDevice) -> dict:
+ """Build a VaeHWParallelConfig + CCLManager for a test mesh.
+
+ Maps mesh axis 0 → height shard, axis 1 → width shard. For a (1, 1) mesh
+ both factors are 1 and ``LTXCausalConv3d`` skips halo exchange.
+ """
+ mesh_shape = tuple(mesh_device.shape)
+ parallel_config = VaeHWParallelConfig(
+ height_parallel=ParallelFactor(factor=mesh_shape[0], mesh_axis=0),
+ width_parallel=ParallelFactor(factor=mesh_shape[1], mesh_axis=1),
+ )
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear)
+ return {"parallel_config": parallel_config, "ccl_manager": ccl_manager}
+
+
+# Single-chip: no fabric (avoids BH fabric router handshake when only one device is opened).
+# Stand-alone conv/res/upsample tests use raw (unsharded) tensors — 1x1 only.
+_LTX_VAE_MESH_DEVICE_PARAMS = [
+ ((1, 1), {}),
+]
+_LTX_VAE_MESH_DEVICE_IDS = ["1x1"]
+
+# (num_frames, height, width): H/W divisible by 64; (num_frames - 1) % 8 == 0 for VAE.
+_LTX_DECODER_SHAPE_PARAMS = [
+ pytest.param(145, 1088, 1920, id="145f_1088x1920"), # 1080p production (19 latent frames)
+]
+
+_LTX_DECODER_2K_SHAPE_PARAMS = [
+ pytest.param(9, 1088, 2048, id="9f_1088x2048"), # 2K (1088x2048, latent 34x64)
+]
+
+# Smoke resolution — only shape exercised on a 1x1 mesh in test_ltx_video_decoder.
+_LTX_DECODER_1X1_MAX_HW = (256, 256)
+
+
+def _skip_ltx_decoder_if_single_chip_too_large(mesh_device: ttnn.MeshDevice, height: int, width: int) -> None:
+ if tuple(mesh_device.shape) == (1, 1) and (
+ height > _LTX_DECODER_1X1_MAX_HW[0] or width > _LTX_DECODER_1X1_MAX_HW[1]
+ ):
+ pytest.skip(f"{height}x{width} LTX decode requires a multi-device mesh")
+
+
+def _unwrap_tt_output(tt_out):
+ if isinstance(tt_out, (tuple, list)):
+ # Newer TT path may return auxiliary tensors alongside the primary output.
+ return tt_out[0]
+ return tt_out
+
+
+# Full-decoder test — exercises the production decoder_blocks under sharding.
+_LTX_PROD_DECODER_BLOCKS = [
+ ("res_x", {"num_layers": 4}),
+ ("compress_space", {"multiplier": 2}),
+ ("res_x", {"num_layers": 6}),
+ ("compress_time", {"multiplier": 2}),
+ ("res_x", {"num_layers": 4}),
+ ("compress_all", {"multiplier": 1}),
+ ("res_x", {"num_layers": 2}),
+ ("compress_all", {"multiplier": 2}),
+ ("res_x", {"num_layers": 2}),
+]
+
+
+@pytest.mark.parametrize(
+ "in_c, out_c, kernel_size, stride, T, H, W",
+ [
+ (128, 128, 3, 1, 3, 16, 16), # small residual block conv
+ (128, 256, 3, 1, 3, 16, 16), # channel expansion
+ (48, 128, 3, 1, 5, 32, 32), # conv_in (after patchify)
+ (128, 1024, 3, 1, 21, 17, 15), # ltx_s0_conv_in @ 2x4 per-device (1080p path)
+ (1024, 1024, 3, 1, 21, 17, 15), # ltx_s0_res
+ (1024, 4096, 3, 1, 21, 17, 15), # ltx_s0_up (upsample projection)
+ (512, 512, 3, 1, 39, 34, 30), # ltx_s1_res
+ (128, 48, 3, 1, 147, 136, 120), # ltx_s4_out
+ ],
+ ids=[
+ "res_128_128",
+ "expand_128_256",
+ "conv_in_48_128",
+ "ltx_s0_conv_in",
+ "ltx_s0_res",
+ "ltx_s0_up",
+ "ltx_s1_res",
+ "ltx_s4_out",
+ ],
+)
+@pytest.mark.parametrize("mean, std", [(0, 1), (2, 3)], ids=["std1", "std3"])
+@pytest.mark.parametrize(
+ "mesh_device, device_params",
+ _LTX_VAE_MESH_DEVICE_PARAMS,
+ ids=_LTX_VAE_MESH_DEVICE_IDS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_causal_conv3d(
+ mesh_device: ttnn.MeshDevice,
+ in_c: int,
+ out_c: int,
+ kernel_size: int,
+ stride: int,
+ T: int,
+ H: int,
+ W: int,
+ mean: float,
+ std: float,
+):
+ """
+ Test LTXCausalConv3d against diffusers LTX causal conv reference.
+ """
+ vae_mods = _require_diffusers_ltx_vae()
+ TorchCausalConv3d = vae_mods["causal_conv"]
+ ltx2 = vae_mods["ltx2"]
+
+ B = 1
+ torch.manual_seed(42)
+
+ conv_kwargs: dict[str, Any] = {
+ "in_channels": in_c,
+ "out_channels": out_c,
+ "kernel_size": kernel_size,
+ "stride": stride if ltx2 else (stride, stride, stride),
+ }
+ if not ltx2:
+ conv_kwargs["is_causal"] = True
+
+ torch_model = TorchCausalConv3d(**conv_kwargs)
+ torch_model.eval()
+
+ # TT model
+ tt_model = LTXCausalConv3d(
+ in_channels=in_c,
+ out_channels=out_c,
+ kernel_size=kernel_size,
+ stride=stride,
+ mesh_device=mesh_device,
+ **_vae_parallel_kwargs(mesh_device),
+ )
+ tt_model.load_torch_state_dict(torch_model.state_dict())
+
+ # Input
+ x = torch.randn(B, in_c, T, H, W, dtype=torch.float32) * std + mean
+
+ # PyTorch forward (BCTHW format)
+ with torch.no_grad():
+ torch_out = _run_diffusers_causal_conv(torch_model, x, causal=True, ltx2=ltx2)
+
+ # TT forward (BTHWC format)
+ x_bthwc = x.permute(0, 2, 3, 4, 1) # (B, T, H, W, C)
+ x_bthwc = conv_pad_in_channels(x_bthwc) # Pad C to alignment
+ x_tt = ttnn.from_torch(x_bthwc, device=mesh_device, layout=ttnn.ROW_MAJOR_LAYOUT, dtype=ttnn.bfloat16)
+
+ tt_out = _unwrap_tt_output(tt_model(x_tt))
+ tt_out_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_out)[0]) # (B, T_out, H_out, W_out, C_out)
+ tt_out_torch = tt_out_torch[:, :, :, :, :out_c] # Trim padded channels
+ tt_out_torch = tt_out_torch.permute(0, 4, 1, 2, 3) # Back to BCTHW
+
+ logger.info(f"PyTorch out: {torch_out.shape}, TT out: {tt_out_torch.shape}")
+ assert_quality(torch_out, tt_out_torch, pcc=0.999)
+ logger.info(f"PASSED: LTXCausalConv3d ({in_c}->{out_c}) matches reference")
+
+
+@pytest.mark.parametrize(
+ "in_c, out_c, T, H, W",
+ [
+ (128, 128, 3, 16, 16), # same channels (no shortcut)
+ (128, 256, 3, 16, 16), # channel expansion (with shortcut)
+ (1024, 1024, 21, 17, 15), # ltx_s0_res @ 2x4 per-device
+ (512, 512, 39, 34, 30), # ltx_s1_res
+ (256, 256, 147, 70, 66), # ltx_s3_res @ 2K per-device
+ (128, 128, 147, 138, 130), # ltx_s4_res @ 2K per-device
+ ],
+ ids=[
+ "same_channels",
+ "expand_channels",
+ "ltx_s0_res",
+ "ltx_s1_res",
+ "ltx_s3_res_2k",
+ "ltx_s4_res_2k",
+ ],
+)
+@pytest.mark.parametrize("mean, std", [(0, 1), (2, 3)], ids=["std1", "std3"])
+@pytest.mark.parametrize(
+ "mesh_device, device_params",
+ _LTX_VAE_MESH_DEVICE_PARAMS,
+ ids=_LTX_VAE_MESH_DEVICE_IDS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_resnet_block(
+ mesh_device: ttnn.MeshDevice, in_c: int, out_c: int, T: int, H: int, W: int, mean: float, std: float
+):
+ """
+ Test LTXResnetBlock3D against diffusers LTX ResnetBlock3D reference.
+ """
+ vae_mods = _require_diffusers_ltx_vae()
+ TorchResnetBlock3D = vae_mods["resnet"]
+ ltx2 = vae_mods["ltx2"]
+
+ B = 1
+ torch.manual_seed(42)
+
+ torch_model = TorchResnetBlock3D(in_channels=in_c, out_channels=out_c)
+ torch_model.eval()
+
+ # TT model
+ tt_model = LTXResnetBlock3D(
+ in_channels=in_c,
+ out_channels=out_c,
+ mesh_device=mesh_device,
+ **_vae_parallel_kwargs(mesh_device),
+ )
+ tt_model.load_torch_state_dict(_diffusers_resnet_state_to_tt(torch_model.state_dict()))
+
+ # Input
+ x = torch.randn(B, in_c, T, H, W, dtype=torch.float32) * std + mean
+
+ # PyTorch forward
+ with torch.no_grad():
+ torch_out = _run_diffusers_resnet(torch_model, x, causal=True, ltx2=ltx2)
+
+ # TT forward
+ x_bthwc = x.permute(0, 2, 3, 4, 1)
+ x_bthwc = conv_pad_in_channels(x_bthwc)
+ x_tt = ttnn.from_torch(x_bthwc, device=mesh_device, layout=ttnn.ROW_MAJOR_LAYOUT, dtype=ttnn.bfloat16)
+
+ tt_out = _unwrap_tt_output(tt_model(x_tt))
+ tt_out_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_out)[0])
+ tt_out_torch = tt_out_torch[:, :, :, :, :out_c]
+ tt_out_torch = tt_out_torch.permute(0, 4, 1, 2, 3)
+
+ logger.info(f"PyTorch out: {torch_out.shape}, TT out: {tt_out_torch.shape}")
+ # Lower threshold for channel-expansion blocks (GroupNorm(1) vs layer_norm precision)
+ min_pcc = 0.995 if in_c != out_c else 0.999
+ assert_quality(torch_out, tt_out_torch, pcc=min_pcc)
+ logger.info(f"PASSED: LTXResnetBlock3D ({in_c}->{out_c}) matches reference")
+
+
+@pytest.mark.parametrize(
+ "in_c, stride, T, H, W",
+ [
+ (128, (2, 2, 2), 4, 8, 8), # full 3D upsample (compress_all)
+ (128, (1, 2, 2), 3, 8, 8), # spatial only (compress_space)
+ (128, (2, 1, 1), 4, 8, 8), # temporal only (compress_time)
+ (512, (2, 2, 2), 39, 34, 30), # compress_all @ ltx_s1 stage
+ (1024, (1, 2, 2), 21, 17, 15), # compress_space @ ltx_s0 stage
+ (256, (2, 1, 1), 75, 68, 60), # compress_time @ ltx_s2 stage
+ ],
+ ids=[
+ "upsample_all",
+ "upsample_space",
+ "upsample_time",
+ "upsample_all_s1",
+ "upsample_space_s0",
+ "upsample_time_s2",
+ ],
+)
+@pytest.mark.parametrize("mean, std", [(0, 1), (2, 3)], ids=["std1", "std3"])
+@pytest.mark.parametrize(
+ "mesh_device, device_params",
+ _LTX_VAE_MESH_DEVICE_PARAMS,
+ ids=_LTX_VAE_MESH_DEVICE_IDS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_depth_to_space_upsample(
+ mesh_device: ttnn.MeshDevice, in_c: int, stride: tuple, T: int, H: int, W: int, mean: float, std: float
+):
+ """Test LTXDepthToSpaceUpsample against diffusers LTX upsampler reference."""
+ vae_mods = _require_diffusers_ltx_vae()
+ TorchUpsample = vae_mods["upsample"]
+ ltx2 = vae_mods["ltx2"]
+
+ B = 1
+ torch.manual_seed(42)
+
+ torch_model = TorchUpsample(in_channels=in_c, stride=stride, upscale_factor=1)
+ torch_model.eval()
+
+ tt_model = LTXDepthToSpaceUpsample(
+ in_channels=in_c,
+ stride=stride,
+ mesh_device=mesh_device,
+ **_vae_parallel_kwargs(mesh_device),
+ )
+ tt_model.load_torch_state_dict(torch_model.state_dict())
+
+ x = torch.randn(B, in_c, T, H, W, dtype=torch.float32) * std + mean
+ with torch.no_grad():
+ torch_out = _run_diffusers_upsample(torch_model, x, causal=True, ltx2=ltx2)
+
+ x_bthwc = x.permute(0, 2, 3, 4, 1)
+ x_bthwc = conv_pad_in_channels(x_bthwc)
+ x_tt = ttnn.from_torch(x_bthwc, device=mesh_device, layout=ttnn.ROW_MAJOR_LAYOUT, dtype=ttnn.bfloat16)
+
+ tt_out = _unwrap_tt_output(tt_model(x_tt))
+ tt_out_torch = ttnn.to_torch(ttnn.get_device_tensors(tt_out)[0])
+ out_c = torch_out.shape[1]
+ tt_out_torch = tt_out_torch[:, :, :, :, :out_c].permute(0, 4, 1, 2, 3)
+
+ logger.info(f"Upsample stride={stride}: {x.shape} -> torch {torch_out.shape}, tt {tt_out_torch.shape}")
+ assert_quality(torch_out, tt_out_torch, pcc=0.999)
+ logger.info(f"PASSED: DepthToSpaceUpsample stride={stride}")
+
+
+def _run_ltx_decoder_parity(
+ mesh_device: ttnn.MeshDevice,
+ h_axis: int,
+ w_axis: int,
+ num_links: int,
+ num_frames: int,
+ height: int,
+ width: int,
+ *,
+ mean: float = 0.0,
+ std: float = 1.0,
+ pcc: float = 0.99,
+):
+ """Build torch + TT production decoders at ``(num_frames, height, width)`` and
+ assert numerical parity. Shared by the standard multi-resolution decoder test
+ and the 2K decoder test. The TT decoder is given num_frames/height/width so the
+ dim walker fires and ``_BLOCKINGS`` gets an exact-match lookup for every conv3d.
+ """
+ vae_mods = _require_diffusers_ltx_vae()
+ if not vae_mods["ltx2"]:
+ pytest.skip("Full decoder parity requires diffusers autoencoder_kl_ltx2 (runtime causal= support)")
+
+ B = 1
+ torch.manual_seed(42)
+ spatial_compression = 32
+ latent_frames = (num_frames - 1) // 8 + 1
+ latent_h = height // spatial_compression
+ latent_w = width // spatial_compression
+
+ # PyTorch reference — full production decoder shape (diffusers primitives).
+ torch_decoder = _TorchLTXVideoDecoder(
+ decoder_blocks=_LTX_PROD_DECODER_BLOCKS,
+ in_channels=128,
+ out_channels=3,
+ patch_size=4,
+ base_channels=128,
+ causal=False,
+ spatial_padding_mode="zeros",
+ vae_mods=vae_mods,
+ )
+ torch_decoder.eval()
+
+ # Per-channel stats set to identity so denormalization is a no-op (random
+ # weights produce garbage stats otherwise).
+ torch_state = torch_decoder.state_dict()
+ torch_state["per_channel_statistics.mean-of-means"] = torch.zeros(128)
+ torch_state["per_channel_statistics.std-of-means"] = torch.ones(128)
+ torch_decoder.load_state_dict(torch_state)
+
+ # Build VAE parallel config from h_axis / w_axis (mirrors test_wan_decoder).
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear, num_links=num_links)
+ parallel_config = VaeHWParallelConfig(
+ height_parallel=ParallelFactor(factor=tuple(mesh_device.shape)[h_axis], mesh_axis=h_axis),
+ width_parallel=ParallelFactor(factor=tuple(mesh_device.shape)[w_axis], mesh_axis=w_axis),
+ )
+
+ # TT decoder — pass num_frames/height/width so the dim walker fires and
+ # _BLOCKINGS gets an exact-match lookup for every conv3d.
+ tt_decoder = LTXVideoDecoder(
+ decoder_blocks=_LTX_PROD_DECODER_BLOCKS,
+ in_channels=128,
+ out_channels=3,
+ patch_size=4,
+ base_channels=128,
+ causal=False,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ mesh_device=mesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+ tt_decoder.load_torch_state_dict(_diffusers_decoder_state_to_tt(torch_decoder.state_dict()))
+
+ # Latent shape matches what the pipeline would produce at this resolution.
+ latent = torch.randn(B, 128, latent_frames, latent_h, latent_w, dtype=torch.float32) * std + mean
+
+ with torch.no_grad():
+ torch_out = torch_decoder(latent)
+
+ tt_out = tt_decoder(latent)
+
+ logger.info(f"Decoder: {latent.shape} -> torch {torch_out.shape}, tt {tt_out.shape}")
+ assert_quality(torch_out, tt_out, pcc=pcc)
+ logger.info("PASSED: LTXVideoDecoder matches PyTorch reference")
+
+
+@pytest.mark.parametrize("num_frames, height, width", _LTX_DECODER_SHAPE_PARAMS)
+@pytest.mark.parametrize("mean, std", [(0, 1), (2, 3)], ids=["std1", "std3"])
+@pytest.mark.parametrize(
+ ("mesh_device", "h_axis", "w_axis", "num_links", "device_params", "topology"),
+ LTX_VAE_DECODER_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_video_decoder(
+ mesh_device: ttnn.MeshDevice,
+ num_frames: int,
+ height: int,
+ width: int,
+ mean: float,
+ std: float,
+ h_axis: int,
+ w_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+):
+ """
+ Test full LTXVideoDecoder against PyTorch VideoDecoder reference using the
+ production LTX-2.3 22B decoder_blocks. Random weights — checks numerical
+ parity, not absolute output quality.
+
+ For 2x4 this validates the parallel halo-exchange path end-to-end, which
+ is otherwise only covered by the slow pipeline-level test.
+ """
+ _skip_ltx_decoder_if_single_chip_too_large(mesh_device, height, width)
+ _run_ltx_decoder_parity(mesh_device, h_axis, w_axis, num_links, num_frames, height, width, mean=mean, std=std)
+
+
+_NUM_FRAMES = 145
+_HEIGHT = 1088
+_WIDTH = 1920
+
+
+def _build_tt_decoder(mesh):
+ """Production decoder with random weights; torch module built for its
+ state_dict only (no host forward)."""
+ vae_mods = _require_diffusers_ltx_vae()
+ if not vae_mods["ltx2"]:
+ pytest.skip("LTX-2 decoder profiling requires diffusers autoencoder_kl_ltx2")
+
+ torch.manual_seed(42)
+ torch_decoder = _TorchLTXVideoDecoder(
+ decoder_blocks=_LTX_PROD_DECODER_BLOCKS,
+ in_channels=128,
+ out_channels=3,
+ patch_size=4,
+ base_channels=128,
+ causal=False,
+ spatial_padding_mode="zeros",
+ vae_mods=vae_mods,
+ )
+ torch_decoder.eval()
+ state = torch_decoder.state_dict()
+ # Identity per-channel stats so denorm is a no-op (random weights give garbage stats).
+ state["per_channel_statistics.mean-of-means"] = torch.zeros(128)
+ state["per_channel_statistics.std-of-means"] = torch.ones(128)
+ torch_decoder.load_state_dict(state)
+
+ # num_links=2 mirrors the BH production device config; NP_LINKS probes the W-transport bound.
+ ccl_manager = CCLManager(mesh, topology=ttnn.Topology.Linear, num_links=int(os.environ.get("NP_LINKS", "2")))
+ parallel_config = VaeHWParallelConfig(
+ height_parallel=ParallelFactor(factor=tuple(mesh.shape)[0], mesh_axis=0),
+ width_parallel=ParallelFactor(factor=tuple(mesh.shape)[1], mesh_axis=1),
+ )
+ tt_decoder = LTXVideoDecoder(
+ decoder_blocks=_LTX_PROD_DECODER_BLOCKS,
+ in_channels=128,
+ out_channels=3,
+ patch_size=4,
+ base_channels=128,
+ causal=False,
+ num_frames=_NUM_FRAMES,
+ height=_HEIGHT,
+ width=_WIDTH,
+ mesh_device=mesh,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+ tt_decoder.load_torch_state_dict(_diffusers_decoder_state_to_tt(torch_decoder.state_dict()))
+ return tt_decoder
+
+
+def _latent():
+ latent_frames = (_NUM_FRAMES - 1) // 8 + 1
+ return torch.randn(1, 128, latent_frames, _HEIGHT // 32, _WIDTH // 32, dtype=torch.float32)
+
+
+@pytest.mark.parametrize("device_params", [{"fabric_config": ttnn.FabricConfig.FABRIC_1D}], indirect=True)
+@pytest.mark.parametrize("mesh_device", [(4, 8)], indirect=True)
+def test_prof_vae_ltx_devicetime(mesh_device, device_params):
+ mesh = mesh_device.create_submesh(ttnn.MeshShape(4, 8))
+ tt_decoder = _build_tt_decoder(mesh)
+ latent = _latent()
+
+ # One forward only: the decoder emits ~10x a transformer block's ops, so warm-ups
+ # would overflow the profiler DRAM buffer. Device kernel durations are warm-independent.
+ signpost("start")
+ _ = tt_decoder(latent)
+ ttnn.synchronize_device(mesh)
+ signpost("stop")
+ ttnn.ReadDeviceProfiler(mesh)
+
+
+@pytest.mark.parametrize("num_frames, height, width", _LTX_DECODER_2K_SHAPE_PARAMS)
+@pytest.mark.parametrize("mean, std", [(0, 1)])
+@pytest.mark.parametrize(
+ ("mesh_device", "h_axis", "w_axis", "num_links", "device_params", "topology"),
+ LTX_VAE_DECODER_MULTI_ONLY_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_video_decoder_2k(
+ mesh_device: ttnn.MeshDevice,
+ num_frames: int,
+ height: int,
+ width: int,
+ mean: float,
+ std: float,
+ h_axis: int,
+ w_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+):
+ """Full LTXVideoDecoder at the 2K production resolution (2048x1080 -> 1088x2048).
+
+ Multi-device only (2x4 is the production layout; 4x8 also exercised). The two
+ 512->4096 / 1024->4096 upsample-projection convs are not in the 2K ``_BLOCKINGS``
+ table (they OOM a 2x4 mesh at full production T), so at 2K they fall back to the
+ generic blockings — correct, but not yet perf-tuned. See conv3d.py.
+ """
+ _run_ltx_decoder_parity(mesh_device, h_axis, w_axis, num_links, num_frames, height, width, mean=mean, std=std)
+
+
+# ---------------------------------------------------------------------------
+# Encoder parity (LTXVideoEncoder vs a diffusers-primitives reference encoder)
+# ---------------------------------------------------------------------------
+def _diffusers_encoder_state_to_tt(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
+ """Remap diffusers LTX-2 encoder keys (incl. nested resnets) to the TT/Lightricks layout."""
+ out: dict[str, torch.Tensor] = {}
+ for key, value in state.items():
+ if key.endswith("conv_shortcut.weight"):
+ out[key.replace("conv_shortcut.weight", "conv_shortcut.conv.weight")] = value
+ elif key.endswith("conv_shortcut.bias"):
+ out[key.replace("conv_shortcut.bias", "conv_shortcut.conv.bias")] = value
+ else:
+ out[key] = value
+ return out
+
+
+class _TorchLTXVideoEncoder(nn.Module):
+ """Block-list LTX encoder built from diffusers VAE primitives (matches TT/Lightricks layout).
+
+ Mirrors the reference ``VideoEncoder`` encode path: patchify -> conv_in -> down_blocks
+ (``res_x`` mid blocks + ``compress_*_res`` space-to-depth downsamplers) -> PixelNorm + SiLU
+ -> conv_out (latent means) -> per-channel normalize. Always causal.
+ """
+
+ _STRIDE_MAP = {
+ "compress_time_res": (2, 1, 1),
+ "compress_space_res": (1, 2, 2),
+ "compress_all_res": (2, 2, 2),
+ }
+
+ def __init__(
+ self,
+ *,
+ encoder_blocks: list[tuple[str, dict | int]],
+ in_channels: int = 3,
+ out_channels: int = 128,
+ patch_size: int = 4,
+ base_channels: int = 128,
+ causal: bool = True,
+ spatial_padding_mode: str = "zeros",
+ vae_mods: dict,
+ ) -> None:
+ super().__init__()
+ self.patch_size = patch_size
+ self.causal = causal
+ self.ltx2 = vae_mods["ltx2"]
+ causal_conv = vae_mods["causal_conv"]
+ resnet_cls = vae_mods["resnet"]
+ downsample_cls = vae_mods["downsample"]
+ pixel_norm = vae_mods["pixel_norm"]
+
+ in_channels_patched = in_channels * patch_size**2
+ feature_channels = base_channels
+
+ conv_kwargs: dict[str, Any] = {}
+ if self.ltx2:
+ if spatial_padding_mode != "zeros":
+ conv_kwargs["spatial_padding_mode"] = spatial_padding_mode
+ else:
+ conv_kwargs["is_causal"] = causal
+
+ self.conv_in = causal_conv(
+ in_channels=in_channels_patched,
+ out_channels=feature_channels,
+ kernel_size=3,
+ stride=1,
+ **conv_kwargs,
+ )
+
+ self.down_blocks = nn.ModuleList()
+ ch = feature_channels
+ for block_name, block_params in encoder_blocks:
+ block_config = {"num_layers": block_params} if isinstance(block_params, int) else block_params
+ if block_name == "res_x":
+ self.down_blocks.append(
+ _TorchUNetMidBlock3D(
+ in_channels=ch,
+ num_layers=block_config["num_layers"],
+ resnet_cls=resnet_cls,
+ ltx2=self.ltx2,
+ spatial_padding_mode=spatial_padding_mode,
+ )
+ )
+ elif block_name in self._STRIDE_MAP:
+ multiplier = block_config.get("multiplier", 2)
+ new_ch = ch * multiplier
+ downsample_kwargs: dict[str, Any] = {
+ "in_channels": ch,
+ "out_channels": new_ch,
+ "stride": self._STRIDE_MAP[block_name],
+ }
+ if self.ltx2 and spatial_padding_mode != "zeros":
+ downsample_kwargs["spatial_padding_mode"] = spatial_padding_mode
+ self.down_blocks.append(downsample_cls(**downsample_kwargs))
+ ch = new_ch
+ else:
+ raise ValueError(f"Unknown encoder block: {block_name}")
+
+ self.conv_norm_out = pixel_norm()
+ self.conv_act = nn.SiLU()
+ self.conv_out = causal_conv(
+ in_channels=ch,
+ out_channels=out_channels,
+ kernel_size=3,
+ stride=1,
+ **conv_kwargs,
+ )
+ self.per_channel_statistics = _PerChannelStatistics(out_channels)
+
+ def forward(self, sample: torch.Tensor) -> torch.Tensor:
+ # Crop trailing frames so F == 1 + 8*k (mirrors the reference; no-op for the test shapes).
+ frames_count = sample.shape[2]
+ if (frames_count - 1) % 8 != 0:
+ sample = sample[:, :, : frames_count - ((frames_count - 1) % 8)]
+
+ sample = _patchify(sample, patch_size_hw=self.patch_size)
+ sample = _run_diffusers_causal_conv(self.conv_in, sample, causal=self.causal, ltx2=self.ltx2)
+ for down_block in self.down_blocks:
+ if isinstance(down_block, _TorchUNetMidBlock3D):
+ sample = down_block(sample, causal=self.causal, ltx2=self.ltx2)
+ else:
+ sample = _run_diffusers_downsample(down_block, sample, causal=self.causal, ltx2=self.ltx2)
+ if self.ltx2:
+ sample = self.conv_norm_out(sample)
+ else:
+ sample = self.conv_norm_out(sample.movedim(1, -1)).movedim(-1, 1)
+ sample = self.conv_act(sample)
+ sample = _run_diffusers_causal_conv(self.conv_out, sample, causal=self.causal, ltx2=self.ltx2)
+ return self.per_channel_statistics.normalize(sample)
+
+
+# Small encoder config: conv_in (48->128) -> res_x -> compress_space_res -> compress_time_res.
+# Avoids resnet channel-shortcut paths (res_x keeps channels; compress_*_res change channels via
+# space-to-depth), so the parity check isolates the new encoder primitives.
+_LTX_ENCODER_SMALL_BLOCKS = [
+ ("res_x", {"num_layers": 1}),
+ ("compress_space_res", {"multiplier": 2}),
+ ("compress_time_res", {"multiplier": 2}),
+]
+
+
+@pytest.mark.parametrize(
+ "num_frames, height, width",
+ [(9, 64, 64)],
+ ids=["9f_64x64"],
+)
+@pytest.mark.parametrize(
+ "mesh_device, device_params",
+ _LTX_VAE_MESH_DEVICE_PARAMS,
+ ids=_LTX_VAE_MESH_DEVICE_IDS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_video_encoder(mesh_device: ttnn.MeshDevice, num_frames: int, height: int, width: int):
+ """LTXVideoEncoder parity vs a diffusers-primitives reference encoder.
+
+ Random weights — checks the encode path (patchify -> conv_in -> down_blocks -> PixelNorm+SiLU
+ -> conv_out (means) -> per-channel normalize) matches the reference numerically.
+ """
+ vae_mods = _require_diffusers_ltx_vae()
+ if not vae_mods["ltx2"]:
+ pytest.skip("Encoder parity requires diffusers autoencoder_kl_ltx2 (runtime causal= support)")
+
+ torch.manual_seed(42)
+ latent_channels = 128
+
+ ref = _TorchLTXVideoEncoder(
+ encoder_blocks=_LTX_ENCODER_SMALL_BLOCKS,
+ in_channels=3,
+ out_channels=latent_channels,
+ patch_size=4,
+ base_channels=latent_channels,
+ causal=True,
+ spatial_padding_mode="zeros",
+ vae_mods=vae_mods,
+ )
+ # Set known per-channel stats so normalize() = (x - mean) / std is deterministic/well-conditioned.
+ with torch.no_grad():
+ ref.per_channel_statistics.get_buffer("mean-of-means").copy_(torch.randn(latent_channels))
+ ref.per_channel_statistics.get_buffer("std-of-means").copy_(torch.rand(latent_channels) + 0.5)
+ ref.eval()
+
+ x = torch.randn(1, 3, num_frames, height, width, dtype=torch.float32)
+ with torch.no_grad():
+ ref_out = ref(x.clone()) # (1, 128, F', H', W')
+
+ tt_model = LTXVideoEncoder(
+ encoder_blocks=_LTX_ENCODER_SMALL_BLOCKS,
+ in_channels=3,
+ out_channels=latent_channels,
+ patch_size=4,
+ mesh_device=mesh_device,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ **_vae_parallel_kwargs(mesh_device),
+ )
+ tt_model.load_torch_state_dict(_diffusers_encoder_state_to_tt(ref.state_dict()))
+
+ tt_out = _unwrap_tt_output(tt_model(x.clone())) # host (B, C, F', H', W')
+
+ logger.info(f"ref out {tuple(ref_out.shape)}, TT out {tuple(tt_out.shape)}")
+ assert tt_out.shape == ref_out.shape, f"shape mismatch: TT {tt_out.shape} vs ref {ref_out.shape}"
+ assert torch.isfinite(tt_out).all(), "NaN/Inf in TT encoder output"
+ assert_quality(ref_out, tt_out, pcc=0.99)
+ logger.info("PASSED: LTXVideoEncoder matches diffusers reference encoder")
+
+
+# ---------------------------------------------------------------------------
+# Production encoder parity under sharding (regression for the I2V VAE-encoder bug)
+# ---------------------------------------------------------------------------
+# Full LTX-2.3 22B encoder block list (from the checkpoint config metadata). The two
+# ``compress_all_res`` blocks are NOT exercised by ``test_ltx_video_encoder`` above, and
+# only matter under spatial sharding — they fold a (2,2,2) patch into channels per device.
+_LTX_PROD_ENCODER_BLOCKS = [
+ ("res_x", {"num_layers": 4}),
+ ("compress_space_res", {"multiplier": 2}),
+ ("res_x", {"num_layers": 6}),
+ ("compress_time_res", {"multiplier": 2}),
+ ("res_x", {"num_layers": 4}),
+ ("compress_all_res", {"multiplier": 2}),
+ ("res_x", {"num_layers": 2}),
+ ("compress_all_res", {"multiplier": 1}),
+ ("res_x", {"num_layers": 2}),
+]
+
+
+def _run_ltx_encoder_parity(
+ mesh_device: ttnn.MeshDevice,
+ h_axis: int,
+ w_axis: int,
+ num_links: int,
+ num_frames: int,
+ height: int,
+ width: int,
+ *,
+ pcc: float = 0.99,
+):
+ """Build torch (diffusers reference) + TT production encoders at ``(num_frames, height, width)``
+ and assert numerical parity. The TT encoder shards H/W across the mesh exactly like the I2V
+ pipeline; this is the path that fails when a per-device spatial dim is not divisible by the
+ downsample stride (patch groups would straddle a shard boundary)."""
+ vae_mods = _require_diffusers_ltx_vae()
+ if not vae_mods["ltx2"]:
+ pytest.skip("Encoder parity requires diffusers autoencoder_kl_ltx2 (runtime causal= support)")
+
+ torch.manual_seed(42)
+ latent_channels = 128
+
+ ref = _TorchLTXVideoEncoder(
+ encoder_blocks=_LTX_PROD_ENCODER_BLOCKS,
+ in_channels=3,
+ out_channels=latent_channels,
+ patch_size=4,
+ base_channels=latent_channels,
+ causal=True,
+ spatial_padding_mode="zeros",
+ vae_mods=vae_mods,
+ )
+ with torch.no_grad():
+ ref.per_channel_statistics.get_buffer("mean-of-means").copy_(torch.randn(latent_channels))
+ ref.per_channel_statistics.get_buffer("std-of-means").copy_(torch.rand(latent_channels) + 0.5)
+ ref.eval()
+
+ x = torch.randn(1, 3, num_frames, height, width, dtype=torch.float32)
+ with torch.no_grad():
+ ref_out = ref(x.clone()) # (1, 128, F', H', W')
+
+ ccl_manager = CCLManager(mesh_device, topology=ttnn.Topology.Linear, num_links=num_links)
+ parallel_config = VaeHWParallelConfig(
+ height_parallel=ParallelFactor(factor=tuple(mesh_device.shape)[h_axis], mesh_axis=h_axis),
+ width_parallel=ParallelFactor(factor=tuple(mesh_device.shape)[w_axis], mesh_axis=w_axis),
+ )
+
+ tt_model = LTXVideoEncoder(
+ encoder_blocks=_LTX_PROD_ENCODER_BLOCKS,
+ in_channels=3,
+ out_channels=latent_channels,
+ patch_size=4,
+ mesh_device=mesh_device,
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+ tt_model.load_torch_state_dict(_diffusers_encoder_state_to_tt(ref.state_dict()))
+
+ tt_out = _unwrap_tt_output(tt_model(x.clone()))
+
+ logger.info(f"prod encoder: ref out {tuple(ref_out.shape)}, TT out {tuple(tt_out.shape)}")
+ assert tt_out.shape == ref_out.shape, f"shape mismatch: TT {tt_out.shape} vs ref {ref_out.shape}"
+ assert torch.isfinite(tt_out).all(), "NaN/Inf in TT encoder output"
+ assert_quality(ref_out, tt_out, pcc=pcc)
+ logger.info("PASSED: production LTXVideoEncoder matches diffusers reference encoder")
+
+
+# (num_frames, H, W). On a 4x8 mesh (H//4, W//8) the per-device spatial dim after patchify and
+# the early downsamples decides whether the last compress_all_res can fold locally:
+# - "clean": per-device dims stay even through every fold (fast local path).
+# - "boundary": per-device dim becomes odd at the last compress_all_res, so the space-to-depth
+# patch would straddle a shard boundary — the case that corrupts the I2V latent.
+_LTX_ENCODER_PROD_SHAPE_PARAMS = [
+ pytest.param(1, 128, 256, id="1f_128x256_clean"),
+ pytest.param(1, 192, 384, id="1f_192x384_boundary"),
+ # The two real I2V conditioning-image resolutions (single frame): stage-0 (544x960 -> latent
+ # 17x30) and stage-1 (1088x1920 -> latent 34x60). Both put an odd per-device spatial dim into
+ # the last compress_all_res on 4x8 — this is the exact case that corrupted the I2V latent.
+ pytest.param(1, 544, 960, id="1f_544x960_stage0"),
+ pytest.param(1, 1088, 1920, id="1f_1088x1920_stage1"),
+]
+
+
+@pytest.mark.parametrize("num_frames, height, width", _LTX_ENCODER_PROD_SHAPE_PARAMS)
+@pytest.mark.parametrize(
+ ("mesh_device", "h_axis", "w_axis", "num_links", "device_params", "topology"),
+ LTX_VAE_ENCODER_PROD_MESH_PARAMS,
+ indirect=["mesh_device", "device_params"],
+)
+def test_ltx_video_encoder_prod(
+ mesh_device: ttnn.MeshDevice,
+ num_frames: int,
+ height: int,
+ width: int,
+ h_axis: int,
+ w_axis: int,
+ num_links: int,
+ topology: ttnn.Topology,
+):
+ """Production-config LTXVideoEncoder parity vs the diffusers reference, sharded like the I2V pipeline.
+
+ The "boundary" resolution reproduces the I2V conditioning-latent corruption (device-vs-host
+ PCC ~0.3): a per-device spatial dim becomes odd at the second ``compress_all_res``, so the
+ per-device space-to-depth folds a patch that should straddle two shards.
+ """
+ _run_ltx_encoder_parity(mesh_device, h_axis, w_axis, num_links, num_frames, height, width)
diff --git a/code/models/tt_dit/tests/models/minimax_h3/.gitkeep b/code/models/tt_dit/tests/models/minimax_h3/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/code/models/tt_dit/tests/models/minimax_h3/common.py b/code/models/tt_dit/tests/models/minimax_h3/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..5cddf6de79636766469a7cb90d718fbefc963e14
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/common.py
@@ -0,0 +1,330 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Shared helpers for the MiniMax-H3 bringup tests."""
+
+import json
+import math
+import os
+
+import numpy as np
+import pytest
+import torch
+from PIL import Image
+
+import ttnn
+
+from ....utils.tensor import from_torch
+from ....utils.test import ring_params_8k_req_exact_devices, ring_params_req_exact_devices
+
+# Fixed VAE work units: encoder (17, 256, 256) tiles, decoder (7, 16, 16) latent chunks.
+TILE = 256
+CLIP_FRAMES = 17
+LATENT_TILE = 16
+DECODE_LATENT_FRAMES = 7
+
+
+def weights_subdir(subfolder: str) -> str | None:
+ base = os.environ.get("MINIMAX_H3_MODEL_PATH")
+ if not base:
+ return None
+ candidate = os.path.join(base, subfolder)
+ return candidate if os.path.isfile(os.path.join(candidate, "config.json")) else None
+
+
+def load_config(weights_dir: str) -> dict:
+ return {
+ k: v
+ for k, v in json.loads(open(os.path.join(weights_dir, "config.json")).read()).items()
+ if not k.startswith("_")
+ }
+
+
+def _reference_class(name: str):
+ pytest.importorskip("diffusers", reason="pinned diffusers reference not installed")
+ from diffusers.models.autoencoders import autoencoder_kl_minimax_h3 as ref
+
+ return getattr(ref, name)
+
+
+def random_encoder_state(config: dict) -> dict:
+ cls = _reference_class("MiniMaxH3VideoEncoder3d")
+ module = cls(
+ in_channels=3,
+ out_channels=2 * config["latent_channels"],
+ block_out_channels=tuple(config["block_out_channels"]),
+ layers_per_block=config["layers_per_block"],
+ spatial_downsample_factors=tuple(config["spatial_downsample_factors"]),
+ temporal_downsample_factors=tuple(config["temporal_downsample_factors"]),
+ norm_num_groups=config["norm_num_groups"],
+ norm_eps=config["norm_eps"],
+ spatial_padding_mode=config["spatial_padding_mode"],
+ )
+ return dict(module.state_dict())
+
+
+def random_decoder_state(config: dict, *, num_layers: int | None = None) -> dict:
+ cls = _reference_class("MiniMaxH3VideoViTDecoder3d")
+ module = cls(
+ in_channels=config["latent_channels"],
+ out_channels=config["out_channels"],
+ patch_size=16,
+ patch_size_t=4,
+ num_layers=config["decoder_num_layers"] if num_layers is None else num_layers,
+ num_attention_heads=config["decoder_num_attention_heads"],
+ attention_head_dim=config["decoder_attention_head_dim"],
+ num_register_tokens=config["decoder_num_register_tokens"],
+ ffn_mult=config["decoder_ffn_mult"],
+ rope_theta=config["decoder_rope_theta"],
+ rope_dim_ratio=config["decoder_rope_dim_ratio"],
+ norm_eps=config["decoder_norm_eps"],
+ )
+ return dict(module.state_dict())
+
+
+def psnr(reference: torch.Tensor, test: torch.Tensor) -> float:
+ """PSNR in dB, with the peak taken from the reference's own range."""
+ mse = torch.mean((reference.float() - test.float()) ** 2).item()
+ if mse == 0.0:
+ return float("inf")
+ peak = reference.abs().max().item()
+ return float("inf") if peak == 0.0 else 20.0 * math.log10(peak) - 10.0 * math.log10(mse)
+
+
+def create_fractal_image(width: int, height: int) -> Image.Image:
+ """Mandelbrot I2V seed: content the model would never generate, so a pipeline that ignores the keyframe cannot pass."""
+ c = np.linspace(-2.0, 1.0, width)[None, :] + 1j * np.linspace(-1.5, 1.5, height)[:, None]
+ z = np.zeros_like(c)
+ img = np.zeros(c.shape, dtype=np.uint8)
+ for i in range(32):
+ z = z * z + c
+ img[(img == 0) & (np.abs(z) > 2)] = 255 - 8 * i
+ return Image.fromarray(np.dstack((img, np.roll(img, width // 10, 1), np.roll(img, height // 10, 0))), "RGB")
+
+
+# The mesh shapes MiniMax-H3 is tuned for, named individually and composed into a list -- the shape
+# `ltx_mesh_params.py` uses. Both carry `require_exact_physical_num_devices`, so exactly one runs on
+# any given cluster and the other skips.
+#
+# The fabric must be FABRIC_1D_RING: `CCLManager` runs ring collectives, and on a line fabric one
+# cannot resolve a forwarding direction (`TT_FATAL fabric.cpp:174 forwarding_direction.has_value()`).
+# 4x32 additionally takes the 8 KB router payload, matching Wan's 4x32 rows, and a trace region for
+# the quad's `trace_denoise`; the region is only reserved, so 4x8 pays nothing but address space.
+_L1_SMALL = 65536
+_ring = {**ring_params_req_exact_devices, "l1_small_size": _L1_SMALL}
+_ring_8k = {**ring_params_8k_req_exact_devices, "l1_small_size": _L1_SMALL}
+_ring_8k_trace = {**ring_params_8k_req_exact_devices, "trace_region_size": 150_000_000, "l1_small_size": _L1_SMALL}
+
+MESH_4X8_RING = pytest.param((4, 8), _ring_8k, id="4x8")
+MESH_4X32_RING = pytest.param((4, 32), _ring_8k_trace, id="4x32")
+
+GALAXY_MESHES = [MESH_4X8_RING, MESH_4X32_RING]
+
+
+def randomize_norm_weights(module: torch.nn.Module, *, scale: float = 0.5) -> torch.nn.Module:
+ """Randomize every `nn.RMSNorm` affine in place, BEFORE taking `state_dict`: all-ones norms make norm-weight loading invisible to PCC."""
+ for submodule in module.modules():
+ if isinstance(submodule, torch.nn.RMSNorm) and submodule.weight is not None:
+ submodule.weight.data = 1.0 + scale * torch.randn_like(submodule.weight.data)
+ return module
+
+
+# ---- transformer test fixtures, shared by the correctness and perf tests so they cannot drift ----
+
+# The transformer tests take the axes explicitly: TP stays on axis 0 at factor 4 and SP absorbs the
+# rest, so 4x8 -> 4x32 moves only `sp_factor`, which every test body derives from `mesh_device.shape`.
+# `device_params` travels inside the tuple because the router payload differs per shape; crossing them
+# independently would pair a 4x8 mesh with the 4x32 router config.
+# The transformer tests take the axes explicitly: TP stays on axis 0 at factor 4 and SP absorbs the
+# rest, so 4x8 -> 4x32 moves only `sp_factor`, which every test body derives from `mesh_device.shape`.
+# `device_params` travels inside the tuple because the router payload differs per shape; crossing them
+# independently would pair a 4x8 mesh with the 4x32 router config.
+GALAXY_RING = pytest.mark.parametrize(
+ ("mesh_device", "sp_axis", "tp_axis", "num_links", "device_params", "topology", "is_fsdp"),
+ [
+ # 4x8 takes the 8 KB router payload like 4x32: sp_sim runs on it emulate the 4x32 machine,
+ # and the exp ring SDPA's fabric all-gather packs 4 tiles per packet only at 8 KB.
+ pytest.param((4, 8), 1, 0, 2, _ring_8k, ttnn.Topology.Ring, False, id="4x8sp1tp0nl2_ring_is_fsdp0"),
+ pytest.param((4, 32), 1, 0, 2, _ring_8k_trace, ttnn.Topology.Ring, False, id="4x32sp1tp0nl2_ring_is_fsdp0"),
+ ],
+ indirect=["mesh_device", "device_params"],
+)
+
+REAL_BLOCK_CONFIG = dict(
+ hidden_size=5376,
+ num_attention_heads=56,
+ attention_head_dim=128,
+ ffn_dim=14336,
+ time_embed_dim=2688,
+ norm_eps=1e-5,
+ qk_norm_eps=1e-5,
+)
+ROPE_FREQ_DIM = 16
+ROPE_THETA = 10000.0
+
+TT_BLOCK_CONFIG = dict(
+ hidden_size=REAL_BLOCK_CONFIG["hidden_size"],
+ num_heads=REAL_BLOCK_CONFIG["num_attention_heads"],
+ head_dim=REAL_BLOCK_CONFIG["attention_head_dim"],
+ ffn_dim=REAL_BLOCK_CONFIG["ffn_dim"],
+ time_embed_dim=REAL_BLOCK_CONFIG["time_embed_dim"],
+ norm_eps=REAL_BLOCK_CONFIG["norm_eps"],
+ qk_norm_eps=REAL_BLOCK_CONFIG["qk_norm_eps"],
+)
+
+# Reference token tags: 0 video, 1 text, 2 audio.
+TAG_VIDEO, TAG_TEXT, TAG_AUDIO = 0, 1, 2
+
+
+def packed_layout(
+ num_text: int,
+ num_audio: int,
+ num_video: int,
+ grid_hw: tuple[int, int] = (8, 8),
+ padded_len: int | None = None,
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
+ """Build one packed layout `(position_ids, token_tags, timestep_indices)`: text, audio, video rows over two timesteps, zero-padded to `padded_len`."""
+ grid_h, grid_w = grid_hw
+ frame = grid_h * grid_w
+ assert num_video % frame == 0, "num_video must fill whole (h, w) frames"
+ grid_t = num_video // frame
+ assert grid_t >= 2, "need at least one conditioning frame and one target frame"
+
+ tags = torch.cat(
+ [
+ torch.full((num_text,), TAG_TEXT, dtype=torch.long),
+ torch.full((num_audio,), TAG_AUDIO, dtype=torch.long),
+ torch.full((num_video,), TAG_VIDEO, dtype=torch.long),
+ ]
+ )
+ timestep_indices = torch.cat(
+ [
+ torch.zeros(num_text, dtype=torch.long),
+ torch.ones(num_audio, dtype=torch.long),
+ torch.zeros(frame, dtype=torch.long),
+ torch.ones(num_video - frame, dtype=torch.long),
+ ]
+ )
+
+ vt, vh, vw = torch.meshgrid(torch.arange(grid_t), torch.arange(grid_h), torch.arange(grid_w), indexing="ij")
+ video_pos = torch.stack([vt.reshape(-1), vh.reshape(-1), vw.reshape(-1)], dim=-1)
+
+ def clock_pos(n: int) -> torch.Tensor:
+ return torch.stack([torch.arange(n), torch.zeros(n, dtype=torch.long), torch.zeros(n, dtype=torch.long)], -1)
+
+ position_ids = torch.cat([clock_pos(num_text), clock_pos(num_audio), video_pos], dim=0)
+
+ if padded_len is not None:
+ pad = padded_len - position_ids.shape[0]
+ if pad:
+ position_ids = torch.cat([position_ids, torch.zeros((pad, 3), dtype=position_ids.dtype)])
+ tags = torch.cat([tags, torch.zeros(pad, dtype=tags.dtype)])
+ timestep_indices = torch.cat([timestep_indices, torch.zeros(pad, dtype=timestep_indices.dtype)])
+ return position_ids, tags, timestep_indices
+
+
+def upload_rope(rope_cos: torch.Tensor, rope_sin: torch.Tensor, *, mesh_device, sp_axis: int):
+ """Upload prepared `(cos, sin)` tables: fp32, fractured on SP along the sequence, replicated on TP."""
+
+ def _upload(table: torch.Tensor) -> ttnn.Tensor:
+ return from_torch(
+ table.reshape(1, 1, *table.shape),
+ device=mesh_device,
+ dtype=ttnn.float32,
+ mesh_axes=[..., sp_axis, None],
+ )
+
+ return _upload(rope_cos), _upload(rope_sin)
+
+
+def build_visual_encoder(config: dict, mesh_device, num_frames: int, **overrides):
+ from ....models.vae.minimax_h3.encoder_minimax_h3 import MiniMaxH3Encoder3d
+
+ kwargs = dict(
+ num_frames=num_frames,
+ height=TILE,
+ width=TILE,
+ in_channels=3,
+ out_channels=2 * config["latent_channels"],
+ block_out_channels=tuple(config["block_out_channels"]),
+ layers_per_block=config["layers_per_block"],
+ spatial_downsample_factors=tuple(config["spatial_downsample_factors"]),
+ temporal_downsample_factors=tuple(config["temporal_downsample_factors"]),
+ mesh_device=mesh_device,
+ )
+ kwargs.update(overrides)
+ return MiniMaxH3Encoder3d(**kwargs)
+
+
+def build_visual_decoder(config: dict, mesh_device, **overrides):
+ from ....models.vae.minimax_h3.decoder_minimax_h3 import MiniMaxH3ViTDecoder3d
+
+ kwargs = dict(
+ num_frames=DECODE_LATENT_FRAMES,
+ height=LATENT_TILE,
+ width=LATENT_TILE,
+ in_channels=config["latent_channels"],
+ out_channels=config["out_channels"],
+ patch_size=16,
+ patch_size_t=4,
+ num_layers=config["decoder_num_layers"],
+ num_heads=config["decoder_num_attention_heads"],
+ head_dim=config["decoder_attention_head_dim"],
+ num_register_tokens=config["decoder_num_register_tokens"],
+ ffn_mult=config["decoder_ffn_mult"],
+ rope_theta=config["decoder_rope_theta"],
+ rope_dim_ratio=config["decoder_rope_dim_ratio"],
+ eps=config["decoder_norm_eps"],
+ mesh_device=mesh_device,
+ )
+ kwargs.update(overrides)
+ return MiniMaxH3ViTDecoder3d(**kwargs)
+
+
+def build_audio_decoder(config: dict, mesh_device, **overrides):
+ from ....models.audio_vae.minimax_h3.decoder_minimax_h3_audio import MiniMaxH3AudioDecoder
+
+ kwargs = dict(
+ latent_channels=config["latent_channels"],
+ latent_dim=config["latent_dim"],
+ decoder_dim=config["decoder_dim"],
+ decoder_rates=tuple(config["decoder_rates"]),
+ decoder_kernel_sizes=tuple(config["decoder_kernel_sizes"]),
+ resblock_kernel_sizes=tuple(config["resblock_kernel_sizes"]),
+ resblock_dilation_sizes=tuple(tuple(d) for d in config["resblock_dilation_sizes"]),
+ mesh_device=mesh_device,
+ )
+ kwargs.update(overrides)
+ return MiniMaxH3AudioDecoder(**kwargs)
+
+
+CONDITIONER_SUBFOLDER = "text_encoder"
+
+
+def conditioner_checkpoint_dir(patterns: list[str]) -> str:
+ """Resolve the conditioner directory from $MINIMAX_H3_MODEL_PATH; a missing checkpoint skips.
+
+ `patterns` are the shard patterns the test needs present (validation only, nothing is fetched).
+ """
+ import glob
+
+ root = os.environ.get("MINIMAX_H3_MODEL_PATH", "")
+ if not root or not os.path.isdir(root):
+ pytest.skip("set MINIMAX_H3_MODEL_PATH to a MiniMax-H3 diffusers snapshot")
+ missing = [pattern for pattern in patterns if not glob.glob(os.path.join(root, pattern))]
+ if missing:
+ pytest.skip(f"MiniMax-H3 conditioner checkpoint at {root} is missing {missing}")
+ return os.path.join(root, CONDITIONER_SUBFOLDER)
+
+
+def load_reference_conditioner(path: str):
+ """Load the conditioner with load-info checked: a silently partial load would pass parity while testing the fresh init."""
+ import transformers
+
+ hf, info = transformers.Qwen3VLForConditionalGeneration.from_pretrained(
+ path, dtype=torch.bfloat16, output_loading_info=True
+ )
+ bad = {k: sorted(info[k])[:5] for k in ("missing_keys", "unexpected_keys", "mismatched_keys") if info[k]}
+ assert not bad, f"conditioner load key mismatch: {bad}"
+ return hf
diff --git a/code/models/tt_dit/tests/models/minimax_h3/common_av.py b/code/models/tt_dit/tests/models/minimax_h3/common_av.py
new file mode 100644
index 0000000000000000000000000000000000000000..320a5496047ec0a01bf7741bdf166d15f21d8e0a
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/common_av.py
@@ -0,0 +1,563 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Reference-free A/V sanity checks plus the scaffolding shared by the MiniMax-H3 e2e gates.
+A/V sync is checked structurally (duration/ordering); envelope-vs-motion correlation is diagnostic only."""
+
+from __future__ import annotations
+
+import os
+import shutil
+import subprocess
+from pathlib import Path
+
+import numpy as np
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+
+from ....pipelines.minimax_h3.packing import MINIMAX_H3_FPS
+
+
+def check_audio_sanity(audio, *, sampling_rate, expected_seconds, tolerance_seconds=0.05):
+ """Guard against a soundtrack that is silent, clipped, constant, or the wrong length."""
+ if isinstance(audio, torch.Tensor):
+ audio = audio.detach().cpu().numpy()
+ audio = np.asarray(audio)
+ if audio.ndim == 3:
+ assert audio.shape[0] == 1, f"expected a single generation, got batch {audio.shape[0]}"
+ audio = audio[0]
+ assert audio.ndim == 2, f"expected (channels, samples), got shape {audio.shape}"
+
+ channels, samples = audio.shape
+ assert channels == 2, f"H3 generates stereo; got {channels} channel(s)"
+ assert np.isfinite(audio).all(), "soundtrack contains NaN/Inf"
+
+ seconds = samples / sampling_rate
+ assert abs(seconds - expected_seconds) <= tolerance_seconds, (
+ f"soundtrack is {seconds:.3f} s against {expected_seconds:.3f} s of video "
+ f"(off by {seconds - expected_seconds:+.3f} s, tolerance {tolerance_seconds:.3f} s)"
+ )
+
+ peak = float(np.abs(audio).max())
+ rms = float(np.sqrt((audio.astype(np.float64) ** 2).mean()))
+ assert peak > 1e-3, f"soundtrack is silent (peak {peak:.2e})"
+ assert rms > 1e-4, f"soundtrack is near-silent (rms {rms:.2e})"
+ for index in range(channels):
+ channel_std = float(audio[index].std())
+ assert channel_std > 1e-4, f"channel {index} is constant (std {channel_std:.2e})"
+ # Widespread clipping means the denormalization is wrong, not that the mix is loud.
+ clipped = float((np.abs(audio) >= 0.999).mean())
+ assert clipped < 0.01, f"{clipped:.1%} of samples are at full scale; suspect a scaling error"
+
+ logger.info(
+ f"Audio sanity OK: {channels}ch {seconds:.3f} s @ {sampling_rate} Hz, "
+ f"peak={peak:.3f}, rms={rms:.4f}, clipped={clipped:.3%}"
+ )
+
+
+def check_av_sync(frames, audio, *, sampling_rate, fps, tolerance_seconds=0.05):
+ """Durations must agree and stereo channels must differ -- identical channels mean
+ `unpack_audio_tokens` collapsed the channel-major layout. Envelope-motion lag is logged only."""
+ if isinstance(frames, torch.Tensor):
+ frames = frames.detach().cpu().numpy()
+ if isinstance(audio, torch.Tensor):
+ audio = audio.detach().cpu().numpy()
+ frames = np.asarray(frames)
+ audio = np.asarray(audio)
+ if audio.ndim == 3:
+ audio = audio[0]
+
+ num_frames = frames.shape[0]
+ video_seconds = num_frames / fps
+ audio_seconds = audio.shape[-1] / sampling_rate
+ assert num_frames > 1, f"need more than one frame to talk about sync, got {num_frames}"
+ assert audio.shape[-1] > sampling_rate // fps, "soundtrack is shorter than a single frame"
+ assert abs(video_seconds - audio_seconds) <= tolerance_seconds, (
+ f"video is {video_seconds:.3f} s but audio is {audio_seconds:.3f} s "
+ f"(off by {audio_seconds - video_seconds:+.3f} s)"
+ )
+
+ if audio.shape[0] == 2:
+ assert not np.allclose(audio[0], audio[1]), "stereo channels are identical; suspect audio row unpacking"
+
+ # Diagnostic only.
+ motion = np.abs(np.diff(frames.astype(np.float32), axis=0)).mean(axis=(1, 2, 3))
+ samples_per_frame = audio.shape[-1] / num_frames
+ envelope = np.array(
+ [
+ np.abs(audio[:, int(i * samples_per_frame) : int((i + 1) * samples_per_frame)]).mean()
+ for i in range(1, num_frames)
+ ]
+ )
+ lag_frames = 0
+ if motion.std() > 1e-8 and envelope.std() > 1e-8:
+ m = (motion - motion.mean()) / motion.std()
+ e = (envelope - envelope.mean()) / envelope.std()
+ correlation = np.correlate(m, e, mode="full") / len(m)
+ lag_frames = int(np.argmax(correlation)) - (len(m) - 1)
+ logger.info(
+ f"A/V envelope-motion best lag: {lag_frames:+d} frames ({lag_frames / fps:+.3f} s), "
+ f"peak r={correlation.max():.3f} (diagnostic, not asserted)"
+ )
+
+ logger.info(
+ f"A/V sync OK: video {video_seconds:.3f} s / {num_frames} frames @ {fps} fps, "
+ f"audio {audio_seconds:.3f} s @ {sampling_rate} Hz, delta {audio_seconds - video_seconds:+.4f} s"
+ )
+ return {"video_seconds": video_seconds, "audio_seconds": audio_seconds, "lag_frames": lag_frames}
+
+
+def check_spatial_seams(frames, *, vertical_boundaries, horizontal_boundaries, max_ratio=2.0):
+ """Gradient energy at the VAE's tile boundaries vs everywhere else (~1.0 = no seam; whole-frame means hide seams)."""
+ if isinstance(frames, torch.Tensor):
+ frames = frames.detach().cpu().numpy()
+ frames = np.asarray(frames).astype(np.float32)
+ if frames.ndim == 4:
+ frames = frames.mean(axis=-1)
+
+ def ratio(gradient, boundaries):
+ inside = np.array([b for b in boundaries if 1 <= b < len(gradient) - 1], dtype=int)
+ if not len(inside):
+ return float("nan")
+ # Exclude +-2 px around each boundary so a smeared seam cannot inflate its own baseline.
+ baseline = np.ones(len(gradient), dtype=bool)
+ for b in inside:
+ baseline[max(0, b - 2) : b + 3] = False
+ if not baseline.any() or gradient[baseline].mean() == 0:
+ return float("nan")
+ return float(gradient[inside].mean() / gradient[baseline].mean())
+
+ column_gradient = np.abs(np.diff(frames, axis=2)).mean(axis=(0, 1))
+ row_gradient = np.abs(np.diff(frames, axis=1)).mean(axis=(0, 2))
+ vertical = ratio(column_gradient, vertical_boundaries)
+ horizontal = ratio(row_gradient, horizontal_boundaries)
+
+ logger.info(
+ f"Spatial seam ratios (1.0 = no seam): vertical {vertical:.3f} at x={list(vertical_boundaries)}, "
+ f"horizontal {horizontal:.3f} at y={list(horizontal_boundaries)}"
+ )
+ for name, value in (("vertical", vertical), ("horizontal", horizontal)):
+ if np.isfinite(value):
+ assert value < max_ratio, (
+ f"{name} tile-boundary gradient is {value:.2f}x the surrounding image; "
+ "suspect the tile blend extent or per-tile normalization (artifact rubric: seams)"
+ )
+ return {"vertical": vertical, "horizontal": horizontal}
+
+
+def log_spectral_flatness(audio, *, sampling_rate, num_bands=64):
+ """Log-spectrum shape, logged only: white noise drives flatness toward 1.0, a stuck tone toward 0."""
+ if isinstance(audio, torch.Tensor):
+ audio = audio.detach().cpu().numpy()
+ audio = np.asarray(audio)
+ if audio.ndim == 3:
+ audio = audio[0]
+ mono = audio.mean(axis=0).astype(np.float64)
+
+ window = 2048
+ hop = window // 2
+ frames = [mono[i : i + window] for i in range(0, max(1, len(mono) - window), hop)]
+ if not frames:
+ return {"flatness": float("nan")}
+ spectrum = np.abs(np.fft.rfft(np.stack(frames) * np.hanning(window), axis=-1)) ** 2
+ power = spectrum.mean(axis=0)[1:]
+ flatness = float(np.exp(np.log(power + 1e-20).mean()) / (power.mean() + 1e-20))
+ band_edges = np.linspace(0, len(power), num_bands + 1).astype(int)
+ bands = np.array([power[a:b].mean() for a, b in zip(band_edges[:-1], band_edges[1:]) if b > a])
+ logger.info(
+ f"Audio log-spectrum: flatness={flatness:.4f}, "
+ f"band dB range=[{10 * np.log10(bands.min() + 1e-20):.1f}, {10 * np.log10(bands.max() + 1e-20):.1f}]"
+ )
+ return {"flatness": flatness}
+
+
+def _ffmpeg():
+ exe = shutil.which("ffmpeg")
+ if exe:
+ return exe
+ try:
+ import imageio_ffmpeg
+ except ImportError:
+ return None
+ return imageio_ffmpeg.get_ffmpeg_exe()
+
+
+def write_artifacts(frames, audio, sampling_rate, directory: Path, stem: str = "t2va"):
+ import wave
+
+ paths = {}
+ wav_path = directory / f"{stem}.wav"
+ interleaved = np.ascontiguousarray(audio[0].T if audio.ndim == 3 else audio.T)
+ pcm = np.clip(interleaved, -1.0, 1.0)
+ with wave.open(str(wav_path), "wb") as handle:
+ handle.setnchannels(pcm.shape[1])
+ handle.setsampwidth(2)
+ handle.setframerate(sampling_rate)
+ handle.writeframes((pcm * 32767.0).astype(" dict:
+ exe = shutil.which("ffprobe")
+ if exe is None:
+ return {}
+ import json as _json
+
+ result = subprocess.run(
+ [
+ exe,
+ "-v",
+ "error",
+ "-show_entries",
+ "stream=codec_type,duration,nb_frames,sample_rate,channels,width,height",
+ "-of",
+ "json",
+ str(path),
+ ],
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+ streams = {}
+ for stream in _json.loads(result.stdout).get("streams", []):
+ streams[stream.get("codec_type", "?")] = stream
+ return streams
+
+
+# Only reached when ffprobe is unavailable.
+_FALLBACK_HEIGHT = 768
+_FALLBACK_WIDTH = 1344
+
+
+def decoded_frames(path: Path, count: int, height: int = 0, width: int = 0) -> np.ndarray:
+ """Sample `count` frames evenly out of the written file, as uint8 luma.
+
+ `height`/`width` are the caller's known canvas. They matter: the frame count is derived by
+ dividing the raw byte stream by the frame size, and the only other source for that size is
+ `probe_streams`, which needs `ffprobe` -- absent wherever ffmpeg comes from imageio_ffmpeg.
+ Without them this fell back to a hardcoded 768x1344, which silently miscounts every canvas
+ with a different pixel total (4:3 read 94 frames for a correct 124-frame file). 16:9 and 21:9
+ both happen to be 1032192 px, so the constant looked fine until a sweep left that budget.
+ """
+ exe = _ffmpeg()
+ if exe is None:
+ return np.empty((0,))
+ result = subprocess.run(
+ [
+ exe,
+ "-v",
+ "error",
+ # Count what the file STORES. Muxing AAC writes an mp4 edit list (the codec's priming
+ # shifts the timeline), and a decoder honouring it drops frames at the edges: a 362-frame
+ # write reads back as 362 stored / 361 in-container / 359 after the edit list. The gate
+ # asks "were the frames written", so the edit list must not be applied here.
+ "-ignore_editlist",
+ "1",
+ "-i",
+ str(path),
+ # `select` with count<=1 is a no-op (`not(mod(n,1))` is always true) but combined with
+ # `-vsync 0` on an edit-list-shifted timeline it silently drops frames at the edges --
+ # 2 of them on a muxed 15 s clip. Only ask for the filter when actually subsampling.
+ *(["-vf", f"select='not(mod(n\\,{count}))'", "-vsync", "0"] if count > 1 else []),
+ "-f",
+ "rawvideo",
+ "-pix_fmt",
+ "gray",
+ "-",
+ ],
+ check=True,
+ capture_output=True,
+ )
+ probe = probe_streams(path).get("video", {})
+ height = int(probe.get("height") or height or _FALLBACK_HEIGHT)
+ width = int(probe.get("width") or width or _FALLBACK_WIDTH)
+ buffer = np.frombuffer(result.stdout, dtype=np.uint8)
+ usable = (buffer.size // (height * width)) * height * width
+ return buffer[:usable].reshape(-1, height, width)
+
+
+VBENCH_PYTHON = Path.home() / "vbench_env" / "bin" / "python"
+
+
+def run_vbench(video: Path, prompt: str, dimensions) -> dict[str, float]:
+ """Run VBench in its own interpreter (~/vbench_env): it pins numpy < 2 / transformers 4.33."""
+ if not VBENCH_PYTHON.is_file():
+ pytest.skip(
+ f"no VBench interpreter at {VBENCH_PYTHON}; create it with "
+ "`uv venv --python 3.10 ~/vbench_env && uv pip install --python ~/vbench_env/bin/python "
+ "vbench decord 'numpy==1.26.4' 'opencv-python-headless<4.11' 'setuptools<81'`"
+ )
+ interpreter = str(VBENCH_PYTHON)
+ runner = Path(__file__).with_name("tools") / "vbench_runner.py"
+ result = subprocess.run(
+ [interpreter, str(runner), str(video), ",".join(dimensions), "--prompt", prompt],
+ capture_output=True,
+ text=True,
+ timeout=5400,
+ )
+ marker = "VBENCH_JSON "
+ line = next((l for l in result.stdout.splitlines() if l.startswith(marker)), None)
+ if line is None:
+ raise AssertionError(
+ f"VBench produced no scores (exit {result.returncode}).\n"
+ f"stdout tail:\n{result.stdout[-2000:]}\nstderr tail:\n{result.stderr[-2000:]}"
+ )
+ import json as _json
+
+ return _json.loads(line[len(marker) :])
+
+
+def clip_prompt_alignment(frames: np.ndarray, prompt: str, num_frames: int = 8) -> dict[str, float]:
+ """Mean CLIP similarity between the prompt and evenly-spaced frames, x100."""
+ from PIL import Image
+
+ from ...dataset_eval.clip_encoder import CLIPEncoder
+
+ indices = np.linspace(0, frames.shape[0] - 1, num_frames).astype(int)
+ encoder = CLIPEncoder()
+ scores = [encoder.get_clip_score(prompt, Image.fromarray(frames[i])).item() * 100.0 for i in indices]
+ return {"mean": float(np.mean(scores)), "min": float(min(scores)), "max": float(max(scores))}
+
+
+def temporal_seam_score(frames: np.ndarray, period: int) -> float:
+ """Inter-frame delta at temporal chunk boundaries vs elsewhere (~1.0 = no seam)."""
+ deltas = np.abs(np.diff(frames.astype(np.float32), axis=0)).mean(axis=(1, 2))
+ if len(deltas) < 2 * period:
+ return float("nan")
+ index = np.arange(1, len(deltas) + 1)
+ at_boundary = deltas[index % period == 0]
+ elsewhere = deltas[index % period != 0]
+ if not len(at_boundary) or not elsewhere.mean():
+ return float("nan")
+ return float(at_boundary.mean() / elsewhere.mean())
+
+
+# ------------------------------------------------------------------ shared e2e gate scaffolding
+
+# Matched pair with the tier-6 bars (CLIP 37.37, imaging_quality 0.6896); imported by fl2va so it cannot drift.
+CALIBRATED_FOX_PROMPT = (
+ "A red fox trots across a snowy field at dawn, its breath visible in the cold air. "
+ "The low sun throws long blue shadows behind it, and loose snow lifts from each footfall."
+)
+
+
+def weights_dir(*required_subdirs: str) -> Path:
+ """The snapshot dir from MINIMAX_H3_MODEL_PATH; skips when it or a required partition is missing."""
+ root = os.environ.get("MINIMAX_H3_MODEL_PATH", "")
+ if not root or not Path(root).is_dir():
+ pytest.skip("set MINIMAX_H3_MODEL_PATH to a MiniMax-H3 diffusers snapshot")
+ directory = Path(root)
+ missing = [name for name in required_subdirs if not (directory / name).is_dir()]
+ if missing:
+ pytest.skip(f"MiniMax-H3 snapshot at {directory} is missing {missing}")
+ return directory
+
+
+def artifact_dir(name: str) -> Path:
+ """The gate's artifact directory `~/{name}`, created if absent."""
+ directory = Path.home() / name
+ directory.mkdir(parents=True, exist_ok=True)
+ return directory
+
+
+def run_warm_generation(pipeline, prompt: str, *, seed: int, **gen_kwargs):
+ """Warmup then the timed generation with identical kwargs; asserts padded-length agreement (programs are keyed on it)."""
+ pipeline.warmup(prompt=prompt, **gen_kwargs)
+ warm_padded_len = pipeline.last_padded_len
+
+ # The warmup is a full generation too, so its stage timings are the *cold* numbers -- program
+ # compilation, per-shape conv3d blocking and buffer allocation all land in them. Log them
+ # before the measured call overwrites `last_timings`. Not gated: cold cost depends on what the
+ # weight/JIT caches already held, so it is a diagnostic, not a bar.
+ cold_rows = list(pipeline.last_timings)
+ if cold_rows:
+ cold_total = sum(seconds for _, seconds in cold_rows)
+ logger.info("WARMUP (cold: program compile + buffer alloc included, not a perf target)")
+ for row_label, seconds in cold_rows:
+ share = 100 * seconds / cold_total if cold_total else 0.0
+ logger.info(f" {row_label:<18} {seconds:8.1f} s ({share:4.1f} %)")
+ logger.info(f" {'Total (warmup)':<18} {cold_total:8.1f} s")
+
+ ttnn.synchronize_device(pipeline.mesh_device)
+ if ttnn.using_distributed_env():
+ ttnn.distributed_context_barrier()
+
+ output = pipeline(prompt, seed=seed, **gen_kwargs)
+
+ assert pipeline.last_padded_len == warm_padded_len, (
+ f"warmup ran at padded_len {warm_padded_len} but the measured call ran at "
+ f"{pipeline.last_padded_len}; this number is not warm"
+ )
+ return output
+
+
+def log_timing_table(pipeline, label: str, num_forwards: int, video_seconds: float, expected_total_s=None, extra=""):
+ """The MEASUREMENT block; `expected_total_s`, when given, asserts the total. Returns the total."""
+ rows = pipeline.last_timings
+ total = sum(seconds for _, seconds in rows)
+ shape = tuple(pipeline.mesh_device.shape)
+ logger.info(
+ f"MEASUREMENT {label} fully warm | mesh {shape[0]}x{shape[1]} Blackhole, "
+ f"TP={pipeline.tp_factor} axis {pipeline.tp_axis} / SP={pipeline.sp_factor} axis {pipeline.sp_axis}, "
+ f"{pipeline.ccl_manager.topology}, {pipeline.ccl_manager.num_links} links{extra} "
+ f"| warm window: one full warmup generation at this shape, prepares and export excluded"
+ )
+ for row_label, seconds in rows:
+ logger.info(f" {row_label:<18} {seconds:8.1f} s ({100 * seconds / total:4.1f} %)")
+ logger.info(f" {'Total (compute)':<18} {total:8.1f} s")
+ denoise = dict(rows).get("Denoise")
+ if denoise:
+ logger.info(
+ f" per forward {denoise / num_forwards * 1000:8.1f} ms "
+ f"({num_forwards} forwards over {denoise:.1f} s)"
+ )
+ logger.info(f" realtime factor {total / video_seconds:8.1f} x (compute / video seconds)")
+ if expected_total_s is not None:
+ assert (
+ total < expected_total_s
+ ), f"fully-warm total {total:.1f} s exceeds the {expected_total_s:.0f} s floor bar"
+ return total
+
+
+def to_uint8_frames(output) -> np.ndarray:
+ """`(1, 3, F, H, W)` [0, 1] -> `(F, H, W, 3)` uint8; clamp + round first -- a bare astype wraps out-of-range values."""
+ video = output.video
+ assert video.ndim == 5 and video.shape[0] == 1, f"unexpected video shape {tuple(video.shape)}"
+ frames = video[0].permute(1, 2, 3, 0).clamp(0, 1).mul(255).round().to(torch.uint8)
+ return frames.cpu().numpy()
+
+
+def check_written_file(paths: dict, expected_frames: int, seam_period: int = 17, height: int = 0, width: int = 0):
+ """Gate the written *file* (streams, A/V skew, frame count, temporal seam); silently no-ops without an mp4."""
+ if "mp4" not in paths:
+ return
+ streams = probe_streams(paths["mp4"])
+ if streams:
+ logger.info(f"container streams: { {k: v.get('duration') for k, v in streams.items()} }")
+ assert "video" in streams and "audio" in streams, f"muxed file is missing a stream: {list(streams)}"
+ durations = {k: float(v["duration"]) for k, v in streams.items() if v.get("duration")}
+ if {"video", "audio"} <= set(durations):
+ skew = durations["audio"] - durations["video"]
+ # AAC pads to a frame boundary, so allow a little more than the tensor-level check.
+ assert abs(skew) < 0.15, f"muxed A/V skew {skew:+.3f} s"
+ logger.info(f"muxed A/V skew: {skew:+.4f} s")
+
+ decoded = decoded_frames(paths["mp4"], count=1, height=height, width=width)
+ if decoded.size:
+ assert (
+ decoded.shape[0] >= expected_frames - 1
+ ), f"the written mp4 decodes to {decoded.shape[0]} frames, expected ~{expected_frames}"
+ # The VAE's temporal chunk covers clip_length (17) pixel frames.
+ seam = temporal_seam_score(decoded, period=seam_period)
+ logger.info(f"temporal seam score at the {seam_period}-frame chunk period: {seam:.3f} (1.0 = no seam)")
+ if np.isfinite(seam):
+ assert seam < 3.0, (
+ f"inter-frame delta at chunk boundaries is {seam:.2f}x the delta elsewhere; "
+ "suspect temporal stitching (see the artifact rubric)"
+ )
+
+
+def gate_clip(frames: np.ndarray, prompt: str, threshold: float, label: str):
+ """CLIP prompt-alignment gate; skips only if `open_clip` is missing."""
+ pytest.importorskip("open_clip", reason="the CLIP gate needs open_clip, which is not installed")
+ alignment = clip_prompt_alignment(frames, prompt)
+ logger.info(
+ f"{label} CLIP prompt alignment: mean={alignment['mean']:.2f} "
+ f"min={alignment['min']:.2f} max={alignment['max']:.2f} (bar {threshold})"
+ )
+ assert alignment["mean"] >= threshold, (
+ f"{label} CLIP prompt alignment {alignment['mean']:.2f} is below the {threshold} bar; "
+ "the video does not match the prompt"
+ )
+ return alignment
+
+
+def gate_vbench(paths: dict, prompt: str, thresholds: dict, label: str, skip_without_mp4=False):
+ """VBench gate (skips only when its interpreter is missing); a requested dimension with no returned score FAILS, never silently passes."""
+ if "mp4" not in paths:
+ message = "the VBench gate needs the muxed mp4, which ffmpeg did not produce"
+ if skip_without_mp4:
+ pytest.skip(message)
+ raise AssertionError(message)
+ scores = run_vbench(paths["mp4"], prompt, tuple(thresholds))
+ for dimension, bar in thresholds.items():
+ assert dimension in scores, f"VBench returned no score for {dimension}"
+ logger.info(f"{label} VBench {dimension} = {scores[dimension]:.4f} (bar {bar})")
+ failures = [f"{d} = {scores[d]:.4f} < {bar:.4f}" for d, bar in thresholds.items() if scores[d] < bar]
+ assert not failures, "VBench below threshold: " + "; ".join(failures)
+ return scores
diff --git a/code/models/tt_dit/tests/models/minimax_h3/cpu_vs_device.py b/code/models/tt_dit/tests/models/minimax_h3/cpu_vs_device.py
new file mode 100644
index 0000000000000000000000000000000000000000..9dcc0b606e4adc5288787722d3477e5300a757a3
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/cpu_vs_device.py
@@ -0,0 +1,286 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""CPU reference vs device decode, fp32, scored on real clips.
+
+Per clip: resample to 32 kHz, take one production window (207 latents = 5.17 s), encode with the
+torch/diffusers reference, decode on CPU (ground truth) and on device, score device against CPU. WAVs
+are written so the difference can be heard. Batch defaults to 2 for comparability with numbers quoted
+before `CVD_BATCH` existed; the real per-clip cost is `CVD_BATCH=1`, since latency scales ~linearly
+with batch and one request is one clip. The decoder is built once, so times are steady-state.
+
+Usage -- single device, shipping defaults:
+
+ export MINIMAX_H3_MODEL_PATH=/path/to/MiniMax-H3-diffusers
+ python models/tt_dit/tests/models/minimax_h3/cpu_vs_device.py
+
+8-way T-shard on a 4x8 mesh with trace, all-fast levers (the sub-300 ms configuration):
+
+ env CVD_MESH=4x8 CVD_T_FACTOR=8 CVD_MESH_AXIS=1 CVD_TRACED=1 \
+ CVD_SPLIT_MODE=off python .../cpu_vs_device.py
+
+Env: CVD_MESH (default 1x1), CVD_T_FACTOR, CVD_MESH_AXIS, CVD_TRACED, CVD_BATCH (default 2, 1 for the
+real per-clip cost), CVD_SPLIT_MODE (off|weight|full),
+CVD_MAX_C_IN_BLOCK, CVD_OUT_DIR, CVD_BASELINE_PSNR.
+
+Accuracy is scored against the CPU reference, i.e. an absolute number; the T-parallel test only scores
+sharded against unsharded, which is a looser bar. Unset levers keep the constructor default (accurate
+mode), so `=0` is not the same as omitting. Read each run's `levers:` line rather than the filename.
+"""
+
+import json
+import os
+import time
+
+import librosa
+import numpy as np
+import soundfile as sf
+import torch
+
+import ttnn
+
+SR = 32000
+HOP = 800
+NUM_LATENT_FRAMES = 207
+# Under `generated/` rather than any developer's filesystem, so this runs on CI and a fresh clone.
+OUT_DIR = os.environ.get("CVD_OUT_DIR") or os.path.join(
+ os.environ.get("TT_METAL_HOME", os.getcwd()), "generated", "minimax_h3_audio", "clips"
+)
+# `MINIMAX_H3_MODEL_PATH` matches the test suite; the older var is still accepted.
+WEIGHTS = os.environ.get("MINIMAX_H3_MODEL_PATH") or os.environ.get("MINIMAX_H3_DIFFUSERS_DIR", "")
+if not WEIGHTS:
+ raise SystemExit("set MINIMAX_H3_MODEL_PATH to a MiniMax-H3 diffusers snapshot")
+
+# T_FACTOR must equal the length of the axis it shards: on a 4x8 mesh only (4, axis 0) and
+# (8, axis 1) work, anything else dies in `_partition_t` on a non-tile-aligned slice.
+MESH = tuple(int(v) for v in os.environ.get("CVD_MESH", "1x1").split("x"))
+T_FACTOR = int(os.environ.get("CVD_T_FACTOR", "1"))
+MESH_AXIS = int(os.environ.get("CVD_MESH_AXIS", "1"))
+TRACED = os.environ.get("CVD_TRACED", "0") == "1"
+# Default 2 keeps existing invocations/numbers unchanged; CVD_BATCH=1 is the honest per-clip cost.
+BATCH = int(os.environ.get("CVD_BATCH", "2"))
+
+
+# The precision levers select a different operator set, not just a different speed. Quoting one
+# row's latency with another's PSNR is what produced the retired "281.6 ms at 49.45 dB" claim.
+# `None` means "leave the constructor default alone".
+def _flag(name):
+ raw = os.environ.get(name)
+ return None if raw is None else raw not in ("0", "false", "False", "")
+
+
+SPLIT_MODE = os.environ.get("CVD_SPLIT_MODE")
+# conv3d's C_in_block cap. Moves accuracy on its own -- conv_pre's error falls as the block grows
+# (2.40e-03 at 32, 1.86e-03 at 128) -- but 256 buys 0.02 dB and 512 exceeds L1.
+MAX_C_IN_BLOCK = os.environ.get("CVD_MAX_C_IN_BLOCK")
+LEVERS = {
+ k: v
+ for k, v in (
+ ("split_mode", SPLIT_MODE),
+ ("max_c_in_block", int(MAX_C_IN_BLOCK) if MAX_C_IN_BLOCK else None),
+ )
+ if v is not None
+}
+
+IS_DEFAULT = MESH == (1, 1) and T_FACTOR == 1 and not TRACED and not LEVERS
+# Non-default runs get their config in the filename. Overwriting `{label}_2_device.wav` with a
+# differently-configured decode is how the stale `*_3_device_prefix.wav` confusion started.
+_lever_tag = "".join(f"_{k}-{v}" for k, v in (("sm", SPLIT_MODE), ("cin", MAX_C_IN_BLOCK)) if v is not None)
+TAG = "" if IS_DEFAULT else f"_{MESH[0]}x{MESH[1]}_f{T_FACTOR}ax{MESH_AXIS}{'_traced' if TRACED else ''}{_lever_tag}"
+
+# (label, librosa example key, seconds to skip -- past leading silence / into a busy passage)
+CLIPS = [
+ ("voice_libri1", "libri1", 0.5),
+ ("voice_libri2", "libri2", 0.5),
+ ("music_trumpet", "trumpet", 0.5),
+ ("music_brahms", "brahms", 8.0),
+]
+
+
+def load_clip(key: str, offset: float, num_samples: int, batch: int = BATCH) -> torch.Tensor:
+ """-> (batch, 1, num_samples) at SR, peak-normalised, mono duplicated across the batch slots."""
+ y, _ = librosa.load(librosa.ex(key), sr=SR, mono=True, offset=offset, duration=num_samples / SR + 1.0)
+ if len(y) < num_samples:
+ y = np.pad(y, (0, num_samples - len(y)))
+ y = y[:num_samples]
+ peak = float(np.abs(y).max()) or 1.0
+ mono = torch.from_numpy(0.85 * y / peak).float()
+ return mono.unsqueeze(0).unsqueeze(0).repeat(batch, 1, 1)
+
+
+def write_wav(path: str, wav: torch.Tensor) -> None:
+ data = wav.detach().float()[0, 0].numpy() # both batch slots are identical; write one
+ sf.write(path, np.clip(data, -1.0, 1.0), SR, subtype="FLOAT")
+
+
+def psnr(ref: torch.Tensor, test: torch.Tensor) -> float:
+ mse = torch.mean((ref.float() - test.float()) ** 2).item()
+ if mse == 0.0:
+ return float("inf")
+ return 20.0 * np.log10(ref.abs().max().item()) - 10.0 * np.log10(mse)
+
+
+def rel_rmse(ref: torch.Tensor, test: torch.Tensor) -> float:
+ return float((test.double() - ref.double()).pow(2).mean().sqrt() / ref.double().std())
+
+
+def log_spec_distance(a: torch.Tensor, b: torch.Tensor, n_fft: int = 1024, hop: int = 256) -> float:
+ window = torch.hann_window(n_fft)
+ spec = []
+ for sig in (a, b):
+ flat = sig.reshape(-1, sig.shape[-1]).float()
+ spec.append(
+ torch.log(
+ torch.stft(flat, n_fft=n_fft, hop_length=hop, window=window, return_complex=True).abs().clamp_min(1e-5)
+ )
+ )
+ return (spec[0] - spec[1]).abs().mean().item()
+
+
+def main():
+ os.makedirs(OUT_DIR, exist_ok=True)
+ from diffusers import AutoencoderKLMiniMaxH3Audio
+ from safetensors.torch import load_file
+
+ from models.tt_dit.models.audio_vae.minimax_h3.convert_minimax_h3_audio import convert_minimax_h3_audio_state_dict
+ from models.tt_dit.models.audio_vae.minimax_h3.decoder_minimax_h3_audio import MiniMaxH3AudioDecoder
+
+ audio_dir = os.path.join(WEIGHTS, "audio_vae")
+ with open(os.path.join(audio_dir, "config.json")) as fh:
+ config = {k: v for k, v in json.load(fh).items() if not k.startswith("_")}
+ reference = AutoencoderKLMiniMaxH3Audio(**config).eval()
+ reference.load_state_dict(load_file(os.path.join(audio_dir, "diffusion_pytorch_model.safetensors")))
+
+ num_samples = NUM_LATENT_FRAMES * HOP
+ sharded = T_FACTOR > 1
+ if MESH != (1, 1):
+ # open_mesh_device takes no fabric_config; conftest sets it separately, before opening.
+ ttnn.set_fabric_config(ttnn.FabricConfig.FABRIC_1D)
+ device = ttnn.open_mesh_device(
+ ttnn.MeshShape(*MESH),
+ l1_small_size=65536,
+ **({"trace_region_size": 450_000_000} if TRACED else {}),
+ )
+ rows = []
+ try:
+ print(
+ f"config: mesh {MESH[0]}x{MESH[1]} ({device.get_num_devices()} chips), t_factor={T_FACTOR} "
+ f"axis={MESH_AXIS}, traced={TRACED}",
+ flush=True,
+ )
+ parallel_config = None
+ ccl_manager = None
+ if sharded:
+ from models.tt_dit.parallel.config import ParallelFactor
+ from models.tt_dit.parallel.manager import CCLManager
+
+ parallel_config = ParallelFactor(factor=T_FACTOR, mesh_axis=MESH_AXIS)
+ ccl_manager = CCLManager(device, num_links=1, topology=ttnn.Topology.Linear)
+
+ if LEVERS:
+ print(f"levers: {LEVERS} (unset ones keep the constructor default)", flush=True)
+ decoder = MiniMaxH3AudioDecoder(
+ latent_channels=config["latent_channels"],
+ latent_dim=config["latent_dim"],
+ decoder_dim=config["decoder_dim"],
+ decoder_rates=tuple(config["decoder_rates"]),
+ decoder_kernel_sizes=tuple(config["decoder_kernel_sizes"]),
+ resblock_kernel_sizes=tuple(config["resblock_kernel_sizes"]),
+ resblock_dilation_sizes=tuple(tuple(d) for d in config["resblock_dilation_sizes"]),
+ mesh_device=device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ **LEVERS,
+ )
+ decoder.load_torch_state_dict(convert_minimax_h3_audio_state_dict(dict(reference.state_dict())), strict=False)
+
+ for label, key, offset in CLIPS:
+ src = load_clip(key, offset, num_samples)
+ write_wav(os.path.join(OUT_DIR, f"{label}_0_source.wav"), src)
+
+ with torch.no_grad():
+ latents = reference.encode(src).latent_dist.mode()[..., :NUM_LATENT_FRAMES]
+ t0 = time.perf_counter()
+ cpu_out = reference.decode(latents).sample
+ cpu_secs = time.perf_counter() - t0
+ write_wav(os.path.join(OUT_DIR, f"{label}_1_cpu.wav"), cpu_out)
+
+ # Warm with the same `traced` flag the timed runs use, so the trace is captured outside the
+ # measurement rather than inside the first one.
+ decoder(latents, traced=TRACED)
+ runs = []
+ for _ in range(3):
+ t0 = time.perf_counter()
+ dev_out = decoder(latents, traced=TRACED)
+ runs.append(time.perf_counter() - t0)
+ dev_secs = min(runs)
+ write_wav(os.path.join(OUT_DIR, f"{label}_2_device{TAG}.wav"), dev_out)
+
+ rows.append(
+ (
+ label,
+ cpu_secs,
+ dev_secs,
+ psnr(cpu_out, dev_out),
+ rel_rmse(cpu_out, dev_out),
+ log_spec_distance(cpu_out, dev_out),
+ )
+ )
+ print(f"done {label}: cpu {cpu_secs:.3f}s device {dev_secs:.3f}s psnr {rows[-1][3]:.2f} dB", flush=True)
+ finally:
+ if TRACED:
+ try:
+ decoder.release_trace()
+ except Exception: # nothing to release if the run died before capture
+ pass
+ ttnn.close_mesh_device(device)
+ if MESH != (1, 1):
+ # Leaving fabric enabled after close can wedge the next job's init.
+ ttnn.set_fabric_config(ttnn.FabricConfig.DISABLED)
+
+ print(f"\n=== fp32 decode, {num_samples / SR:.2f} s of 32 kHz audio per clip, batch {BATCH} ===")
+ print(f"config: mesh {MESH[0]}x{MESH[1]}, t_factor={T_FACTOR} axis={MESH_AXIS}, traced={TRACED}")
+ print(f"{'clip':<15} {'CPU s':>7} {'device s':>9} {'speedup':>8} {'PSNR dB':>9} {'rel_rmse':>11} {'log-spec':>9}")
+ print("-" * 74)
+ for label, c, d, p, r, ls in rows:
+ print(f"{label:<15} {c:>7.3f} {d:>9.3f} {c / d:>7.2f}x {p:>9.2f} {r:>11.3e} {ls:>9.4f}")
+ if rows:
+ print("-" * 74)
+ n = len(rows)
+ print(
+ f"{'mean':<15} {sum(r[1] for r in rows) / n:>7.3f} {sum(r[2] for r in rows) / n:>9.3f} "
+ f"{sum(r[1] for r in rows) / sum(r[2] for r in rows):>7.2f}x {sum(r[3] for r in rows) / n:>9.2f}"
+ )
+ mean_psnr = sum(r[3] for r in rows) / n
+ mean_secs = sum(r[2] for r in rows) / n
+ # The accuracy criterion is "no worse than the single-device path at the same levers", not a
+ # fixed number: the decoder's constructed defaults are accurate mode (split_mode='full')
+ # and score ~67 dB, where all-fast scores far lower. A bar that does not name
+ # its lever set is meaningless across that spread.
+ # So the baseline has to be *measured* on this branch -- run once without the CVD_* variables and
+ # pass the mean back in via CVD_BASELINE_PSNR. Unset, this reports and asserts nothing on
+ # accuracy, because a hard-coded bar from another configuration is worse than no bar.
+ baseline_env = os.environ.get("CVD_BASELINE_PSNR")
+ if baseline_env:
+ baseline = float(baseline_env)
+ drop = baseline - mean_psnr
+ accuracy = (
+ f"psnr {mean_psnr:.2f} dB vs {baseline:.2f} baseline "
+ f"({'no degradation' if drop <= 0.05 else f'DOWN {drop:.2f} dB'}): "
+ f"{'PASS' if drop <= 0.05 else 'FAIL'}"
+ )
+ else:
+ accuracy = (
+ f"psnr {mean_psnr:.2f} dB (no baseline set -- run single-device first, then pass CVD_BASELINE_PSNR)"
+ )
+ print(
+ f"\nACCEPTANCE {accuracy}"
+ f" | latency {mean_secs * 1e3:.1f} ms vs 300 ms: {'PASS' if mean_secs <= 0.300 else 'FAIL'}"
+ f" (222 ms stretch: {'PASS' if mean_secs <= 0.222 else 'FAIL'})"
+ )
+ print(f"\nWAVs in {OUT_DIR} (per clip: _0_source, _1_cpu, _2_device{TAG})")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/code/models/tt_dit/tests/models/minimax_h3/decode_bench.py b/code/models/tt_dit/tests/models/minimax_h3/decode_bench.py
new file mode 100644
index 0000000000000000000000000000000000000000..3775e9aa1f1da6bd01fe4fcf88db74091d4473dd
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/decode_bench.py
@@ -0,0 +1,87 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Median decode time over N runs, building the decoder once.
+
+Build once, warm once, then time N decodes and report median plus spread. Timing a single call per
+process is where an earlier script's run-to-run noise came from.
+
+Usage -- the label names the saved waveform:
+
+ export MINIMAX_H3_MODEL_PATH=/path/to/MiniMax-H3-diffusers
+ python models/tt_dit/tests/models/minimax_h3/decode_bench.py baseline
+
+Env: BENCH_N (default 5), BENCH_OUT (default `generated/minimax_h3_audio/bench`).
+
+For latency *with* PSNR against a CPU reference, and for mesh/sharding/trace configurations, use
+`cpu_vs_device.py` instead; this script is single-device and reports timing only.
+"""
+import json
+import os
+import re
+import statistics
+import sys
+import time
+
+import torch
+from safetensors.torch import load_file
+
+import ttnn
+from models.tt_dit.models.audio_vae.minimax_h3.convert_minimax_h3_audio import convert_minimax_h3_audio_state_dict
+from models.tt_dit.models.audio_vae.minimax_h3.decoder_minimax_h3_audio import MiniMaxH3AudioDecoder
+
+# Reaches a filename below, so keep it to one path-safe component.
+LABEL = re.sub(r"[^A-Za-z0-9._-]", "_", os.path.basename(sys.argv[1])) or "bench"
+N = int(os.environ.get("BENCH_N", "5"))
+OUT = os.environ.get("BENCH_OUT") or os.path.join(
+ os.environ.get("TT_METAL_HOME", os.getcwd()), "generated", "minimax_h3_audio", "bench"
+)
+# `MINIMAX_H3_MODEL_PATH` matches the test suite; the older var is still accepted.
+_root = os.environ.get("MINIMAX_H3_MODEL_PATH") or os.environ.get("MINIMAX_H3_DIFFUSERS_DIR", "")
+if not _root:
+ raise SystemExit("set MINIMAX_H3_MODEL_PATH to a MiniMax-H3 diffusers snapshot")
+wd = os.path.join(_root, "audio_vae")
+cfg = {k: v for k, v in json.load(open(os.path.join(wd, "config.json"))).items() if not k.startswith("_")}
+
+d = ttnn.open_mesh_device(ttnn.MeshShape(1, 1), l1_small_size=65536)
+try:
+ dec = MiniMaxH3AudioDecoder(
+ latent_channels=cfg["latent_channels"],
+ latent_dim=cfg["latent_dim"],
+ decoder_dim=cfg["decoder_dim"],
+ decoder_rates=tuple(cfg["decoder_rates"]),
+ decoder_kernel_sizes=tuple(cfg["decoder_kernel_sizes"]),
+ resblock_kernel_sizes=tuple(cfg["resblock_kernel_sizes"]),
+ resblock_dilation_sizes=tuple(tuple(x) for x in cfg["resblock_dilation_sizes"]),
+ mesh_device=d,
+ )
+ dec.load_torch_state_dict(
+ convert_minimax_h3_audio_state_dict(load_file(os.path.join(wd, "diffusion_pytorch_model.safetensors"))),
+ strict=False,
+ )
+ torch.manual_seed(2)
+ lat = torch.randn(2, cfg["latent_channels"], 207) * 0.1
+ out = dec(lat)
+ ttnn.synchronize_device(d)
+ s = []
+ for _ in range(N):
+ t0 = time.perf_counter()
+ out = dec(lat)
+ ttnn.synchronize_device(d)
+ s.append(time.perf_counter() - t0)
+ os.makedirs(OUT, exist_ok=True)
+ # Both halves come from outside the process (BENCH_OUT, argv[1]), so contain the result here
+ # rather than relying on the sanitising above.
+ _out_dir = os.path.realpath(OUT)
+ _dest = os.path.realpath(os.path.join(_out_dir, LABEL + ".pt"))
+ if os.path.commonpath([_out_dir, _dest]) != _out_dir:
+ raise SystemExit(f"refusing to write outside {_out_dir}: {_dest}")
+ torch.save({"wav": torch.as_tensor(out).float().cpu(), "samples": s}, _dest)
+ print(
+ f"RESULT {LABEL}: median {statistics.median(s):.4f}s min {min(s):.4f} max {max(s):.4f} n={N} "
+ f"[{', '.join(f'{v:.3f}' for v in s)}]",
+ flush=True,
+ )
+finally:
+ ttnn.close_mesh_device(d)
diff --git a/code/models/tt_dit/tests/models/minimax_h3/test_adaln_precompute_minimax_h3.py b/code/models/tt_dit/tests/models/minimax_h3/test_adaln_precompute_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb6b18ec6007c609e1a77c81f9b04df9090299c7
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/test_adaln_precompute_minimax_h3.py
@@ -0,0 +1,206 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Host-only gate for the MiniMax-H3 AdaLN precompute, on a small synthetic checkpoint.
+The design rests on ``adaln_proj`` being batch-stable while ``time_embedder`` is not. The full
+26 GB build was verified once against the real checkpoint (git history: tools/build_adaln_table.py)."""
+
+import pytest
+import torch
+from safetensors.torch import save_file
+
+from ....pipelines.minimax_h3 import adaln_precompute as ap
+from ....pipelines.minimax_h3.packing import MINIMAX_H3_KEYFRAME_NOISE_AUG
+from ....pipelines.minimax_h3.scheduler import MiniMaxH3Scheduler
+
+HIDDEN = 64
+TIME_EMBED_DIM = 32
+FREQ_DIM = 16
+NUM_LAYERS = 3
+STEPS = 6
+
+
+def _schedules(steps=STEPS):
+ video = MiniMaxH3Scheduler(12.0)
+ audio = MiniMaxH3Scheduler(3.0)
+ video.set_timesteps(steps)
+ audio.set_timesteps(steps)
+ return video, audio
+
+
+def _step_timesteps(steps=STEPS):
+ video, audio = _schedules(steps)
+ return ap.request_step_timesteps(video.sigmas, audio.sigmas, MINIMAX_H3_KEYFRAME_NOISE_AUG)
+
+
+@pytest.fixture
+def synthetic_checkpoint(tmp_path):
+ torch.manual_seed(0)
+ tensors = {
+ "time_embedder.proj_in.weight": torch.randn(HIDDEN, FREQ_DIM),
+ "time_embedder.proj_in.bias": torch.randn(HIDDEN),
+ "time_embedder.proj_out.weight": torch.randn(TIME_EMBED_DIM, HIDDEN),
+ "time_embedder.proj_out.bias": torch.randn(TIME_EMBED_DIM),
+ "final_layer.adaln_proj.linear.weight": torch.randn(2 * HIDDEN, TIME_EMBED_DIM).bfloat16(),
+ "final_layer.adaln_proj.linear.bias": torch.randn(2 * HIDDEN).bfloat16(),
+ }
+ for layer in range(NUM_LAYERS):
+ out = ap.MINIMAX_H3_ADALN_PARAMS * HIDDEN * ap.MINIMAX_H3_MODALITY_NUM
+ tensors[f"blocks.{layer}.adaln_proj.linear.weight"] = torch.randn(out, TIME_EMBED_DIM).bfloat16()
+ tensors[f"blocks.{layer}.adaln_proj.linear.bias"] = torch.randn(out).bfloat16()
+ # Two shards, so the multi-shard key index is exercised.
+ keys = sorted(tensors)
+ save_file({k: tensors[k] for k in keys[: len(keys) // 2]}, tmp_path / "model-00001-of-00002.safetensors")
+ save_file({k: tensors[k] for k in keys[len(keys) // 2 :]}, tmp_path / "model-00002-of-00002.safetensors")
+ return tmp_path, tensors
+
+
+def _table(checkpoint, steps=STEPS):
+ return ap.precompute_adaln_table(
+ checkpoint,
+ _step_timesteps(steps),
+ num_layers=NUM_LAYERS,
+ hidden_size=HIDDEN,
+ freq_dim=FREQ_DIM,
+ )
+
+
+def test_frequency_embedding_is_cosine_then_sine():
+ diffusers_embeddings = pytest.importorskip("diffusers.models.embeddings")
+ timesteps = torch.tensor([0.0, 0.02, 0.5, 0.999, 1.0], dtype=torch.float32)
+ ours = ap.timestep_frequency_embedding(timesteps, 256)
+ theirs = diffusers_embeddings.get_timestep_embedding(timesteps, 256, flip_sin_to_cos=True, downscale_freq_shift=0)
+ assert torch.equal(ours, theirs)
+
+
+def test_silu_must_run_before_the_bfloat16_cast():
+ """bf16 activation shifts modulation ~7.8e-3, coherently across every block and step."""
+ torch.manual_seed(1)
+ weight = torch.randn(6 * HIDDEN * 3, TIME_EMBED_DIM).bfloat16()
+ bias = torch.randn(6 * HIDDEN * 3).bfloat16()
+ temb = torch.randn(4, TIME_EMBED_DIM, dtype=torch.float32)
+
+ correct = ap.project_block_adaln(temb, weight, bias, HIDDEN)
+ hoisted = torch.nn.functional.linear(torch.nn.functional.silu(temb.bfloat16()), weight, bias)
+ hoisted = torch.stack(hoisted.view(-1, 6 * HIDDEN).chunk(6, dim=-1), dim=1)
+
+ assert not torch.equal(correct, hoisted)
+ assert (correct.float() - hoisted.float()).abs().max() > 1e-3
+
+
+def test_adaln_projection_is_batch_stable():
+ torch.manual_seed(2)
+ weight = torch.randn(6 * HIDDEN * 3, TIME_EMBED_DIM).bfloat16()
+ bias = torch.randn(6 * HIDDEN * 3).bfloat16()
+ temb = torch.randn(16, TIME_EMBED_DIM, dtype=torch.float32)
+
+ batched = ap.project_block_adaln(temb, weight, bias, HIDDEN)
+ for row in (0, 5, 15):
+ alone = ap.project_block_adaln(temb[row : row + 1], weight, bias, HIDDEN)
+ assert torch.equal(batched[row * 3 : (row + 1) * 3], alone)
+
+
+def test_time_embedding_is_not_batch_stable(synthetic_checkpoint):
+ """Why ``temb`` is computed per step, asserted rather than silently depended on."""
+ _, tensors = synthetic_checkpoint
+ args = (
+ tensors["time_embedder.proj_in.weight"],
+ tensors["time_embedder.proj_in.bias"],
+ tensors["time_embedder.proj_out.weight"],
+ tensors["time_embedder.proj_out.bias"],
+ )
+ steps = _step_timesteps()
+ everything = torch.unique(torch.cat(steps), sorted=True)
+
+ batched = ap.time_embedding(everything, *args, freq_dim=FREQ_DIM)
+ per_step = ap.time_embedding(steps[0], *args, freq_dim=FREQ_DIM)
+ rows = [int((everything == value).nonzero()[0, 0]) for value in steps[0]]
+
+ assert torch.allclose(batched[rows], per_step, atol=1e-4)
+
+
+def test_step_timesteps_pin_conditioning_at_the_noise_aug_floor():
+ video, audio = _schedules()
+ steps = ap.request_step_timesteps(video.sigmas, audio.sigmas, MINIMAX_H3_KEYFRAME_NOISE_AUG)
+ for index, levels in enumerate(steps):
+ expected = max(float(video.timesteps[index]), MINIMAX_H3_KEYFRAME_NOISE_AUG)
+ assert pytest.approx(float(levels.max())) == expected
+
+
+def test_table_matches_per_step_recompute(synthetic_checkpoint):
+ checkpoint, tensors = synthetic_checkpoint
+ steps = _step_timesteps()
+ table = _table(checkpoint)
+
+ total_rows = sum(int(levels.numel()) for levels in steps)
+ assert len(steps) == _schedules()[0].num_inference_steps
+ assert table.num_layers == NUM_LAYERS
+ assert table.hidden_size == HIDDEN
+ assert table.num_steps == len(steps)
+ assert tuple(table.block_params.shape) == (
+ NUM_LAYERS,
+ total_rows * ap.MINIMAX_H3_MODALITY_NUM,
+ ap.MINIMAX_H3_ADALN_PARAMS,
+ HIDDEN,
+ )
+ assert table.block_params.dtype == torch.bfloat16
+ assert tuple(table.final_shift.shape) == (total_rows, HIDDEN)
+ assert int(table.step_offsets[0]) == 0
+ assert int(table.step_offsets[-1]) == total_rows
+
+ embed_args = (
+ tensors["time_embedder.proj_in.weight"],
+ tensors["time_embedder.proj_in.bias"],
+ tensors["time_embedder.proj_out.weight"],
+ tensors["time_embedder.proj_out.bias"],
+ )
+
+ for step, levels in enumerate(steps):
+ assert levels.numel() in (2, 3)
+ assert bool((levels[1:] > levels[:-1]).all())
+ assert torch.equal(table.step_timesteps(step), levels)
+
+ temb = ap.time_embedding(levels, *embed_args, freq_dim=FREQ_DIM)
+ rows = table.step_rows(step, torch.arange(levels.numel()))
+ low = int(rows[0]) * ap.MINIMAX_H3_MODALITY_NUM
+ high = (int(rows[-1]) + 1) * ap.MINIMAX_H3_MODALITY_NUM
+
+ for layer in range(NUM_LAYERS):
+ expected = ap.project_block_adaln(
+ temb,
+ tensors[f"blocks.{layer}.adaln_proj.linear.weight"],
+ tensors[f"blocks.{layer}.adaln_proj.linear.bias"],
+ HIDDEN,
+ )
+ assert torch.equal(table.block_params[layer, low:high], expected)
+
+ shift, scale = ap.project_final_adaln(
+ temb,
+ tensors["final_layer.adaln_proj.linear.weight"],
+ tensors["final_layer.adaln_proj.linear.bias"],
+ )
+ assert torch.equal(table.final_shift[rows[0] : rows[-1] + 1], shift)
+ assert torch.equal(table.final_scale[rows[0] : rows[-1] + 1], scale)
+
+
+def test_adaln_indices_address_timestep_and_modality(synthetic_checkpoint):
+ checkpoint, _ = synthetic_checkpoint
+ table = _table(checkpoint)
+ step = 3
+ offset = int(table.step_offsets[step])
+
+ timestep_indices = torch.tensor([0, 0, 1, 1])
+ token_tags = torch.tensor([0, 2, 1, -1])
+ indices = table.adaln_indices(step, timestep_indices, token_tags)
+
+ expected = torch.tensor(
+ [
+ (offset + 0) * 3 + 0,
+ (offset + 0) * 3 + 2,
+ (offset + 1) * 3 + 1,
+ # tag -1 padding rows clamp to the video slot; their output is discarded
+ (offset + 1) * 3 + 0,
+ ]
+ )
+ assert torch.equal(indices, expected)
diff --git a/code/models/tt_dit/tests/models/minimax_h3/test_audio_minimax_h3.py b/code/models/tt_dit/tests/models/minimax_h3/test_audio_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..57587da80848f42e3716e557c3958cecb0a47f03
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/test_audio_minimax_h3.py
@@ -0,0 +1,698 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Gate M8d: the MiniMax-H3 audio VAE (BigVGAN) at production shapes; stereo rides as batch 2.
+
+T-parallel decode works: ``test_audio_decode_t_parallel`` at the bottom of this file is resurrected from
+git history, as the previous version of this docstring asked for. The 8-way shard layout it called
+known-broken was `Vocoder.conv_pre` returning uninitialized memory when sharded (plus a halo bug in the
+since-retired tap-matmul path); both were fixed, and
+the test's PSNR assert is enforced for every factor rather than excused. ``test_neighbor_pad_t_minimax_h3.py``
+is still in git history if the halo itself ever needs its own gate again -- it was verified correct here
+(exact at pad 1/3/25) while hunting the conv_pre bug."""
+
+from __future__ import annotations
+
+import copy
+import os
+import time
+
+import pytest
+import torch
+from loguru import logger
+
+import ttnn
+
+from ....layers.audio_ops import Conv1dViaConv3d, depthwise_tap_filter
+from ....models.audio_vae.minimax_h3.blockings_minimax_h3_audio import register_h3_audio_blockings
+from ....models.audio_vae.minimax_h3.convert_minimax_h3_audio import (
+ assert_weight_norm_axes_consistent,
+ convert_minimax_h3_audio_state_dict,
+ fuse_attention_biases,
+ fuse_weight_norm,
+ remap_amp_activations,
+)
+from ....models.audio_vae.minimax_h3.decoder_minimax_h3_audio import MiniMaxH3AudioDecoder
+from ....models.audio_vae.vocoder_ltx import TILE_HEIGHT
+from ....parallel.config import ParallelFactor
+from ....parallel.manager import CCLManager
+from ....pipelines.minimax_h3.pipeline_minimax_h3 import resolve_mesh_preset
+from ....utils.check import assert_quality
+from .common import build_audio_decoder, load_config, psnr, weights_subdir
+
+# The vocoder needs extra L1 scratch, as the LTX audio tests do.
+SINGLE_DEVICE = [pytest.param((1, 1), {"l1_small_size": 65536}, id="single_device")]
+
+# Encode floor, calibrated before accurate mode became the constructed default (measured ~0.105 with
+# fp32 conv multipliers over ~130 convs); the accurate defaults only improve on it.
+AUDIO_RELATIVE_RMSE = 0.12
+
+CONV_PRE_SHAPE = (2048, 1024, 7) # conv_pre: the decoder's widest reduction, Cin 2048 x k 7
+CONV_PRE_LATENT_FRAMES = 207
+
+LATENT_CHANNELS = 32
+HOP_LENGTH = 800
+SAMPLING_RATE = 32000
+
+PRODUCTION_LATENT_FRAMES = [pytest.param(207, id="5s")] # 40 Hz latent; 10 s exercises no new path
+
+
+def _build_reference(load_weights: bool = True):
+ """The reference audio VAE, with weight norm still attached (the converter removes it)."""
+ weights_dir = weights_subdir("audio_vae")
+ if weights_dir is None:
+ pytest.skip("MiniMax-H3 audio_vae not found; set MINIMAX_H3_MODEL_PATH")
+ pytest.importorskip("diffusers", reason="pinned diffusers reference not installed")
+ from diffusers import AutoencoderKLMiniMaxH3Audio
+
+ config = load_config(weights_dir)
+ reference = AutoencoderKLMiniMaxH3Audio(**config).eval()
+ if load_weights:
+ from safetensors.torch import load_file
+
+ reference.load_state_dict(load_file(os.path.join(weights_dir, "diffusion_pytorch_model.safetensors")))
+ return reference, config
+
+
+def _golden_latents(reference, num_latent_frames: int):
+ """``(waveform, latents, expected_decode)``; latents come from the reference encoder, not ``randn``."""
+ waveform = torch.randn(2, 1, num_latent_frames * HOP_LENGTH) * 0.1
+ with torch.no_grad():
+ posterior = reference.encode(waveform).latent_dist
+ latents = posterior.mode()[..., :num_latent_frames]
+ expected = reference.decode(latents).sample
+ return waveform, latents, expected
+
+
+def _log_mel_distance(a: torch.Tensor, b: torch.Tensor, *, n_fft: int = 1024, hop: int = 256) -> float:
+ """Mean absolute log-magnitude-spectrogram difference; catches spectral drift an aggregate PCC hides."""
+ window = torch.hann_window(n_fft)
+ spectra = []
+ for signal in (a, b):
+ flat = signal.reshape(-1, signal.shape[-1]).float()
+ stft = torch.stft(flat, n_fft=n_fft, hop_length=hop, window=window, return_complex=True)
+ spectra.append(torch.log(stft.abs().clamp_min(1e-5)))
+ return (spectra[0] - spectra[1]).abs().mean().item()
+
+
+def _tt_decoder(config: dict, mesh_device) -> MiniMaxH3AudioDecoder:
+ return build_audio_decoder(config, mesh_device)
+
+
+@pytest.mark.parametrize("num_latent_frames", PRODUCTION_LATENT_FRAMES)
+@pytest.mark.parametrize(("mesh_device", "device_params"), SINGLE_DEVICE, indirect=["mesh_device", "device_params"])
+def test_decode(mesh_device, num_latent_frames):
+ """The whole decode path against the reference, at a production duration, stereo.
+
+ Constructor defaults are accurate mode (split_mode='full'), so the bars are the
+ accurate-mode ones: measured 0.0045 rel RMSE / 99.9990% PCC / 67.5 dB PSNR.
+ """
+ reference, config = _build_reference()
+ torch.manual_seed(1)
+
+ _, latents, expected = _golden_latents(reference, num_latent_frames)
+ with torch.no_grad():
+ expected_proj = reference.dec_in_proj(latents)
+
+ tt_decoder = _tt_decoder(config, mesh_device)
+ # The precision levers are the constructed defaults; assert they landed where they matter.
+ assert tt_decoder.dec_in_proj.split_mode == "full", "split_mode='full' did not land on dec_in_proj"
+ assert tt_decoder.decoder.conv_post.split_mode == "full", "split_mode='full' did not land on conv_post"
+
+ tt_decoder.load_torch_state_dict(convert_minimax_h3_audio_state_dict(dict(reference.state_dict())), strict=False)
+
+ projected = tt_decoder._project_latents_device(latents)
+ assert projected.shape == expected_proj.shape, f"proj {tuple(projected.shape)} != {tuple(expected_proj.shape)}"
+ assert_quality(expected_proj, projected, pcc=0.999)
+
+ actual = tt_decoder(latents)
+
+ assert actual.shape == expected.shape, f"shape {tuple(actual.shape)} != reference {tuple(expected.shape)}"
+ assert_quality(expected, actual, pcc=0.9999, relative_rmse=0.006)
+
+ psnr_db = psnr(expected, actual)
+ mel_distance = _log_mel_distance(expected, actual)
+ assert psnr_db >= 60.0, f"decode PSNR {psnr_db:.2f} dB < 60 dB"
+ assert mel_distance <= 5.0, f"log-spectrogram distance {mel_distance:.3f} > 5.0"
+
+ left, right = actual[0, 0], actual[1, 0]
+ assert not torch.allclose(left, right, atol=1e-4), "stereo channels are identical -- a broadcast bug"
+
+
+@pytest.mark.parametrize(("mesh_device", "device_params"), SINGLE_DEVICE, indirect=["mesh_device", "device_params"])
+def test_depthwise_chunked_conv1d_matches_torch(mesh_device):
+ """`depthwise_tap_filter`'s C-chunked `ttnn.conv1d` recovery, against a torch depthwise reference.
+
+ Covers the slicing, the per-chunk weight, and the concat in isolation, at the shape the decode
+ really chunks (T_pad=166, C=512, K=7, stride=1) -- the activation block is C*K wide there and
+ the slicer runs out of L1 at full C.
+ """
+ torch.manual_seed(0)
+ B, T_pad, C, K, stride = 1, 166, 512, 7, 1
+
+ x = torch.randn(B, T_pad, C, dtype=torch.float32)
+ taps = torch.randn(K, dtype=torch.float32)
+ x_dev = ttnn.from_torch(x, dtype=ttnn.float32, layout=ttnn.ROW_MAJOR_LAYOUT, device=mesh_device)
+
+ cache: dict = {}
+ out = depthwise_tap_filter(
+ x_dev,
+ [float(t) for t in taps],
+ stride,
+ mesh_device=mesh_device,
+ dtype=ttnn.float32,
+ cache=cache,
+ )
+ actual = ttnn.to_torch(ttnn.get_device_tensors(out)[0]).float()
+
+ # Valid depthwise conv over T: one tap vector shared by every channel, groups=C.
+ weight = taps.view(1, 1, K).expand(C, 1, K).contiguous()
+ expected = torch.nn.functional.conv1d(x.transpose(1, 2), weight, stride=stride, groups=C).transpose(1, 2)
+
+ assert actual.shape == expected.shape, f"{actual.shape} != {expected.shape}"
+ psnr_db = psnr(expected, actual)
+ logger.info(f"chunked depthwise conv1d vs torch: {psnr_db:.2f} dB")
+ assert psnr_db > 60.0, f"chunked depthwise conv1d diverges from torch: {psnr_db:.2f} dB"
+
+ # Prove the chunked path ran: it keys its prepared weight on the chunk width where the unchunked
+ # path keys on C. Without this the test would pass on plain conv1d or a silent MAC fallback, i.e.
+ # assert torch equivalence while covering none of the code it exists for.
+ chunk_widths = [k[1] for k in cache if isinstance(k, tuple) and k and k[0] == "w" and k[1] < C]
+ assert chunk_widths, (
+ f"expected the C-chunked conv1d recovery to run at C={C}, K={K}, T_pad={T_pad}, but the weight "
+ f"cache has no sub-C chunk key: {sorted(k for k in cache if isinstance(k, tuple))}. Either "
+ "conv1d now fits at full C (then this shape no longer covers the chunked path and the test needs "
+ "a new one), or it fell back to MAC (then the recovery is broken)."
+ )
+ logger.info(f"chunked into widths {sorted(set(chunk_widths))} from C={C}")
+
+
+@pytest.mark.parametrize(("mesh_device", "device_params"), SINGLE_DEVICE, indirect=["mesh_device", "device_params"])
+def test_conv_operand_split_improves_precision(mesh_device):
+ """Operand splitting really does beat the fp32 conv floor, at ``conv_pre``'s production shape."""
+ # Raw Conv1dViaConv3d does not register the H3 conv blockings (the H3 audio module constructors
+ # do); without this, every conv silently falls back to C_in_block=32 and the "off" bar fails.
+ register_h3_audio_blockings()
+ in_channels, out_channels, kernel = CONV_PRE_SHAPE
+ torch.manual_seed(0)
+ reference = torch.nn.Conv1d(in_channels, out_channels, kernel, padding=kernel // 2).float().eval()
+ x = torch.randn(2, in_channels, CONV_PRE_LATENT_FRAMES) * 0.1
+ with torch.no_grad():
+ golden = copy.deepcopy(reference).double()(x.double()).float()
+
+ x_device = None
+ errors = {}
+ for mode in ("off", "weight", "full"):
+ layer = Conv1dViaConv3d(
+ in_channels,
+ out_channels,
+ kernel_size=kernel,
+ padding_mode="zeros",
+ bias=True,
+ mesh_device=mesh_device,
+ dtype=ttnn.float32,
+ split_mode=mode,
+ )
+ assert layer.split_mode == mode, f"layer resolved split_mode {layer.split_mode!r}, expected {mode!r}"
+ assert (layer.weight_lo is None) == (mode == "off"), f"weight residual allocation wrong for {mode!r}"
+ layer.load_torch_state_dict(
+ {
+ "weight": reference.weight.detach().contiguous(),
+ "bias": reference.bias.detach().contiguous(),
+ },
+ strict=False,
+ )
+ if x_device is None:
+ x_device = ttnn.from_torch(
+ x.transpose(1, 2).contiguous(), dtype=ttnn.float32, layout=ttnn.ROW_MAJOR_LAYOUT, device=mesh_device
+ )
+ actual = ttnn.to_torch(layer(x_device)).float().transpose(1, 2)
+ errors[mode] = float((actual.double() - golden.double()).pow(2).mean().sqrt() / golden.double().std())
+
+ logger.info("conv_pre rel_rmse by split mode: " + ", ".join(f"{k}={v:.3e}" for k, v in errors.items()))
+
+ # 2.1e-03 sits between production's 1.86e-03 and the unregistered C_in_block=32 fallback's 2.40e-03.
+ assert errors["off"] <= 2.1e-3, (
+ f"baseline conv_pre error {errors['off']:.3e} is above the production floor -- "
+ "C_in_block has probably fallen back to 32, meaning register_h3_audio_blockings() did not run"
+ )
+ # Measured 1.86e-03 / 1.25e-03 / 1.00e-03; ratios carry ~1.3x margin.
+ assert errors["weight"] <= 0.85 * errors["off"], f"weight-split did not improve: {errors}"
+ assert errors["full"] <= 0.70 * errors["off"], f"full split did not improve enough: {errors}"
+ assert errors["full"] < errors["weight"], f"full split should beat weight-only: {errors}"
+
+
+def _tt_encoder(config: dict, mesh_device):
+ from ....models.audio_vae.minimax_h3.encoder_minimax_h3_audio import MiniMaxH3AudioEncoder
+
+ return MiniMaxH3AudioEncoder(
+ encoder_dim=config["encoder_dim"],
+ encoder_rates=tuple(config["encoder_rates"]),
+ latent_dim=config["latent_dim"],
+ latent_channels=config["latent_channels"],
+ num_attention_heads=config["num_attention_heads"],
+ mesh_device=mesh_device,
+ )
+
+
+@pytest.mark.parametrize("num_latent_frames", PRODUCTION_LATENT_FRAMES)
+@pytest.mark.parametrize(("mesh_device", "device_params"), SINGLE_DEVICE, indirect=["mesh_device", "device_params"])
+def test_encode(mesh_device, num_latent_frames):
+ """The whole encode path -- DAC trunk, ``pre_block``, posterior heads -- vs the reference."""
+ reference, config = _build_reference()
+ torch.manual_seed(2)
+
+ waveform = torch.randn(2, 1, num_latent_frames * HOP_LENGTH) * 0.1
+ with torch.no_grad():
+ posterior = reference.encode(waveform).latent_dist
+ expected_mean, expected_logs = posterior.mean, posterior.logs
+
+ tt_encoder = _tt_encoder(config, mesh_device)
+ tt_encoder.load_torch_state_dict(convert_minimax_h3_audio_state_dict(dict(reference.state_dict())), strict=False)
+ mean, logs = tt_encoder(waveform)
+
+ assert mean.shape == expected_mean.shape, f"mean shape {tuple(mean.shape)} != {tuple(expected_mean.shape)}"
+ assert mean.shape[2] == num_latent_frames, f"expected {num_latent_frames} latent frames, got {mean.shape[2]}"
+ assert_quality(expected_mean, mean, pcc=0.99, relative_rmse=AUDIO_RELATIVE_RMSE)
+ assert_quality(expected_logs, logs, pcc=0.98)
+
+
+@pytest.mark.parametrize(("mesh_device", "device_params"), SINGLE_DEVICE, indirect=["mesh_device", "device_params"])
+def test_roundtrip(mesh_device):
+ """End to end: encode then decode on device, against the reference's own round trip."""
+ reference, config = _build_reference()
+ torch.manual_seed(3)
+ num_latent_frames = 207
+
+ waveform, _, expected = _golden_latents(reference, num_latent_frames)
+
+ tt_encoder = _tt_encoder(config, mesh_device)
+ tt_decoder = _tt_decoder(config, mesh_device)
+ converted = convert_minimax_h3_audio_state_dict(dict(reference.state_dict()))
+ tt_encoder.load_torch_state_dict(converted, strict=False)
+ tt_decoder.load_torch_state_dict(converted, strict=False)
+
+ mean, _ = tt_encoder(waveform)
+ actual = tt_decoder(mean)
+
+ assert actual.shape == expected.shape, f"shape {tuple(actual.shape)} != {tuple(expected.shape)}"
+ psnr_db = psnr(expected, actual)
+ assert psnr_db >= 28.0, f"round-trip PSNR {psnr_db:.2f} dB < 28 dB"
+ assert _log_mel_distance(expected, actual) <= 5.0, "round-trip spectrum drifted"
+
+
+# -------------------------------------------------------------------- weight-norm fusion and checkpoint conversion (host only)
+
+
+def test_fuse_weight_norm_matches_torch_conv_transpose1d():
+ """The load-bearing case: axis 0 of a ConvTranspose1d weight is ``in_channels``, not out."""
+ torch.manual_seed(1)
+ conv = torch.nn.ConvTranspose1d(32, 16, kernel_size=4, stride=2)
+ assert conv.weight.shape == (32, 16, 4), "ConvTranspose1d weight is (in, out, k)"
+ conv = torch.nn.utils.weight_norm(conv)
+ with torch.no_grad():
+ conv.weight_g.normal_(1.0, 0.2)
+ conv.weight_v.normal_()
+ assert conv.weight_g.shape == (32, 1, 1), "weight_g is per-in_channel, confirming dim=0"
+
+ weight_g = conv.weight_g.detach().clone()
+ weight_v = conv.weight_v.detach().clone()
+ fused = fuse_weight_norm(weight_g, weight_v)
+ torch.nn.utils.remove_weight_norm(conv)
+
+ relative = (fused - conv.weight).abs().max().item() / conv.weight.abs().max().item()
+ assert relative < 1e-6, f"ConvTranspose1d fusion differs from torch by {relative:.3e}"
+
+ wrong_norm = weight_v.transpose(0, 1).flatten(1).norm(dim=1).view(1, -1, 1)
+ wrong = weight_g * weight_v / wrong_norm
+ assert wrong.shape == fused.shape, "the wrong axis still type-checks -- hence this test"
+ assert not torch.allclose(wrong, fused, atol=1e-4), "the two axes agree, so this test proves nothing"
+
+
+def test_remap_amp_activations_interleaves_correctly():
+ """``activations.{0,2,4}`` -> ``acts1.{0,1,2}`` and ``{1,3,5}`` -> ``acts2.{0,1,2}``."""
+ state = {f"resblocks.0.activations.{i}.act.alpha": torch.tensor([float(i)]) for i in range(6)}
+ remapped = remap_amp_activations(state)
+
+ for i in range(3):
+ assert remapped[f"resblocks.0.acts1.{i}.act.alpha"].item() == 2 * i
+ assert remapped[f"resblocks.0.acts2.{i}.act.alpha"].item() == 2 * i + 1
+ assert not any("activations." in key for key in remapped), "an activations key survived"
+
+
+def test_fuse_attention_biases_rejects_a_nonzero_k_bias():
+ """A ``zero_k_bias`` that is not zero must fail loudly, not be dropped."""
+ state = {
+ "pre_block.attn.q_bias": torch.ones(8),
+ "pre_block.attn.v_bias": torch.full((8,), 2.0),
+ "pre_block.attn.zero_k_bias": torch.zeros(8),
+ }
+ fused = fuse_attention_biases(state)
+ bias = fused["pre_block.attn.qkv.bias"]
+ assert bias.shape == (24,)
+ assert torch.equal(bias[:8], torch.ones(8))
+ assert torch.equal(bias[8:16], torch.zeros(8))
+ assert torch.equal(bias[16:], torch.full((8,), 2.0))
+
+ state["pre_block.attn.zero_k_bias"] = torch.ones(8)
+ with pytest.raises(AssertionError, match="not all zero"): # allow-pytest.raises: guards a silent data-loss path
+ fuse_attention_biases(state)
+
+
+def test_real_checkpoint_fusion_matches_reference_module():
+ """Real checkpoint: axes hold, every pair fuses, fused weights equal torch's ``remove_weight_norm``."""
+ weights_dir = weights_subdir("audio_vae")
+ if weights_dir is None:
+ pytest.skip("MiniMax-H3 audio_vae not found; set MINIMAX_H3_MODEL_PATH")
+ pytest.importorskip("diffusers", reason="pinned diffusers reference not installed")
+ from diffusers import AutoencoderKLMiniMaxH3Audio
+ from safetensors.torch import load_file
+
+ config = load_config(weights_dir)
+ reference = AutoencoderKLMiniMaxH3Audio(**config)
+ state = load_file(os.path.join(weights_dir, "diffusion_pytorch_model.safetensors"))
+ reference.load_state_dict(state)
+
+ assert_weight_norm_axes_consistent(state)
+ num_pairs = len([k for k in state if k.endswith(".weight_g")])
+ assert num_pairs > 100, f"expected ~172 weight-normed convs, found {num_pairs}"
+
+ converted = convert_minimax_h3_audio_state_dict(state)
+ assert not [k for k in converted if k.endswith((".weight_g", ".weight_v"))], "a weight-norm pair survived"
+ assert not [k for k in converted if k.endswith(("q_bias", "v_bias", "zero_k_bias"))], "an attn bias survived"
+ assert not [k for k in converted if "activations." in k], "an activations key survived"
+ assert (
+ len(converted) == len(state) - num_pairs - 2
+ ), f"key count {len(converted)} does not match {len(state)} minus {num_pairs} g/v pairs and 2 folded biases"
+
+ for module in reference.modules():
+ if hasattr(module, "weight_g"):
+ torch.nn.utils.remove_weight_norm(module)
+ expected = dict(reference.state_dict())
+
+ checked = 0
+ worst_key, worst = None, 0.0
+ for key, value in expected.items():
+ if not key.endswith(".weight") or key not in converted:
+ continue
+ scale = max(value.abs().max().item(), 1e-12)
+ relative = (converted[key] - value).abs().max().item() / scale
+ if relative > worst:
+ worst_key, worst = key, relative
+ checked += 1
+ assert checked > 100, f"only compared {checked} fused weights"
+ assert worst < 1e-6, f"worst fused weight is {worst_key} at relative {worst:.3e}"
+
+
+# -------------------------------------------------------------------- traced audio decode
+
+# 450 MB trace region: the accurate-mode (default) decode graph needs 375463936 B.
+TRACED = [
+ pytest.param((1, 1), {"l1_small_size": 65536, "trace_region_size": 450_000_000}, id="single_device"),
+]
+NUM_LATENT_FRAMES = 207
+ITERS = 5
+
+
+def _best(fn) -> float:
+ fn()
+ best = float("inf")
+ for _ in range(ITERS):
+ t0 = time.perf_counter()
+ fn()
+ best = min(best, time.perf_counter() - t0)
+ return best
+
+
+@pytest.mark.parametrize(("mesh_device", "device_params"), TRACED, indirect=["mesh_device", "device_params"])
+def test_audio_decode_traced(mesh_device):
+ reference, config = _build_reference()
+ converted = convert_minimax_h3_audio_state_dict(dict(reference.state_dict()))
+
+ torch.manual_seed(2)
+ decoder = _tt_decoder(config, mesh_device)
+ decoder.load_torch_state_dict(converted, strict=False)
+
+ latents = torch.randn(2, config["latent_channels"], NUM_LATENT_FRAMES) * 0.1
+
+ plain = decoder(latents)
+ traced = decoder(latents, traced=True)
+ assert traced.shape == plain.shape, f"{traced.shape} != {plain.shape}"
+
+ # PSNR inf would also appear if traced=True silently fell through, so confirm a trace was captured.
+ tracers = type(decoder.decoder)._forward_device._tracers_keyed.get(decoder.decoder, {})
+ assert tracers, "traced=True captured no trace; the PSNR below would be meaningless"
+ assert plain.abs().max() > 1e-6, "decoder produced all-zero output; PSNR would be vacuous"
+
+ psnr_db = psnr(plain, traced)
+ logger.info(f"traced vs untraced PSNR: {psnr_db:.2f} dB ({len(tracers)} trace(s) captured)")
+
+ untraced_s = _best(lambda: decoder(latents))
+ traced_s = _best(lambda: decoder(latents, traced=True))
+ logger.info(
+ f"PERF audio_decode_5s untraced {untraced_s:.4f} s | traced {traced_s:.4f} s "
+ f"-> {untraced_s / traced_s:.2f}x"
+ )
+ decoder.release_trace()
+
+ assert psnr_db > 60.0, f"traced output diverges from untraced: PSNR {psnr_db:.2f} dB"
+
+
+MESH = [
+ pytest.param(
+ (4, 8),
+ {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "require_exact_physical_num_devices": True,
+ "l1_small_size": 65536,
+ },
+ id="mesh4x8",
+ ),
+ # One Galaxy opened as a 32-wide line: the length of the quad Galaxy's inter-host axis, so the
+ # opt-in factor 32 is exercised on a single machine.
+ pytest.param(
+ (1, 32),
+ {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D,
+ "require_exact_physical_num_devices": True,
+ "l1_small_size": 65536,
+ },
+ id="mesh1x32",
+ ),
+]
+# (t_factor, mesh_axis) per mesh shape. The factor must equal the length of the axis it shards: factor=2
+# or 4 on an 8-wide axis dies in `_partition_t` ("height begin index aligned to tiles"), because the
+# partition indexes by the device's coordinate along the axis and assumes it covers it.
+#
+# KNOWN_BROKEN must stay empty -- an entry silences the per-factor PSNR assert, the only guard against a
+# fast wrong answer. Short clips work via `Vocoder._upload_BCT`'s per-shard tile-floor.
+FACTORS_BY_MESH = {(4, 8): [(1, 1), (4, 0), (8, 1)], (1, 32): [(1, 1), (32, 1)]}
+KNOWN_BROKEN: set[tuple[int, int]] = set()
+# 207 is the production short clip. 192 (T mod 32 == 0) puts the last real row on the final row of its
+# shard with no pad buffer before the next shard: the layout where a fully-padded shard's garbage tail
+# reached real audio while the global PSNR still read 50 dB. Hence the separate tail gate below.
+T_PARALLEL_FRAMES = [207, 192]
+TAIL_ROWS = 32 # latent rows of the trailing suffix checked on its own
+
+
+def _shards_pad(num_latent_frames: int, factor: int) -> bool:
+ """True when `Vocoder._upload_BCT` pads T for this factor (per-shard rows are floored to a tile)."""
+ return max(-(-num_latent_frames // factor), TILE_HEIGHT) * factor > num_latent_frames
+
+
+def _build(mesh_device, config, converted, parallel_config, ccl_manager):
+ """The decoder at this file's shared defaults, plus a shard layout.
+
+ Goes through `build_audio_decoder` rather than constructing directly so the precision levers
+ (`split_mode`, `max_c_in_block`) stay at whatever the shipping default is -- a
+ sharded run must measure the same configuration the unsharded gates do, or a divergence could
+ be a lever difference rather than a sharding bug.
+ """
+ decoder = build_audio_decoder(
+ config,
+ mesh_device,
+ parallel_config=parallel_config,
+ ccl_manager=ccl_manager,
+ )
+ decoder.load_torch_state_dict(converted, strict=False)
+ return decoder
+
+
+def _localize_divergence(baseline, parallel, *, factor: int, logger) -> None:
+ """Say *where* a diverging shard layout diverges. "PSNR -10.2 dB" alone names no bug.
+
+ Three shapes of answer, each pointing somewhere different:
+
+ - concentrated at the internal shard boundaries -> the halo exchange is wrong, and each conv in the
+ stack needs `kernel_size - 1` samples of its neighbour that it is not getting;
+ - uniform across the whole signal -> the shard layout itself is wrong, not its edges;
+ - a prefix or suffix only -> the causal padding or the final trim is off by a shard.
+
+ Also reports the best cross-correlation lag: a diverging-but-highly-correlated output at a nonzero
+ lag is a *shift*, which is a trim bug rather than a numerics one, and PSNR cannot distinguish those.
+ """
+ import numpy as np
+
+ error = (parallel - baseline).abs()
+ total = baseline.shape[-1]
+ logger.info(
+ f" divergence localization, t_factor={factor}: overall mean {error.mean():.6f} "
+ f"max {error.max():.4f} against baseline absmax {baseline.abs().max():.4f}"
+ )
+ per_shard = []
+ for shard in range(factor):
+ lo, hi = shard * total // factor, (shard + 1) * total // factor
+ per_shard.append(float(error[..., lo:hi].mean()))
+ logger.info(f" per-shard mean error: {[f'{v:.6f}' for v in per_shard]}")
+
+ # Boundary vs interior. A halo bug puts the error in a narrow band at each internal boundary.
+ window = 128
+ boundary, interior = [], []
+ for shard in range(1, factor):
+ cut = shard * total // factor
+ boundary.append(float(error[..., max(0, cut - window) : cut + window].mean()))
+ mask = torch.ones(total, dtype=torch.bool)
+ for shard in range(1, factor):
+ cut = shard * total // factor
+ mask[max(0, cut - window) : cut + window] = False
+ interior = float(error[..., mask].mean())
+ logger.info(
+ f" boundary bands (+-{window}): {[f'{v:.6f}' for v in boundary]} interior {interior:.6f} "
+ f"ratio {max(boundary) / max(interior, 1e-12):.2f}"
+ )
+
+ a = baseline[0, 0].numpy()
+ c = parallel[0, 0].numpy()
+ best, best_lag = -2.0, None
+ for lag in range(-4096, 4097, 32):
+ x, y = (a[-lag:], c[: len(c) + lag]) if lag < 0 else (a[: len(a) - lag], c[lag:])
+ n = min(len(x), len(y))
+ if n < 2048:
+ continue
+ r = float(np.corrcoef(x[:n], y[:n])[0, 1])
+ if r > best:
+ best, best_lag = r, lag
+ logger.info(
+ f" correlation at lag 0: {float(np.corrcoef(a, c)[0, 1]):.4f}; "
+ f"best {best:.4f} at lag {best_lag} (nonzero lag => a shift, i.e. a trim bug)"
+ )
+
+
+# ~14 min: three decoder builds plus a decode per factor, against `pytest.ini`'s 300 s default.
+@pytest.mark.timeout(3600)
+@pytest.mark.parametrize("num_latent_frames", T_PARALLEL_FRAMES)
+@pytest.mark.parametrize(("mesh_device", "device_params"), MESH, indirect=["mesh_device", "device_params"])
+def test_audio_decode_t_parallel(mesh_device, num_latent_frames):
+ weights_dir = weights_subdir("audio_vae")
+ if weights_dir is None:
+ pytest.skip("MiniMax-H3 audio_vae not found; set MINIMAX_H3_MODEL_PATH")
+ pytest.importorskip("diffusers", reason="pinned diffusers reference not installed")
+ from diffusers import AutoencoderKLMiniMaxH3Audio
+ from loguru import logger
+ from safetensors.torch import load_file
+
+ from ....models.audio_vae.minimax_h3.convert_minimax_h3_audio import convert_minimax_h3_audio_state_dict
+
+ config = load_config(weights_dir)
+ reference = AutoencoderKLMiniMaxH3Audio(**config).eval()
+ reference.load_state_dict(load_file(os.path.join(weights_dir, "diffusion_pytorch_model.safetensors")))
+ converted = convert_minimax_h3_audio_state_dict(dict(reference.state_dict()))
+
+ torch.manual_seed(2)
+ latents = torch.randn(2, config["latent_channels"], num_latent_frames) * 0.1
+ factors = FACTORS_BY_MESH[(mesh_device.shape[0], mesh_device.shape[1])]
+ # Only sharded factors that pad exercise the tail path; one that pads nothing (e.g. 4 at 192 latents,
+ # 48 rows/shard) decodes bit-identically to unsharded and would only add a cold compile.
+ factors = [(f, a) for f, a in factors if f == 1 or _shards_pad(num_latent_frames, f)]
+
+ baseline_out = None
+ baseline_s = None
+ results = []
+ # Run the pipeline's own CCL preset (Ring, two links on 4x8) so the gate exercises production settings;
+ # an unlisted mesh shape (1x32) has no preset and keeps the single-link line.
+ preset = resolve_mesh_preset(tuple(mesh_device.shape), required=False)
+ num_links = preset.get("num_links", 1)
+ topology = preset.get("topology", ttnn.Topology.Linear)
+ logger.info(f"CCL for the sharded decoders: num_links={num_links}, topology={topology}")
+
+ for factor, axis in factors:
+ pc = None if factor <= 1 else ParallelFactor(factor=factor, mesh_axis=axis)
+ ccl = None if pc is None else CCLManager(mesh_device, num_links=num_links, topology=topology)
+ try:
+ decoder = _build(mesh_device, config, converted, pc, ccl)
+ out = decoder(latents)
+ seconds = _best(lambda: decoder(latents))
+ except Exception as exc:
+ # Every FACTORS entry is a supported layout, so a raise is a regression, not a "result":
+ # recording it as unsupported let factor 4 break while factor 8 kept the test green, since
+ # the asserts below need only *some* factor to run. Optional layouts go in KNOWN_BROKEN.
+ logger.warning(f"t_factor={factor} axis={axis} FAILED: {str(exc)[:160]}")
+ results.append((factor, axis, None, None, None))
+ if (factor, axis) not in KNOWN_BROKEN:
+ raise
+ continue
+
+ if baseline_out is None:
+ baseline_out, baseline_s = out, seconds
+ psnr_db = tail_db = float("inf")
+ else:
+ assert out.shape == baseline_out.shape, f"factor {factor}: {out.shape} != {baseline_out.shape}"
+ # `psnr_db`, not `psnr`: binding the float to `psnr` shadows the imported helper, so the
+ # first factor's `float("inf")` makes the second factor's call a TypeError.
+ psnr_db = psnr(baseline_out, out)
+ # The global PSNR averages a corrupt tail away (50.6 dB overall hid a 33.8 dB last 4 rows).
+ n_tail = min(out.shape[-1], TAIL_ROWS * HOP_LENGTH)
+ tail_db = psnr(baseline_out[..., -n_tail:], out[..., -n_tail:])
+ results.append((factor, axis, seconds, psnr_db, tail_db))
+ logger.info(
+ f"PERF audio_decode t_factor={factor} axis={axis}: {seconds:.4f} s "
+ f"({baseline_s / seconds:.2f}x) PSNR vs 1-device {psnr_db:.1f} dB, last {TAIL_ROWS} rows {tail_db:.1f} dB"
+ )
+ if psnr_db < 40.0 and out is not baseline_out:
+ _localize_divergence(baseline_out.float(), out.float(), factor=factor, logger=logger)
+ del decoder
+
+ logger.info("=== audio decode T-parallel summary ===")
+ for factor, axis, seconds, psnr_db, tail_db in results:
+ if seconds is None:
+ logger.info(f" t_factor={factor:2d} axis={axis}: unsupported")
+ else:
+ logger.info(
+ f" t_factor={factor:2d} axis={axis}: {seconds:.4f} s {baseline_s / seconds:5.2f}x "
+ f"PSNR {psnr_db:6.1f} dB tail {tail_db:6.1f} dB"
+ )
+
+ # The baseline must have run, or there is nothing to compare against and every other factor was
+ # skipped for want of a reference. Without this the test PASSES when the whole stack is broken:
+ # observed once on a device left dirty by an unrelated crash, where all three factors raised
+ # TT_FATAL, each was swallowed as "unsupported", and the run reported green. A gate that cannot
+ # fail is worse than no gate.
+ baseline_ran = any(seconds is not None and factor == 1 for factor, _, seconds, _, _ in results)
+ assert baseline_ran, (
+ "the single-device baseline did not run, so nothing was compared. If this follows a crashed "
+ "run, reset the device (`tt-smi -glx_reset`) -- an allocator TT_FATAL here is usually a dirty "
+ "device, not a code failure"
+ )
+ # `baseline_ran` above catches "everything failed" but not the subtler case: if factor 1 raises,
+ # the loop's `baseline_out is None` branch promotes the *next* factor to baseline, and that factor
+ # then scores PSNR inf against itself and reads as correct. That happened for real -- a fusion
+ # crash killed factor 1, factor 4 became the reference, and a -10.1 dB configuration reported inf
+ # until the baseline was made to run. The first result must be the unsharded one.
+ assert results and results[0][0] == 1 and results[0][2] is not None, (
+ f"factor 1 must be the baseline, but the first result that ran was {results[0][:2] if results else None}; "
+ "a later factor has been promoted to baseline and is being compared against itself"
+ )
+ ran = [(f, a) for f, a, seconds, _, _ in results if seconds is not None and f != 1]
+ assert ran, "no parallel factor ran at all; the T-parallel path is entirely unavailable"
+
+ # Any factor that ran must agree with the single-device path; a fast wrong answer fails. The tail is
+ # gated on its own because the global number passed at 50.6 dB with the last rows at 33.8 dB.
+ for factor, axis, seconds, psnr_db, tail_db in results:
+ if seconds is None or (factor, axis) in KNOWN_BROKEN:
+ continue
+ assert psnr_db > 40.0, f"t_factor={factor} axis={axis} diverges from 1-device: PSNR {psnr_db:.1f} dB"
+ assert tail_db > 40.0, (
+ f"t_factor={factor} axis={axis}: last {TAIL_ROWS} latent rows diverge from 1-device: {tail_db:.1f} dB "
+ f"(global {psnr_db:.1f} dB) -- a shard's tail pad is being filled from the wrong row"
+ )
diff --git a/code/models/tt_dit/tests/models/minimax_h3/test_conditioning_minimax_h3.py b/code/models/tt_dit/tests/models/minimax_h3/test_conditioning_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..82efff2f880db12b41b49b63d8298dfd30e28376
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/test_conditioning_minimax_h3.py
@@ -0,0 +1,317 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Host-only gate for MiniMax-H3 FL2VA keyframe conditioning. Four silent contracts: ImageNet
+pixel statistics (not [-1, 1]), a sampled posterior under a request-independent seed, an fp16
+round trip on the sampled latent, and conditioning noise as the request generator's first draw."""
+
+import os
+from types import SimpleNamespace
+
+import numpy as np
+import pytest
+import torch
+import transformers
+from PIL import Image
+
+from ....pipelines.minimax_h3 import conditioning as c
+from ....pipelines.minimax_h3.packing import (
+ MINIMAX_H3_KEYFRAME_NOISE_AUG,
+ MINIMAX_H3_TEXT_TAG,
+ MINIMAX_H3_VIDEO_TAG,
+ build_packed_sequence,
+ patchify_video_latents,
+ prepare_keyframe_image,
+ resolve_canvas_size,
+)
+from ....pipelines.minimax_h3.pipeline_minimax_h3 import MiniMaxH3Pipeline, draw_request_latents
+from ....pipelines.minimax_h3.scheduler import MiniMaxH3Scheduler
+
+LATENT_CHANNELS = 24
+LATENT_HEIGHT, LATENT_WIDTH = 34, 60
+PATCH = (1, 2, 2)
+CONDITION_SHAPES = ((1, LATENT_HEIGHT, LATENT_WIDTH),) * 2
+
+LATENTS_MEAN = tuple(0.1 * index for index in range(LATENT_CHANNELS))
+LATENTS_STD = tuple(1.0 + 0.05 * index for index in range(LATENT_CHANNELS))
+
+
+def _latents(seed=5, frames=1):
+ return torch.randn(
+ 1, LATENT_CHANNELS, frames, LATENT_HEIGHT, LATENT_WIDTH, generator=torch.Generator().manual_seed(seed)
+ )
+
+
+def _image(height=544, width=960, seed=3):
+ rng = np.random.default_rng(seed)
+ return Image.fromarray(rng.integers(0, 256, (height, width, 3), dtype=np.uint8))
+
+
+def test_pixels_use_imagenet_statistics():
+ assert c.MINIMAX_H3_PIXEL_MEAN == (0.485, 0.456, 0.406)
+ assert c.MINIMAX_H3_PIXEL_STD == (0.229, 0.224, 0.225)
+
+ image = _image()
+ pixels = c.normalize_keyframe_pixels(image)
+ assert pixels.shape == (1, 3, 1, image.size[1], image.size[0])
+ assert pixels.dtype == torch.float32
+
+ mean = torch.tensor(c.MINIMAX_H3_PIXEL_MEAN).view(1, -1, 1, 1, 1)
+ std = torch.tensor(c.MINIMAX_H3_PIXEL_STD).view(1, -1, 1, 1, 1)
+ raw = torch.from_numpy(np.array(image)).permute(2, 0, 1)[None, :, None].float().div(255.0)
+ assert torch.equal(pixels, (raw - mean) / std)
+ assert pixels.min() < -1.0
+
+
+def test_posterior_seed_is_independent_of_request_seed():
+ assert c.MINIMAX_H3_KEYFRAME_ENCODE_SEED == 42
+ moments = torch.randn(1, 2 * LATENT_CHANNELS, 1, 8, 8, generator=torch.Generator().manual_seed(1))
+
+ torch.manual_seed(999)
+ first = c.sample_posterior(moments)
+ torch.manual_seed(12345)
+ second = c.sample_posterior(moments)
+ assert torch.equal(first, second)
+
+ assert not torch.equal(first, c.sample_posterior(moments, seed=43))
+
+ # a sample, not the mean -- the mean would also be request-independent
+ mean = moments.chunk(2, dim=1)[0]
+ assert first.shape == mean.shape
+ assert not torch.equal(first, mean)
+
+
+def test_float16_round_trip_is_load_bearing():
+ """The fp16 round trip is load-bearing; asserted to change the rows so it cannot be optimized away."""
+ latents = _latents()
+ rows = c.keyframe_condition_rows(latents, LATENTS_MEAN, LATENTS_STD, PATCH)
+
+ expected_rows = (LATENT_HEIGHT // 2) * (LATENT_WIDTH // 2)
+ assert rows.shape == (expected_rows, LATENT_CHANNELS * 4)
+ assert rows.dtype == torch.float32
+
+ mean = torch.tensor(LATENTS_MEAN).view(1, LATENT_CHANNELS, 1, 1, 1)
+ std = torch.tensor(LATENTS_STD).view(1, LATENT_CHANNELS, 1, 1, 1)
+ exact = patchify_video_latents((latents - mean) / std, PATCH)
+ rounded = patchify_video_latents((latents.to(torch.float16).float() - mean) / std, PATCH)
+
+ assert torch.equal(rows, rounded)
+ assert not torch.equal(rows, exact)
+ assert (rows - exact).abs().max() > 1e-5
+
+
+def test_encode_keyframes_is_one_encode_per_image():
+ seen = []
+
+ def encode_clip(pixels):
+ seen.append(tuple(pixels.shape))
+ batch, _, frames, height, width = pixels.shape
+ return torch.zeros(batch, 2 * LATENT_CHANNELS, frames, height // 16, width // 16)
+
+ images = [_image(seed=1), _image(seed=2)]
+ rows = c.encode_keyframes(images, encode_clip, LATENTS_MEAN, LATENTS_STD, PATCH)
+
+ assert len(seen) == len(images)
+ assert all(shape[2] == 1 for shape in seen)
+ rows_per_frame = (544 // 16 // 2) * (960 // 16 // 2)
+ assert rows.shape == (len(images) * rows_per_frame, LATENT_CHANNELS * 4)
+
+
+def test_condition_noise_is_deterministic_per_seed():
+ seed = 42
+ first = c.keyframe_condition_noise(CONDITION_SHAPES, LATENT_CHANNELS, PATCH, torch.Generator().manual_seed(seed))
+ second = c.keyframe_condition_noise(CONDITION_SHAPES, LATENT_CHANNELS, PATCH, torch.Generator().manual_seed(seed))
+ assert torch.equal(first, second)
+
+ rows_per_frame = (LATENT_HEIGHT // 2) * (LATENT_WIDTH // 2)
+ assert first.shape == (len(CONDITION_SHAPES) * rows_per_frame, LATENT_CHANNELS * 4)
+
+
+def test_condition_noise_draws_per_condition_in_order():
+ """One concatenated draw would consume the same values, assign them differently, and leave the same generator state."""
+ generator = torch.Generator().manual_seed(0)
+ both = c.keyframe_condition_noise(CONDITION_SHAPES, LATENT_CHANNELS, PATCH, generator)
+
+ per_condition = torch.cat(
+ [
+ c.keyframe_condition_noise((shape,), LATENT_CHANNELS, PATCH, torch.Generator().manual_seed(0))
+ for shape in CONDITION_SHAPES[:1]
+ ]
+ )
+ assert torch.equal(both[: per_condition.shape[0]], per_condition)
+
+
+def test_condition_noise_is_the_first_draw_of_a_request():
+ ordered = torch.Generator().manual_seed(0)
+ c.keyframe_condition_noise(CONDITION_SHAPES, LATENT_CHANNELS, PATCH, ordered)
+ video_after_conditioning = torch.randn(3, 64, generator=ordered)
+
+ skipped = torch.Generator().manual_seed(0)
+ video_without_conditioning = torch.randn(3, 64, generator=skipped)
+
+ assert not torch.equal(video_after_conditioning, video_without_conditioning)
+
+
+def test_noise_augmentation_has_no_second_implementation():
+ """A local t*x0 + (1-t)*noise drifted 2.4e-7 against the scheduler (Python-double 1-t)."""
+ assert not hasattr(c, "noise_augment")
+ assert MINIMAX_H3_KEYFRAME_NOISE_AUG == 0.999
+
+ rows = torch.randn(20, 96, generator=torch.Generator().manual_seed(1))
+ noise = torch.randn(20, 96, generator=torch.Generator().manual_seed(2))
+ out = MiniMaxH3Scheduler(12.0).scale_noise(rows, MINIMAX_H3_KEYFRAME_NOISE_AUG, noise)
+
+ timestep = torch.tensor(MINIMAX_H3_KEYFRAME_NOISE_AUG, dtype=torch.float32)
+ assert torch.equal(out, timestep * rows + (1.0 - timestep) * noise)
+ drifted = MINIMAX_H3_KEYFRAME_NOISE_AUG * rows + (1.0 - MINIMAX_H3_KEYFRAME_NOISE_AUG) * noise
+ assert not torch.equal(out, drifted)
+
+
+PROD_LATENT_FRAMES = 37
+PROD_LATENT_H, PROD_LATENT_W = 48, 84
+PROD_AUDIO_LATENTS = 207
+PROD_AUDIO_CHANNELS = 32
+PROD_ROWS_PER_FRAME = (PROD_LATENT_H // 2) * (PROD_LATENT_W // 2) # 1008
+
+
+def _draw(num_keyframes: int, seed: int = 0):
+ return draw_request_latents(
+ torch.Generator().manual_seed(seed),
+ condition_latent_shapes=((1, PROD_LATENT_H, PROD_LATENT_W),) * num_keyframes,
+ latent_channels=LATENT_CHANNELS,
+ num_latent_frames=PROD_LATENT_FRAMES,
+ latent_height=PROD_LATENT_H,
+ latent_width=PROD_LATENT_W,
+ num_audio_latents=PROD_AUDIO_LATENTS,
+ audio_latent_channels=PROD_AUDIO_CHANNELS,
+ patch_size=PATCH,
+ )
+
+
+def test_t2va_draws_are_unchanged_by_the_keyframe_argument():
+ """The t2va no-regression proof: nothing may be drawn before the video draw."""
+ condition_noise, video_rows, audio_rows = _draw(0)
+ assert condition_noise is None, "t2va must draw no conditioning noise at all"
+
+ generator = torch.Generator().manual_seed(0)
+ expected_video = patchify_video_latents(
+ torch.randn(
+ (1, LATENT_CHANNELS, PROD_LATENT_FRAMES, PROD_LATENT_H, PROD_LATENT_W),
+ generator=generator,
+ dtype=torch.float32,
+ ),
+ PATCH,
+ )
+ expected_audio = torch.randn(
+ (PROD_AUDIO_LATENTS * 2, PROD_AUDIO_CHANNELS), generator=generator, dtype=torch.float32
+ )
+
+ assert torch.equal(video_rows, expected_video), "t2va video draw moved"
+ assert torch.equal(audio_rows, expected_audio), "t2va audio draw moved"
+
+
+@pytest.mark.parametrize("num_keyframes", [1, 2], ids=["first", "first_and_last"])
+def test_conditioning_noise_is_drawn_before_video_and_audio(num_keyframes):
+ condition_noise, video_rows, audio_rows = _draw(num_keyframes)
+ _, t2va_video, t2va_audio = _draw(0)
+
+ assert condition_noise is not None
+ assert condition_noise.shape == (num_keyframes * PROD_ROWS_PER_FRAME, LATENT_CHANNELS * PATCH[1] * PATCH[2])
+ expected_noise = c.keyframe_condition_noise(
+ ((1, PROD_LATENT_H, PROD_LATENT_W),) * num_keyframes,
+ LATENT_CHANNELS,
+ PATCH,
+ torch.Generator().manual_seed(0),
+ )
+ assert torch.equal(condition_noise, expected_noise), "conditioning noise is not the first draw"
+
+ # fl2va at a given seed does NOT reproduce t2va at that seed -- expected, not a regression
+ assert not torch.equal(video_rows, t2va_video), "the conditioning draw did not advance the stream"
+ assert not torch.equal(audio_rows, t2va_audio)
+
+
+def test_keyframe_rows_per_anchor_match_the_layout():
+ text_tags = torch.ones(39, dtype=torch.long)
+ for anchors in (("first",), ("last",), ("first", "last")):
+ layout = build_packed_sequence(
+ text_tags, PROD_LATENT_FRAMES, PROD_LATENT_H, PROD_LATENT_W, PROD_AUDIO_LATENTS, PATCH, anchors
+ )
+ assert layout.num_condition_video_rows == len(anchors) * PROD_ROWS_PER_FRAME
+ assert layout.num_condition_audio_rows == 0, "fl2va conditions video only; audio conditioning is ref2va"
+ expected = torch.arange(39, 39 + layout.num_condition_video_rows)
+ assert torch.equal(layout.video_indices[: layout.num_condition_video_rows], expected)
+
+ condition_noise, _, _ = _draw(len(anchors))
+ assert condition_noise.shape[0] == layout.num_condition_video_rows
+
+
+PROMPT = (
+ "A red fox trots across a snowy field at dawn, its breath visible in the cold air. "
+ "The low sun throws long blue shadows behind it, and loose snow lifts from each footfall."
+)
+
+
+def _weights_dir() -> str:
+ base = os.environ.get("MINIMAX_H3_MODEL_PATH")
+ if not base:
+ pytest.skip("set MINIMAX_H3_MODEL_PATH to a MiniMax-H3 diffusers snapshot")
+ return os.path.join(base, "text_encoder")
+
+
+def _snapshot_root() -> str:
+ return os.path.dirname(_weights_dir())
+
+
+def _presentation(prompt, keyframes):
+ """The real method called unbound: a reimplementation here would gate a second copy of the presentation."""
+ root = _snapshot_root()
+ stub = SimpleNamespace(
+ tokenizer=transformers.AutoTokenizer.from_pretrained(root, subfolder="tokenizer"),
+ image_processor=transformers.AutoImageProcessor.from_pretrained(root, subfolder="text_encoder"),
+ )
+ return MiniMaxH3Pipeline._build_presentation(stub, prompt, keyframes), stub
+
+
+@pytest.mark.parametrize("num_keyframes", [1, 2], ids=["first", "first_and_last"])
+def test_fl2va_presentation_matches_the_reference(num_keyframes):
+ """Token ids, H3 tags (whole vision block video) and Qwen's mm_token_type_ids (pads only) each fail silently alone."""
+ source = Image.fromarray(
+ (torch.rand(720, 1280, 3, generator=torch.Generator().manual_seed(0)) * 255).to(torch.uint8).numpy()
+ )
+ height, width = resolve_canvas_size(*source.size)
+ assert (height, width) == (768, 1344), f"expected the production canvas, got {width}x{height}"
+ keyframes = [prepare_keyframe_image(source, height, width, stretch=(index == 0)) for index in range(num_keyframes)]
+
+ (input_ids, tags, type_ids, pixel_values, grid_thw), stub = _presentation(PROMPT, keyframes)
+ tokenizer, processor = stub.tokenizer, stub.image_processor
+ merge = processor.merge_size**2
+
+ # the reference's builder: `encoders.py::MiniMaxH3TextEncoderStep.encode_prompt`
+ ref_ids, ref_tags = [], []
+ for index in range(num_keyframes):
+ num_image_tokens = int(grid_thw[index].prod()) // merge
+ label = tokenizer(f": ", add_special_tokens=False)["input_ids"]
+ block = (
+ [tokenizer.convert_tokens_to_ids("<|vision_start|>")]
+ + [tokenizer.convert_tokens_to_ids("<|image_pad|>")] * num_image_tokens
+ + [tokenizer.convert_tokens_to_ids("<|vision_end|>")]
+ )
+ ref_ids += label + block
+ ref_tags += [MINIMAX_H3_TEXT_TAG] * len(label) + [MINIMAX_H3_VIDEO_TAG] * len(block)
+ prompt_ids = tokenizer(PROMPT, add_special_tokens=False)["input_ids"]
+ ref_ids += prompt_ids
+ ref_tags += [MINIMAX_H3_TEXT_TAG] * len(prompt_ids)
+
+ assert input_ids.shape == (1, len(ref_ids)), f"{tuple(input_ids.shape)} vs {(1, len(ref_ids))}"
+ assert input_ids[0].tolist() == ref_ids, "token ids differ from the reference presentation"
+ assert tags.tolist() == ref_tags, "H3 row tags differ from the reference presentation"
+
+ full_processor = transformers.AutoProcessor.from_pretrained(_snapshot_root(), subfolder="processor")
+ expected_type_ids = torch.tensor(full_processor.create_mm_token_type_ids([input_ids[0].tolist()]))
+ assert torch.equal(type_ids, expected_type_ids), "mm_token_type_ids differ from the processor's own"
+ assert int(type_ids.sum()) == num_keyframes * 1008
+
+ assert int((tags == MINIMAX_H3_VIDEO_TAG).sum()) == num_keyframes * (1008 + 2)
+ assert grid_thw.shape[0] == num_keyframes
+ assert pixel_values.shape[0] == num_keyframes * 1008 * merge
diff --git a/code/models/tt_dit/tests/models/minimax_h3/test_packing_minimax_h3.py b/code/models/tt_dit/tests/models/minimax_h3/test_packing_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..e3c258bcd4eae26e1d5fc696fb99b541888fea10
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/test_packing_minimax_h3.py
@@ -0,0 +1,311 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Host-only parity gate for the MiniMax-H3 packed sequence: checkpoint contracts, exact
+assertions (drift is silent). Golden digests come from a run verified bit-exact against the
+minimax-h3 diffusers branch; when that branch is installed the layout is re-checked directly."""
+
+import hashlib
+
+import pytest
+import torch
+
+from ....pipelines.minimax_h3 import packing as p
+
+# (label, latent_height, latent_width, num_frames, keyframe_anchors)
+BRINGUP = ("bringup", 544 // 16, 960 // 16, 124, ("first", "last"))
+CANONICAL = ("canonical", 768 // 16, 1344 // 16, 192, ("first",))
+T2VA = ("t2va_768p_5s", 768 // 16, 1344 // 16, 124, ())
+
+# sequence_length, latent frames, audio latents, sha256[:16] of position_ids and token_tags
+GOLDEN = {
+ "bringup": (21301, 37, 207, "dcd77044930c7e5f", "320f230cf65b8e0b"),
+ "canonical": (60101, 57, 320, "3d2878587ddfdc7d", "bc71672198e60490"),
+}
+
+TEXT_LEN = 997
+VISION_BLOCK = slice(20, 70) # a keyframe's vision block: text rows tagged video
+
+
+def _text_tags():
+ tags = torch.ones(TEXT_LEN, dtype=torch.long)
+ tags[VISION_BLOCK] = p.MINIMAX_H3_VIDEO_TAG
+ return tags
+
+
+def _layout(latent_height, latent_width, num_frames, anchors):
+ return p.build_packed_sequence(
+ _text_tags(),
+ p.video_latent_num_frames(num_frames),
+ latent_height,
+ latent_width,
+ p.audio_latent_num_frames(num_frames),
+ (1, 2, 2),
+ anchors,
+ )
+
+
+def _digest(tensor):
+ return hashlib.sha256(tensor.contiguous().numpy().tobytes()).hexdigest()[:16]
+
+
+@pytest.mark.parametrize("num_frames,expected", [(1, 5), (5, 5), (6, 22), (22, 22), (120, 124), (192, 192)])
+def test_align_num_frames(num_frames, expected):
+ assert p.align_num_frames(num_frames) == expected
+
+
+@pytest.mark.parametrize("num_frames,latents", [(5, 2), (22, 7), (39, 12), (124, 37), (192, 57), (362, 107)])
+def test_video_latent_num_frames(num_frames, latents):
+ assert p.video_latent_num_frames(num_frames) == latents
+
+
+def test_video_latent_num_frames_rejects_unaligned(expect_error):
+ with expect_error(ValueError, "17n \\+ 5"):
+ p.video_latent_num_frames(120)
+
+
+@pytest.mark.parametrize("num_frames,latents", [(124, 207), (192, 320), (362, 603)])
+def test_audio_latent_num_frames(num_frames, latents):
+ assert p.audio_latent_num_frames(num_frames) == latents
+
+
+@pytest.mark.parametrize(
+ "aspect,canvas",
+ [
+ ((16, 9), (768, 1344)),
+ ((9, 16), (1344, 768)),
+ ((1, 1), (768, 768)),
+ ((4, 3), (768, 1024)),
+ ],
+)
+def test_resolve_canvas_size(aspect, canvas):
+ assert p.resolve_canvas_size(*aspect) == canvas
+ assert canvas[0] % p.MINIMAX_H3_CANVAS_MULTIPLE == 0
+ assert canvas[1] % p.MINIMAX_H3_CANVAS_MULTIPLE == 0
+
+
+@pytest.mark.parametrize(
+ "aspect,message",
+ [
+ ((5, 1), "aspect ratios from 1:4 to 4:1"),
+ ((1, 5), "aspect ratios from 1:4 to 4:1"),
+ ((0, 1), "must be positive"),
+ ((-16, 9), "must be positive"),
+ ],
+)
+def test_resolve_canvas_size_rejects_out_of_range(aspect, message, expect_error):
+ with expect_error(ValueError, message):
+ p.resolve_canvas_size(*aspect)
+
+
+@pytest.mark.parametrize("case", [BRINGUP, CANONICAL], ids=lambda c: c[0])
+def test_layout_matches_golden(case):
+ label, latent_height, latent_width, num_frames, anchors = case
+ sequence_length, latent_frames, audio_latents, position_sha, tag_sha = GOLDEN[label]
+
+ assert p.video_latent_num_frames(num_frames) == latent_frames
+ assert p.audio_latent_num_frames(num_frames) == audio_latents
+
+ layout = _layout(latent_height, latent_width, num_frames, anchors)
+ assert layout.sequence_length == sequence_length
+ assert layout.position_ids.dtype == torch.float64
+ assert _digest(layout.position_ids) == position_sha
+ assert _digest(layout.token_tags) == tag_sha
+
+
+@pytest.mark.parametrize("case", [BRINGUP, CANONICAL], ids=lambda c: c[0])
+def test_layout_structure(case):
+ _, latent_height, latent_width, num_frames, anchors = case
+ layout = _layout(latent_height, latent_width, num_frames, anchors)
+ rows_per_frame = (latent_height // 2) * (latent_width // 2)
+
+ assert layout.num_condition_video_rows == len(anchors) * rows_per_frame
+ assert layout.num_condition_audio_rows == 0
+
+ covered = torch.cat([layout.text_indices, layout.video_indices, layout.audio_indices]).sort().values
+ assert torch.equal(covered, torch.arange(layout.sequence_length))
+
+ assert torch.equal(layout.token_tags[layout.audio_indices[:1]], torch.tensor([p.MINIMAX_H3_AUDIO_TAG]))
+ assert (layout.token_tags[layout.video_indices] == p.MINIMAX_H3_VIDEO_TAG).all()
+ assert (layout.token_tags[VISION_BLOCK] == p.MINIMAX_H3_VIDEO_TAG).all()
+ assert (layout.token_tags[VISION_BLOCK.stop : TEXT_LEN] == p.MINIMAX_H3_TEXT_TAG).all()
+
+ audio_position_ids = layout.position_ids[layout.audio_indices]
+ assert (audio_position_ids[:, 1] == 0).all()
+ assert audio_position_ids[:, 2].unique().numel() == 2
+
+
+def test_keyframe_anchor_times():
+ """The `last` anchor overshoots the final frame by 5.0 on purpose -- looks like an off-by-one and is not."""
+ _, latent_height, latent_width, num_frames, _ = BRINGUP
+ latent_frames = p.video_latent_num_frames(num_frames)
+ layout = _layout(latent_height, latent_width, num_frames, ("first", "last"))
+ rows_per_frame = (latent_height // 2) * (latent_width // 2)
+
+ first_time = layout.position_ids[TEXT_LEN, 0]
+ last_time = layout.position_ids[TEXT_LEN + rows_per_frame, 0]
+ video_time = layout.position_ids[layout.video_indices[layout.num_condition_video_rows :], 0]
+
+ assert first_time == float(TEXT_LEN)
+ assert first_time == video_time.min()
+ assert float(last_time) == pytest.approx(
+ TEXT_LEN + p._temporal_position_span(latent_frames) - p._ROPE_FRAME_RESCALE
+ )
+ assert last_time > video_time.max()
+ assert float(last_time - video_time.max()) == pytest.approx(5.0)
+
+
+@pytest.mark.parametrize("case", [BRINGUP, CANONICAL], ids=lambda c: c[0])
+def test_row_timesteps_pin_condition_rows(case):
+ _, latent_height, latent_width, num_frames, anchors = case
+ layout = _layout(latent_height, latent_width, num_frames, anchors)
+ # expectations round through fp32: 0.7 is not representable
+ video_t, audio_t, cond_t = torch.tensor([0.7, 0.5, p.MINIMAX_H3_KEYFRAME_NOISE_AUG], dtype=torch.float32).tolist()
+ timesteps, indices = p.build_row_timesteps(layout, video_t, audio_t, cond_t, cond_t)
+
+ assert timesteps.tolist() == sorted({video_t, audio_t, cond_t})
+ resolved = timesteps[indices]
+ condition_rows = layout.video_indices[: layout.num_condition_video_rows]
+ target_rows = layout.video_indices[layout.num_condition_video_rows :]
+
+ assert (resolved[condition_rows] == cond_t).all()
+ assert (resolved[target_rows] == video_t).all()
+ assert (resolved[layout.audio_indices] == audio_t).all()
+ assert (resolved[layout.text_indices] == video_t).all()
+
+
+@pytest.mark.parametrize("case", [BRINGUP, CANONICAL], ids=lambda c: c[0])
+def test_adaln_index_ranges_round_trip(case):
+ _, latent_height, latent_width, num_frames, anchors = case
+ layout = _layout(latent_height, latent_width, num_frames, anchors)
+ _, indices = p.build_row_timesteps(layout, 0.7, 0.5, 0.999, 0.999)
+ table_rows = p.adaln_indices(layout.token_tags, indices)
+
+ runs = p.adaln_index_ranges(table_rows)
+ rebuilt = torch.empty_like(table_rows)
+ for start, stop, value in runs:
+ rebuilt[start:stop] = value
+ assert torch.equal(rebuilt, table_rows)
+ assert len(runs) <= 8
+
+
+def test_adaln_index_ranges_are_shard_local():
+ _, latent_height, latent_width, num_frames, anchors = CANONICAL
+ layout = _layout(latent_height, latent_width, num_frames, anchors)
+ _, indices = p.build_row_timesteps(layout, 0.7, 0.5, 0.999, 0.999)
+ table_rows = p.adaln_indices(layout.token_tags, indices)
+
+ sp_factor = 8
+ shard = layout.sequence_length // sp_factor
+ counts = [len(p.adaln_index_ranges(table_rows[i * shard : (i + 1) * shard])) for i in range(sp_factor)]
+
+ assert sum(count == 1 for count in counts) >= sp_factor - 2
+ assert max(counts) <= 8
+
+
+def test_adaln_index_ranges_empty():
+ assert p.adaln_index_ranges(torch.empty(0, dtype=torch.long)) == []
+
+
+@pytest.mark.parametrize("case", [BRINGUP, CANONICAL], ids=lambda c: c[0])
+def test_patchify_round_trip(case):
+ _, latent_height, latent_width, num_frames, _ = case
+ latent_frames = p.video_latent_num_frames(num_frames)
+ latents = torch.randn(1, 24, latent_frames, latent_height, latent_width)
+
+ rows = p.patchify_video_latents(latents, (1, 2, 2))
+ assert rows.shape == (latent_frames * (latent_height // 2) * (latent_width // 2), 24 * 4)
+ restored = p.unpatchify_video_tokens(rows, latent_frames, latent_height, latent_width, 24, (1, 2, 2))
+ assert torch.equal(restored, latents)
+
+
+def test_unpack_audio_tokens_is_channel_major():
+ num_audio_latents, channels = 207, p.MINIMAX_H3_AUDIO_CHANNELS
+ rows = torch.randn(num_audio_latents * channels, 32)
+ unpacked = p.unpack_audio_tokens(rows, num_audio_latents)
+
+ assert unpacked.shape == (channels, 32, num_audio_latents)
+ assert torch.equal(unpacked[0], rows[:num_audio_latents].transpose(0, 1))
+ assert torch.equal(unpacked[1], rows[num_audio_latents:].transpose(0, 1))
+
+
+ROPE_FREQ_DIM = 16
+ROPE_THETA = 10000.0
+
+
+@pytest.mark.parametrize("case", [BRINGUP, CANONICAL, T2VA], ids=lambda c: c[0])
+def test_rope_tables_match_reference(case):
+ """torch.equal, not PCC: merely-close is the regression."""
+ rope_module = pytest.importorskip(
+ "diffusers.models.transformers.transformer_minimax_h3",
+ reason="requires the minimax-h3 diffusers branch",
+ )
+ _, latent_height, latent_width, num_frames, anchors = case
+ layout = _layout(latent_height, latent_width, num_frames, anchors)
+
+ reference = rope_module.MiniMaxH3RotaryPosEmbed(rope_freq_dim=ROPE_FREQ_DIM, rope_theta=ROPE_THETA)
+ with torch.no_grad():
+ want_cos, want_sin = reference(layout.position_ids)
+ got_cos, got_sin = p.build_rope_tables(layout.position_ids, rope_freq_dim=ROPE_FREQ_DIM, rope_theta=ROPE_THETA)
+
+ assert torch.equal(got_cos, want_cos), "rope cos is not bit-exact against the reference"
+ assert torch.equal(got_sin, want_sin), "rope sin is not bit-exact against the reference"
+
+
+@pytest.mark.parametrize("case", [BRINGUP, CANONICAL, T2VA], ids=lambda c: c[0])
+def test_rope_tables_shape_and_dtype(case):
+ _, latent_height, latent_width, num_frames, anchors = case
+ layout = _layout(latent_height, latent_width, num_frames, anchors)
+ cos, sin = p.build_rope_tables(layout.position_ids, rope_freq_dim=ROPE_FREQ_DIM, rope_theta=ROPE_THETA)
+
+ assert cos.shape == sin.shape == (layout.sequence_length, 2 * 3 * ROPE_FREQ_DIM)
+ assert cos.shape[-1] == 96 < 128, "rotary width must stay narrower than head_dim"
+ assert cos.dtype == sin.dtype == torch.float32
+ assert torch.isfinite(cos).all() and torch.isfinite(sin).all()
+ half = cos.shape[-1] // 2
+ assert torch.equal(cos[:, :half], cos[:, half:])
+ assert torch.equal(sin[:, :half], sin[:, half:])
+
+
+def test_rope_tables_distinguish_the_three_axes():
+ """A collapse to 1-D rope would look like weak spatial coherence."""
+ _, latent_height, latent_width, num_frames, anchors = T2VA
+ layout = _layout(latent_height, latent_width, num_frames, anchors)
+ cos, _ = p.build_rope_tables(layout.position_ids, rope_freq_dim=ROPE_FREQ_DIM, rope_theta=ROPE_THETA)
+
+ row = int(layout.video_indices[-1])
+ t_block, h_block, w_block = (
+ cos[row, :ROPE_FREQ_DIM],
+ cos[row, ROPE_FREQ_DIM : 2 * ROPE_FREQ_DIM],
+ cos[row, 2 * ROPE_FREQ_DIM : 3 * ROPE_FREQ_DIM],
+ )
+ assert not torch.equal(t_block, h_block)
+ assert not torch.equal(h_block, w_block)
+
+
+@pytest.mark.parametrize("case", [BRINGUP, CANONICAL], ids=lambda c: c[0])
+def test_matches_diffusers_reference(case):
+ reference = pytest.importorskip(
+ "diffusers.modular_pipelines.minimax_h3.packing",
+ reason="requires the minimax-h3 diffusers branch",
+ )
+ _, latent_height, latent_width, num_frames, anchors = case
+ kwargs = dict(
+ text_token_tags=_text_tags(),
+ num_latent_frames=p.video_latent_num_frames(num_frames),
+ latent_height=latent_height,
+ latent_width=latent_width,
+ num_audio_latents=p.audio_latent_num_frames(num_frames),
+ patch_size=(1, 2, 2),
+ keyframe_anchors=anchors,
+ )
+ ours = p.build_packed_sequence(**kwargs)
+ theirs = reference.build_packed_sequence(**kwargs)
+
+ assert ours.sequence_length == theirs.sequence_length
+ assert torch.equal(ours.position_ids, theirs.position_ids)
+ assert torch.equal(ours.token_tags, theirs.token_tags)
+ assert torch.equal(ours.video_indices, theirs.video_indices)
+ assert torch.equal(ours.audio_indices, theirs.audio_indices)
+ assert ours.num_condition_video_rows == theirs.num_condition_video_rows
diff --git a/code/models/tt_dit/tests/models/minimax_h3/test_performance_minimax_h3.py b/code/models/tt_dit/tests/models/minimax_h3/test_performance_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd590a9f648ab9d108d74801389a98faf1e1a567
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/test_performance_minimax_h3.py
@@ -0,0 +1,222 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Device perf of one MiniMax-H3 transformer block at 768P; run under `scripts/run_safe_pytest.sh --profile`,
+then `tt-perf-report --start-signpost start --end-signpost stop`. Profile one duration at a time with
+`-k`: a multi-parameter profiled run yields a CSV containing only the first parameter's ops."""
+
+from __future__ import annotations
+
+import pytest
+import torch
+from diffusers.models.transformers.transformer_minimax_h3 import MINIMAX_H3_MODALITY_NUM, MiniMaxH3RotaryPosEmbed
+from diffusers.models.transformers.transformer_minimax_h3 import MiniMaxH3TransformerBlock as TorchMiniMaxH3Block
+from loguru import logger
+from tracy import signpost
+
+import ttnn
+
+from ....models.transformers.minimax_h3.attention_minimax_h3 import prepare_rope_tables
+from ....models.transformers.minimax_h3.transformer_block_minimax_h3 import MiniMaxH3TransformerBlock
+from ....parallel.config import DiTParallelConfig, ParallelFactor
+from ....parallel.manager import CCLManager
+from ....pipelines.minimax_h3.packing import (
+ MINIMAX_H3_FPS,
+ align_num_frames,
+ audio_latent_num_frames,
+ resolve_canvas_size,
+ video_latent_num_frames,
+)
+from ....utils.tensor import bf16_tensor_2dshard, from_torch, local_device_to_torch
+from ....utils.test import skip_if_unsupported_num_links
+from .common import (
+ GALAXY_RING,
+ REAL_BLOCK_CONFIG,
+ ROPE_FREQ_DIM,
+ ROPE_THETA,
+ TT_BLOCK_CONFIG,
+ packed_layout,
+ upload_rope,
+)
+
+HIDDEN_SIZE = REAL_BLOCK_CONFIG["hidden_size"]
+HEAD_DIM = REAL_BLOCK_CONFIG["attention_head_dim"]
+TIME_EMBED_DIM = REAL_BLOCK_CONFIG["time_embed_dim"]
+
+PATCH_SIZE = (1, 2, 2)
+VAE_SPATIAL_DOWNSAMPLE = 16 # prod(spatial_downsample_factors) from the video VAE config
+NUM_TEXT_TOKENS = 512
+ASPECT = (16, 9)
+
+
+def _packed_sizes(duration_s: float) -> dict:
+ """Token counts for `duration_s` seconds of 768P video, derived from the pipeline's own packing helpers."""
+ height, width = resolve_canvas_size(*ASPECT)
+ tokens_per_latent_frame = (height // VAE_SPATIAL_DOWNSAMPLE // PATCH_SIZE[1]) * (
+ width // VAE_SPATIAL_DOWNSAMPLE // PATCH_SIZE[2]
+ )
+ num_frames = align_num_frames(int(duration_s * MINIMAX_H3_FPS))
+ latent_frames = video_latent_num_frames(num_frames)
+ num_audio = audio_latent_num_frames(num_frames)
+ num_video = latent_frames * tokens_per_latent_frame
+ return {
+ "height": height,
+ "width": width,
+ "num_frames": num_frames,
+ "latent_frames": latent_frames,
+ "grid_h": height // VAE_SPATIAL_DOWNSAMPLE // PATCH_SIZE[1],
+ "grid_w": width // VAE_SPATIAL_DOWNSAMPLE // PATCH_SIZE[2],
+ "num_video": num_video,
+ "num_audio": num_audio,
+ "num_text": NUM_TEXT_TOKENS,
+ "seq_len": NUM_TEXT_TOKENS + num_audio + num_video,
+ }
+
+
+@GALAXY_RING
+@pytest.mark.parametrize(
+ "duration_s",
+ [
+ pytest.param(5.0, id="5s_768p"),
+ pytest.param(10.0, id="10s_768p"),
+ pytest.param(15.0, id="15s_768p"),
+ ],
+)
+@pytest.mark.parametrize(
+ "sp_simulate",
+ [
+ pytest.param(1, id="sp_sim1"),
+ pytest.param(4, id="sp_sim4"),
+ ],
+)
+def test_minimax_h3_transformer_block_perf(
+ mesh_device: ttnn.MeshDevice,
+ sp_axis: int,
+ tp_axis: int,
+ num_links: int,
+ duration_s: float,
+ sp_simulate: int,
+ is_fsdp: bool,
+ topology: ttnn.Topology,
+ reset_seeds,
+) -> None:
+ skip_if_unsupported_num_links(mesh_device, num_links)
+ # Simulate a larger SP mesh (e.g. 4x32) on a smaller one (4x8) by shrinking the total sequence
+ # so each device carries a shard the larger mesh would produce. `sp_simulate` is that SP ratio.
+ SIM = sp_simulate
+
+ sp_factor = tuple(mesh_device.shape)[sp_axis]
+ tp_factor = tuple(mesh_device.shape)[tp_axis]
+
+ sizes = _packed_sizes(duration_s)
+ seq_len = sizes["seq_len"]
+ alignment = sp_factor * ttnn.TILE_SIZE * SIM
+ padded_len = ((seq_len + alignment - 1) // alignment) * alignment
+ padded_len = padded_len // SIM
+ logger.info(
+ f"{duration_s:g}s @ {sizes['height']}x{sizes['width']}: {sizes['num_frames']} frames -> "
+ f"{sizes['latent_frames']} latent frames x {sizes['grid_h']}x{sizes['grid_w']} patches = "
+ f"{sizes['num_video']} video + {sizes['num_audio']} audio + {sizes['num_text']} text "
+ f"= seq_len {seq_len} (padded {padded_len}, {padded_len // sp_factor} rows/device)"
+ )
+
+ num_timesteps = 2
+ # Simulate a 4x32 per-device shard on a 4x8 mesh: shrink the total sequence by SIM (32/8) so each
+ # 4x8 device carries a 4x32-sized shard. num_video must stay a whole number of (grid_h, grid_w)
+ # frames, so floor it to a frame boundary rather than dividing the raw token count.
+ frame = sizes["grid_h"] * sizes["grid_w"]
+ sim_num_video = (sizes["num_video"] // SIM // frame) * frame
+ sim_seq_len = sizes["num_text"] // SIM + sizes["num_audio"] // SIM + sim_num_video
+ position_ids, tags, timestep_indices = packed_layout(
+ sizes["num_text"] // SIM,
+ sizes["num_audio"] // SIM,
+ sim_num_video,
+ (sizes["grid_h"], sizes["grid_w"]),
+ padded_len,
+ )
+ adaln_indices = timestep_indices * MINIMAX_H3_MODALITY_NUM + tags.clamp(min=0)
+
+ # Built only to source a correctly-keyed random state dict; its forward is never called.
+ torch_block = TorchMiniMaxH3Block(**REAL_BLOCK_CONFIG).to(torch.float32)
+
+ rope = MiniMaxH3RotaryPosEmbed(rope_freq_dim=ROPE_FREQ_DIM, rope_theta=ROPE_THETA)
+ with torch.no_grad():
+ rope_cos, rope_sin = rope(position_ids)
+ rope_cos, rope_sin = prepare_rope_tables(rope_cos, rope_sin, HEAD_DIM)
+
+ ccl_manager = CCLManager(mesh_device=mesh_device, num_links=num_links, topology=topology)
+ parallel_config = DiTParallelConfig(
+ tensor_parallel=ParallelFactor(mesh_axis=tp_axis, factor=tp_factor),
+ sequence_parallel=ParallelFactor(mesh_axis=sp_axis, factor=sp_factor),
+ cfg_parallel=None,
+ )
+
+ tt_block = MiniMaxH3TransformerBlock(
+ **TT_BLOCK_CONFIG,
+ rotary_dim=2 * 3 * ROPE_FREQ_DIM,
+ mesh_device=mesh_device,
+ ccl_manager=ccl_manager,
+ parallel_config=parallel_config,
+ is_fsdp=is_fsdp,
+ )
+ tt_block.load_torch_state_dict(torch_block.state_dict())
+ del torch_block
+
+ if SIM > 1:
+ # The exp-ring gate keys on sequence_parallel.factor == 32, a proxy for "the per-device
+ # shard is the 4x32 shape". SP simulation produces exactly that shard on a smaller mesh,
+ # but the model only sees the real mesh's factor — force the flag so the simulated run
+ # exercises what a real 4x32 would.
+ tt_block.attn.use_exp_ring_sdpa = tt_block.attn.use_ring and sp_factor * SIM == 32
+ if sp_factor * SIM == 32:
+ assert tt_block.attn.use_exp_ring_sdpa, "simulated 4x32 run must exercise the exp ring SDPA path"
+ assert tt_block.attn._exp_sdpa_program_config(padded_len // sp_factor) is not None, (
+ "exp ring SDPA has no valid program config for the simulated shard "
+ f"({padded_len // sp_factor} tokens/device); the run would silently measure the normal ring path"
+ )
+
+ tt_spatial = bf16_tensor_2dshard(
+ torch.randn(1, 1, padded_len, HIDDEN_SIZE),
+ device=mesh_device,
+ shard_mapping={sp_axis: 2, tp_axis: 3},
+ )
+ tt_temb = from_torch(torch.randn(1, 1, num_timesteps, TIME_EMBED_DIM), device=mesh_device, dtype=ttnn.float32)
+ tt_adaln = from_torch(
+ adaln_indices.to(torch.int32).reshape(1, 1, 1, padded_len),
+ device=mesh_device,
+ dtype=ttnn.int32,
+ layout=ttnn.Layout.ROW_MAJOR,
+ mesh_axes=[..., None, sp_axis],
+ )
+ tt_rope_cos, tt_rope_sin = upload_rope(rope_cos, rope_sin, mesh_device=mesh_device, sp_axis=sp_axis)
+
+ def run_block() -> ttnn.Tensor:
+ out = tt_block(
+ tt_spatial,
+ N=sim_seq_len, # simulated unpadded length: ring attention masks the pad tail via logical_n
+ temb=tt_temb,
+ adaln_indices=tt_adaln,
+ rope_cos=tt_rope_cos,
+ rope_sin=tt_rope_sin,
+ )
+ ttnn.synchronize_device(mesh_device)
+ return out
+
+ logger.info("iteration 1: compiling kernels and populating the program cache")
+ run_block()
+
+ logger.info("iteration 2: warm run (the profiled region)")
+ signpost("start")
+ tt_out = run_block()
+ signpost("stop")
+
+ assert tuple(tt_out.shape) == (
+ 1,
+ 1,
+ padded_len // sp_factor,
+ HIDDEN_SIZE // tp_factor,
+ ), f"unexpected output shape {tuple(tt_out.shape)}"
+ local = local_device_to_torch(tt_out).float()
+ assert torch.isfinite(local).all(), "block output contains NaN or Inf"
+ logger.info(f"output {tuple(tt_out.shape)}, local shard std={local.std().item():.4f}")
diff --git a/code/models/tt_dit/tests/models/minimax_h3/test_performance_vae_minimax_h3.py b/code/models/tt_dit/tests/models/minimax_h3/test_performance_vae_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae9baf07011bcdb474d62d5e1cba2dd1c5ce433b
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/test_performance_vae_minimax_h3.py
@@ -0,0 +1,327 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""Gate M8f: MiniMax-H3 VAE performance baselines, gated against ``EXPECTED_METRICS`` budgets
+(style of ``tests/models/wan2_2/test_performance_wan.py``). Tracy capture entry points live
+in ``tools/tracy_decode_harness.py``; quality gates live in the vae/audio test files."""
+
+import os
+import time
+
+import pytest
+import torch
+
+import ttnn
+
+from ....models.audio_vae.minimax_h3.convert_minimax_h3_audio import convert_minimax_h3_audio_state_dict
+from ....models.audio_vae.minimax_h3.encoder_minimax_h3_audio import MiniMaxH3AudioEncoder
+from ....models.vae.minimax_h3.vae_minimax_h3 import MiniMaxH3Vae, MiniMaxH3VaeConfig
+from ....parallel.manager import CCLManager
+from .common import (
+ CLIP_FRAMES,
+ DECODE_LATENT_FRAMES,
+ LATENT_TILE,
+ TILE,
+ build_audio_decoder,
+ build_visual_decoder,
+ build_visual_encoder,
+ load_config,
+ random_decoder_state,
+ random_encoder_state,
+ weights_subdir,
+)
+
+SINGLE_DEVICE = [pytest.param((1, 1), {"l1_small_size": 65536}, id="single_device")]
+
+HOP_LENGTH = 800
+
+# Work units per working point: 768P = 4x7 tiles, 1440P = 8x13; clip/chunk counts per the 17n+5 -> 5n+2 rule.
+WORK_UNITS = {
+ "768P_5s": {"tiles": 28, "encode_clips": 8, "decode_chunks": 7},
+ "768P_10s": {"tiles": 28, "encode_clips": 15, "decode_chunks": 14},
+ "768P_15s": {"tiles": 28, "encode_clips": 22, "decode_chunks": 21},
+ "1440P_5s": {"tiles": 104, "encode_clips": 8, "decode_chunks": 7},
+ "1440P_10s": {"tiles": 104, "encode_clips": 15, "decode_chunks": 14},
+ "1440P_15s": {"tiles": 104, "encode_clips": 22, "decode_chunks": 21},
+}
+
+AUDIO_LATENT_FRAMES_5S = 207 # ~5 s at 40 latent fps; budgets were calibrated at 207, not the exact 200
+
+# Seconds per measurement. Generous regression bars, not tuned targets; visual entries gate a 32-unit wave.
+# Audio decode runs the accurate-mode defaults (~13 s eager, ~3x the retired fast path); 60 s stays generous.
+EXPECTED_METRICS = {
+ "visual_encoder_clip_wave": 20.0,
+ "visual_encoder_keyframe_wave": 5.0,
+ "visual_decoder_wave": 20.0,
+ "audio_encode_5s": 60.0,
+ "audio_decode_5s": 60.0,
+}
+
+
+def _time_it(fn, *, warmup: int = 1, iterations: int = 2, mesh_device=None) -> float:
+ for _ in range(warmup):
+ fn()
+ if mesh_device is not None:
+ ttnn.synchronize_device(mesh_device)
+ best = float("inf")
+ for _ in range(iterations):
+ start = time.time()
+ fn()
+ if mesh_device is not None:
+ ttnn.synchronize_device(mesh_device)
+ best = min(best, time.time() - start)
+ return best
+
+
+def _report(measurements: dict[str, float]) -> None:
+ from loguru import logger
+
+ failures = []
+ for key, seconds in measurements.items():
+ budget = EXPECTED_METRICS[key]
+ logger.info(f"PERF {key}: {seconds:.3f} s (budget {budget:.1f} s)")
+ if seconds > budget:
+ failures.append(f"{key} took {seconds:.3f} s, budget {budget:.1f} s")
+ assert not failures, "\n".join(failures)
+
+
+MESH_4X8 = [
+ pytest.param(
+ (4, 8),
+ {"fabric_config": None, "require_exact_physical_num_devices": True, "l1_small_size": 65536},
+ id="mesh4x8",
+ )
+]
+
+
+@pytest.mark.parametrize(("mesh_device", "device_params"), MESH_4X8, indirect=["mesh_device", "device_params"])
+def test_visual_data_parallel_throughput(mesh_device):
+ """Per-wave time with one work unit per device, gated, plus full-video projections."""
+ from loguru import logger
+
+ weights_dir = weights_subdir("vae")
+ if weights_dir is None:
+ pytest.skip("MiniMax-H3 vae not found; set MINIMAX_H3_MODEL_PATH")
+ config = load_config(weights_dir)
+ devices = mesh_device.get_num_devices()
+ torch.manual_seed(0)
+
+ measurements = {}
+
+ for label, num_frames, taps in (
+ ("visual_encoder_clip_wave", CLIP_FRAMES, 3),
+ ("visual_encoder_keyframe_wave", 1, 1),
+ ):
+ encoder = build_visual_encoder(config, mesh_device, num_frames, temporal_taps=taps)
+ encoder.load_torch_state_dict(random_encoder_state(config))
+ x = torch.randn(devices, num_frames, TILE, TILE, encoder.conv_in.in_channels)
+ x_device = ttnn.from_torch(
+ x,
+ dtype=ttnn.float32,
+ device=mesh_device,
+ layout=ttnn.ROW_MAJOR_LAYOUT,
+ mesh_mapper=ttnn.ShardTensorToMesh(mesh_device, dim=0),
+ )
+ seconds = _time_it(lambda: encoder(x_device), iterations=8, mesh_device=mesh_device)
+ logger.info(f"PERF {label} of {devices} units: {seconds:.3f} s ({seconds / devices:.4f} s/unit)")
+ measurements[label] = seconds
+
+ decoder = build_visual_decoder(config, mesh_device)
+ decoder.load_torch_state_dict(random_decoder_state(config))
+ tokens = torch.randn(devices, DECODE_LATENT_FRAMES * LATENT_TILE * LATENT_TILE, config["latent_channels"])
+ tokens_device = ttnn.from_torch(
+ tokens,
+ dtype=ttnn.bfloat16,
+ device=mesh_device,
+ layout=ttnn.TILE_LAYOUT,
+ mesh_mapper=ttnn.ShardTensorToMesh(mesh_device, dim=0),
+ )
+ decoder_wave = _time_it(lambda: decoder(tokens_device), iterations=8, mesh_device=mesh_device)
+ # 10.6 = TFLOP per invocation at 1797 tokens x 36 layers x dim 2048.
+ logger.info(
+ f"PERF visual_decoder_wave of {devices} units: {decoder_wave:.3f} s "
+ f"({decoder_wave / devices:.4f} s/unit, {10.6 / (decoder_wave / devices):.1f} TFLOP/s effective per unit)"
+ )
+ measurements["visual_decoder_wave"] = decoder_wave
+
+ encoder_wave = measurements["visual_encoder_clip_wave"]
+ for name, units in WORK_UNITS.items():
+ encode_units = units["tiles"] * units["encode_clips"]
+ decode_units = units["tiles"] * units["decode_chunks"]
+ encode_waves = -(-encode_units // devices)
+ decode_waves = -(-decode_units // devices)
+ encode_total = encode_waves * encoder_wave
+ decode_total = decode_waves * decoder_wave
+ logger.info(
+ f"PROJECTED {name}: encode {encode_units} units / {encode_waves} waves = {encode_total:.1f} s, "
+ f"decode {decode_units} units / {decode_waves} waves = {decode_total:.1f} s, "
+ f"total {encode_total + decode_total:.1f} s"
+ )
+
+ _report(measurements)
+
+
+@pytest.mark.parametrize(("mesh_device", "device_params"), SINGLE_DEVICE, indirect=["mesh_device", "device_params"])
+def test_audio_baselines(mesh_device):
+ """Audio encode/decode timing baselines at 5 s, against their budgets."""
+ weights_dir = weights_subdir("audio_vae")
+ if weights_dir is None:
+ pytest.skip("MiniMax-H3 audio_vae not found; set MINIMAX_H3_MODEL_PATH")
+ from safetensors.torch import load_file
+
+ config = load_config(weights_dir)
+ converted = convert_minimax_h3_audio_state_dict(
+ load_file(os.path.join(weights_dir, "diffusion_pytorch_model.safetensors"))
+ )
+
+ torch.manual_seed(2)
+ waveform = torch.randn(2, 1, AUDIO_LATENT_FRAMES_5S * HOP_LENGTH) * 0.1
+ latents = torch.randn(2, config["latent_channels"], AUDIO_LATENT_FRAMES_5S) * 0.1
+
+ encoder = MiniMaxH3AudioEncoder(
+ encoder_dim=config["encoder_dim"],
+ encoder_rates=tuple(config["encoder_rates"]),
+ latent_dim=config["latent_dim"],
+ latent_channels=config["latent_channels"],
+ num_attention_heads=config["num_attention_heads"],
+ mesh_device=mesh_device,
+ )
+ decoder = build_audio_decoder(config, mesh_device)
+ # `strict=False`: the converted dict carries both halves' tensors.
+ encoder.load_torch_state_dict(converted, strict=False)
+ decoder.load_torch_state_dict(converted, strict=False)
+
+ measurements = {
+ "audio_encode_5s": _time_it(lambda: encoder(waveform), iterations=1, mesh_device=mesh_device),
+ "audio_decode_5s": _time_it(lambda: decoder(latents), iterations=1, mesh_device=mesh_device),
+ }
+ _report(measurements)
+
+
+# ---- whole-stage `decode` timing, per output type -------------------------------------------
+#
+# `test_visual_data_parallel_throughput` times one decoder wave; this times the stage the pipeline
+# bills to "VAE decode" -- tiling, upload, readback, unpatchify, stitch and the temporal assembly --
+# and prints `_report_profile`'s breakdown, which is the point: a stage total alone cannot say which
+# of device / readback / stitch moved.
+#
+# RING, not the `fabric_config=None` the throughput test uses: `_decode_clip_device_stitched`
+# hardcodes `all_gather(topology=Ring)`, so `yuv420` cannot run on a line fabric. Built with a
+# `CCLManager` because `_read_wave_units` picks its reader on `ccl_manager is None` -- without one a
+# single-host run silently takes the `ConcatMeshToTensor` path and measures something production
+# never runs.
+#
+# export MINIMAX_H3_MODEL_PATH=/path/to/MiniMax-H3
+# pytest models/tt_dit/tests/models/minimax_h3/test_performance_vae_minimax_h3.py -k decode_stage -s
+
+MESH_4X8_RING = [
+ pytest.param(
+ (4, 8),
+ {
+ "fabric_config": ttnn.FabricConfig.FABRIC_1D_RING,
+ "require_exact_physical_num_devices": True,
+ "l1_small_size": 65536,
+ },
+ id="4x8ring",
+ )
+]
+
+# 1344x768 at 16x spatial compression. Latent frame counts follow the 17n -> 5n rule less
+# `token_drop`, so 124 frames (5 s) is 37 and 362 (15 s) is 107.
+DECODE_STAGE_LATENT_HW = (48, 84)
+DECODE_STAGE_FRAMES = {5: 37, 15: 107}
+# 7 latent frames is one chunk, the cheapest warm-up that still exercises a whole wave.
+DECODE_STAGE_WARM_FRAMES = 7
+
+MINIMAX_H3_PIXEL_MEAN = (0.485, 0.456, 0.406)
+MINIMAX_H3_PIXEL_STD = (0.229, 0.224, 0.225)
+
+# Each mode is one lever, so a row of this table measures that lever alone.
+DECODE_STAGE_MODES = {
+ # Today's default: bf16 tiles in token space, blended and unpatchified on host.
+ "float": {},
+ # Same path, uint8 across PCIe. Halves the transfer.
+ "uint8": {"pixel_denorm": (MINIMAX_H3_PIXEL_MEAN, MINIMAX_H3_PIXEL_STD), "readback_uint8": True},
+ # Stitch, clamp and colour-convert on device; read one planar canvas at 1.5 bytes/pixel.
+ "yuv420": {"pixel_denorm": (MINIMAX_H3_PIXEL_MEAN, MINIMAX_H3_PIXEL_STD), "device_stitch": True},
+}
+
+
+def _decode_stage_state(weights_dir: str) -> dict[str, torch.Tensor]:
+ """Just the decoder-side tensors; over half the checkpoint is the encoder."""
+ import json
+
+ from safetensors.torch import load_file
+
+ index_path = os.path.join(weights_dir, "diffusion_pytorch_model.safetensors.index.json")
+ if not os.path.isfile(index_path):
+ return {
+ k: v
+ for k, v in load_file(os.path.join(weights_dir, "diffusion_pytorch_model.safetensors")).items()
+ if k.startswith(("decoder.", "post_quant_conv."))
+ }
+ weight_map = json.loads(open(index_path).read())["weight_map"]
+ wanted = {k: f for k, f in weight_map.items() if k.startswith(("decoder.", "post_quant_conv."))}
+ state: dict[str, torch.Tensor] = {}
+ for shard in sorted(set(wanted.values())):
+ loaded = load_file(os.path.join(weights_dir, shard))
+ state.update({k: loaded[k] for k in wanted if k in loaded})
+ return state
+
+
+@pytest.mark.timeout(3600)
+@pytest.mark.parametrize(("mesh_device", "device_params"), MESH_4X8_RING, indirect=["mesh_device", "device_params"])
+@pytest.mark.parametrize("seconds", [5, 15], ids=["5s", "15s"])
+@pytest.mark.parametrize("mode", sorted(DECODE_STAGE_MODES), ids=sorted(DECODE_STAGE_MODES))
+def test_decode_stage(mesh_device, seconds, mode):
+ """Measurement driver, not a gate: reports the split, asserts nothing about wall time."""
+ from loguru import logger
+
+ weights_dir = weights_subdir("vae")
+ if weights_dir is None:
+ pytest.skip("MiniMax-H3 vae not found; set MINIMAX_H3_MODEL_PATH")
+
+ config = MiniMaxH3VaeConfig.from_pretrained(weights_dir)
+ ccl_manager = CCLManager(mesh_device, num_links=2, topology=ttnn.Topology.Ring)
+ vae = MiniMaxH3Vae(config, mesh_device=mesh_device, ccl_manager=ccl_manager, **DECODE_STAGE_MODES[mode])
+ output_type = "yuv420" if mode == "yuv420" else "float"
+
+ vae.load_decoder_state(_decode_stage_state(weights_dir))
+ # Build the per-shape decoder outside the timed region, as `_prepare_vae(decode_shape=...)` does:
+ # its weight upload is seconds and would otherwise land inside the first `decode`.
+ vae._decoder_for(config.tokens_chunk_size + config.token_overlap, 16, 16)
+
+ torch.manual_seed(0)
+ latent_h, latent_w = DECODE_STAGE_LATENT_HW
+ warm = torch.randn(1, config.latent_channels, DECODE_STAGE_WARM_FRAMES, latent_h, latent_w)
+ latents = torch.randn(1, config.latent_channels, DECODE_STAGE_FRAMES[seconds], latent_h, latent_w)
+
+ vae.decode(warm, output_type=output_type)
+
+ started = time.time()
+ video = vae.decode(latents, output_type=output_type)
+ elapsed = time.time() - started
+
+ profile = dict(vae.last_decode_profile)
+ logger.info(f"DECODE_STAGE mode={mode} seconds={seconds} total={elapsed:.3f}s video={tuple(video.shape)}")
+ logger.info(
+ f"DECODE_SPLIT mode={mode} seconds={seconds} "
+ + " ".join(
+ f"{k}={profile.get(k, 0.0):.3f}"
+ for k in (
+ "device",
+ "readback",
+ "stitch",
+ "unpatchify",
+ "blend",
+ "concat",
+ "tiling",
+ "upload",
+ "host_prep",
+ "residual",
+ )
+ )
+ + f" waves={int(profile.get('waves', 0))} units={int(profile.get('units', 0))}"
+ + f" readback_gb={profile.get('readback_mb', 0.0) / 1000:.2f}"
+ )
diff --git a/code/models/tt_dit/tests/models/minimax_h3/test_pipeline_fl2va_minimax_h3.py b/code/models/tt_dit/tests/models/minimax_h3/test_pipeline_fl2va_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d76c103278f92f54550cf70d147d515ab5b2f2f
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/test_pipeline_fl2va_minimax_h3.py
@@ -0,0 +1,308 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""MiniMax-H3 `fl2va` e2e: prompt plus first/last keyframes in, video and audio out.
+`test_fl2va_follows_the_keyframe` (fractal keyframe) is the discriminating conditioning gate.
+Separate file so it runs in its own process: DiT programs + CCL buffers at several padded lengths is a memory risk."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import numpy as np
+import pytest
+import torch
+from loguru import logger
+from PIL import Image
+
+from ....pipelines.minimax_h3.packing import align_num_frames, prepare_keyframe_image
+from ....pipelines.minimax_h3.pipeline_minimax_h3 import MiniMaxH3Pipeline
+from ..wan2_2.common import check_output_sanity
+from .common import GALAXY_MESHES, create_fractal_image
+from .common_av import (
+ CALIBRATED_FOX_PROMPT,
+ artifact_dir,
+ check_audio_sanity,
+ check_av_sync,
+ check_spatial_seams,
+ check_written_file,
+ gate_clip,
+ log_spectral_flatness,
+ log_timing_table,
+ run_warm_generation,
+ to_uint8_frames,
+ weights_dir,
+ write_artifacts,
+)
+
+HEIGHT, WIDTH = 768, 1344
+NUM_FRAMES = 124
+NUM_INFERENCE_STEPS = 50
+SEED = 0
+
+PROMPT = CALIBRATED_FOX_PROMPT # the tier-6 bars are calibrated against this exact prompt
+
+# Ring collectives require FABRIC_1D_RING.
+MESHES = GALAXY_MESHES
+
+ANCHOR_PCC_FLOOR = 0.95 # measured 0.9943-0.9971 across the three anchor cases
+
+CLIP_THRESHOLD = 33.0 # t2va's bar; measured to transfer (36.63-37.30 vs t2va's 37.37)
+
+EXPECTED_TOTAL_S = 400.0 # loose did-something-collapse bar, not a perf target
+
+
+def check_keyframe_anchor(frames, keyframe, *, index, stretch, width, height, pcc_floor=0.3):
+ """Decoded frame `index` must correlate with its keyframe; uses the pipeline's own
+ `prepare_keyframe_image` (stretch vs cover-crop) so the comparison canvas cannot drift."""
+ frame = frames[index]
+ if isinstance(frame, torch.Tensor):
+ frame = frame.cpu().numpy()
+ frame = np.asarray(frame).astype(np.float64)
+
+ prepared = prepare_keyframe_image(keyframe.convert("RGB"), height, width, stretch)
+ expected = np.asarray(prepared).astype(np.float64)
+ assert frame.shape == expected.shape, f"frame {index} shape {frame.shape} != keyframe {expected.shape}"
+
+ pcc = float(np.corrcoef(frame.ravel(), expected.ravel())[0, 1])
+ label = "first" if index == 0 else "last"
+ logger.info(f"fl2va {label}-keyframe anchor: decoded frame {index} vs keyframe PCC = {pcc:.4f}")
+ assert pcc > pcc_floor, (
+ f"decoded frame {index} barely correlates with the {label} keyframe (PCC={pcc:.3f}); "
+ "the fl2va conditioning path is likely broken"
+ )
+ return pcc
+
+
+def check_tile_boundary_gradient(frames, *, vertical_boundaries, horizontal_boundaries, max_ratio=3.0):
+ """One-pixel gradient at each tile boundary vs its neighbourhood -- catches seams the block-mean
+ check cannot see. Known-good measures 1.2-1.5x; the built-in control columns must sit near 1.0."""
+ frames = np.asarray(frames)
+ luma = frames.astype(np.float64).mean(-1) if frames.ndim == 4 else frames.astype(np.float64)
+ gx = np.abs(np.diff(luma, axis=2)).mean(axis=(0, 1))
+ gy = np.abs(np.diff(luma, axis=1)).mean(axis=(0, 2))
+
+ def ratio(profile, index):
+ near = np.concatenate([profile[index - 12 : index - 3], profile[index + 3 : index + 12]])
+ return float(profile[index - 1] / max(float(np.median(near)), 1e-9))
+
+ results = {}
+ for name, profile, boundaries in (("vertical", gx, vertical_boundaries), ("horizontal", gy, horizontal_boundaries)):
+ ratios = {int(b): ratio(profile, int(b)) for b in boundaries if 12 < int(b) < len(profile) - 12}
+ results[name] = ratios
+ if ratios:
+ logger.info(
+ f"{name} tile-boundary gradient ratios (1.0 = no seam): "
+ + ", ".join(f"x={b}:{r:.3f}" if name == "vertical" else f"y={b}:{r:.3f}" for b, r in ratios.items())
+ )
+
+ generator = np.random.default_rng(0)
+ candidates = generator.integers(30, len(gx) - 30, 24)
+ control = [c for c in candidates if all(abs(int(c) - int(b)) > 16 for b in vertical_boundaries)][:12]
+ control_ratios = [ratio(gx, int(c)) for c in control]
+ mean_control = float(np.mean(control_ratios))
+ logger.info(f"control non-boundary columns: mean ratio {mean_control:.3f}, max {max(control_ratios):.3f}")
+ assert mean_control < 1.15, (
+ f"control columns average {mean_control:.3f}; this statistic is tracking image structure rather "
+ "than tile boundaries, so its boundary numbers mean nothing"
+ )
+
+ worst = max((r, f"{n} {b}") for n, rs in results.items() for b, r in rs.items())
+ assert worst[0] < max_ratio, (
+ f"tile-boundary gradient at {worst[1]} is {worst[0]:.2f}x its neighbourhood (control "
+ f"{mean_control:.2f}); a visible seam. See the artifact rubric"
+ )
+ results["control"] = mean_control
+ return results
+
+
+def _gated_keyframe() -> Image.Image:
+ """Frame 0 of the calibrated t2va generation -- the only keyframe the tier-6 bars apply to.
+ As a conditioning signal it is confounded; `test_fl2va_follows_the_keyframe` rules that out."""
+ artifact = Path.home() / "h3_t2va_artifacts" / "t2va.mp4"
+ if not artifact.is_file():
+ pytest.skip(
+ f"no calibrated t2va artifact at {artifact}; run test_pipeline_minimax_h3.py first so the "
+ "fl2va keyframe comes from content the tier-6 thresholds were calibrated on"
+ )
+ frame = _first_frame(artifact)
+ assert frame.size == (WIDTH, HEIGHT), f"t2va artifact is {frame.size}, expected {(WIDTH, HEIGHT)}"
+ return frame
+
+
+def _first_frame(path: Path) -> Image.Image:
+ import imageio.v3 as iio
+
+ return Image.fromarray(np.asarray(iio.imread(path, index=0, plugin="pyav"))).convert("RGB")
+
+
+@pytest.mark.timeout(10800)
+@pytest.mark.parametrize(("mesh_device", "device_params"), MESHES, indirect=["mesh_device", "device_params"])
+def test_fl2va_end_to_end(mesh_device, reset_seeds):
+ """The `first_and_last` case: both preparation paths and a two-run vision scatter; perf + quality."""
+ case = "first_and_last"
+ keyframe = _gated_keyframe()
+ image = keyframe
+ last_image = keyframe
+
+ pipeline = MiniMaxH3Pipeline.create_pipeline(mesh_device=mesh_device, weights_dir=weights_dir())
+
+ # Warmup must be fl2va-shaped (keyframes included): programs are keyed on padded length; the helper asserts it.
+ output = run_warm_generation(
+ pipeline,
+ PROMPT,
+ image=image,
+ last_image=last_image,
+ num_frames=NUM_FRAMES,
+ height=HEIGHT,
+ width=WIDTH,
+ num_inference_steps=NUM_INFERENCE_STEPS,
+ seed=SEED,
+ )
+
+ expected_frames = align_num_frames(NUM_FRAMES)
+ logger.info(
+ f"fl2va[{case}] padded_len={pipeline.last_padded_len} "
+ f"video={tuple(output.video.shape)} audio={tuple(output.audio.shape)}"
+ )
+
+ num_forwards = NUM_INFERENCE_STEPS - 1
+ video_seconds = expected_frames / output.fps
+ log_timing_table(
+ pipeline,
+ "fl2va",
+ num_forwards=num_forwards,
+ video_seconds=video_seconds,
+ expected_total_s=EXPECTED_TOTAL_S,
+ extra=(
+ f" | {WIDTH}x{HEIGHT}, {expected_frames} frames @ {output.fps} fps "
+ f"({video_seconds:.2f} s), {num_forwards} forwards, first+last anchors, "
+ f"padded_len {pipeline.last_padded_len}"
+ ),
+ )
+ assert output.video.shape[2] == expected_frames, f"{output.video.shape[2]} frames, expected {expected_frames}"
+
+ frames = to_uint8_frames(output)
+
+ check_output_sanity(frames, num_frames=expected_frames, height=HEIGHT, width=WIDTH)
+ check_audio_sanity(output.audio, sampling_rate=output.sampling_rate, expected_seconds=expected_frames / output.fps)
+ check_av_sync(frames, output.audio, sampling_rate=output.sampling_rate, fps=output.fps)
+ log_spectral_flatness(output.audio, sampling_rate=output.sampling_rate)
+
+ # Boundaries from the VAE's own tile grid (vertical seams come from the x starts).
+ ratio = pipeline.vae_config.spatial_compression_ratio
+ (y_starts, _, _), (x_starts, _, _) = pipeline.vae.decode_tile_grid(HEIGHT // ratio, WIDTH // ratio)
+ check_spatial_seams(frames, vertical_boundaries=x_starts[1:], horizontal_boundaries=y_starts[1:])
+ check_tile_boundary_gradient(frames, vertical_boundaries=x_starts[1:], horizontal_boundaries=y_starts[1:])
+
+ # `stretch` follows the pipeline's rule: the FIRST keyframe given is the geometry anchor.
+ if image is not None:
+ check_keyframe_anchor(
+ frames, image, index=0, stretch=True, width=WIDTH, height=HEIGHT, pcc_floor=ANCHOR_PCC_FLOOR
+ )
+ if last_image is not None:
+ check_keyframe_anchor(
+ frames,
+ last_image,
+ index=-1,
+ stretch=image is None,
+ width=WIDTH,
+ height=HEIGHT,
+ pcc_floor=ANCHOR_PCC_FLOOR,
+ )
+
+ artifacts = artifact_dir("h3_t2va_artifacts")
+ paths = write_artifacts(frames, output.audio.cpu().numpy(), output.sampling_rate, artifacts, stem=f"fl2va_{case}")
+ check_written_file(paths, expected_frames)
+ if "mp4" in paths:
+ for index in (0, 17, 62, expected_frames - 1):
+ Image.fromarray(frames[index]).save(artifacts / f"fl2va_{case}_frame_{index}.png")
+
+ gate_clip(frames, PROMPT, CLIP_THRESHOLD, f"fl2va[{case}]")
+
+ logger.info(
+ "REMINDER: read the artifact rubric against these frames -- seams and flicker are what every "
+ "whole-tensor metric averages away, and both are parallelism bugs"
+ )
+
+
+def test_lone_last_keyframe_is_stretched_not_cover_cropped():
+ """The pipeline keys `stretch` on list position, so a lone `last_image` (index 0) is stretched."""
+ source = np.zeros((512, 512, 3), dtype=np.uint8)
+ source[:64, :, 0] = 255
+ source[-64:, :, 2] = 255
+ source[:, :, 1] = 32 # non-zero interior so the crop is not comparing zeros to zeros
+ keyframe = Image.fromarray(source)
+
+ # The pipeline's own rule, replicated for the lone-last request (image=None).
+ image, last_image = None, keyframe
+ sources = [k for k in (image, last_image) if k is not None]
+ prepared = [prepare_keyframe_image(k, HEIGHT, WIDTH, stretch=(i == 0)) for i, k in enumerate(sources)]
+ assert len(prepared) == 1
+
+ stretched = prepare_keyframe_image(keyframe, HEIGHT, WIDTH, True)
+ cropped = prepare_keyframe_image(keyframe, HEIGHT, WIDTH, False)
+ got = np.asarray(prepared[0])
+
+ assert prepared[0].size == stretched.size == cropped.size == (WIDTH, HEIGHT)
+ assert not np.array_equal(np.asarray(stretched), np.asarray(cropped))
+
+ assert np.array_equal(got, np.asarray(stretched)), "a lone last_image must be stretched (it is the geometry anchor)"
+
+ assert np.asarray(stretched)[0, :, 0].mean() > 200, "stretch must keep the source's top edge"
+ assert np.asarray(stretched)[-1, :, 2].mean() > 200, "stretch must keep the source's bottom edge"
+ assert np.asarray(cropped)[0, :, 0].mean() < 50, "cover-crop of a 1:1 source must discard the top band"
+ assert np.asarray(cropped)[-1, :, 2].mean() < 50, "cover-crop of a 1:1 source must discard the bottom band"
+
+
+@pytest.mark.timeout(7200)
+@pytest.mark.parametrize(("mesh_device", "device_params"), MESHES, indirect=["mesh_device", "device_params"])
+def test_fl2va_follows_the_keyframe(mesh_device, reset_seeds):
+ """Discriminating gate: a fractal keyframe the model would never produce must drive frame 0."""
+ fractal = create_fractal_image(WIDTH, HEIGHT)
+ pipeline = MiniMaxH3Pipeline.create_pipeline(mesh_device=mesh_device, weights_dir=weights_dir())
+ output = pipeline(
+ PROMPT,
+ image=fractal,
+ num_frames=NUM_FRAMES,
+ height=HEIGHT,
+ width=WIDTH,
+ num_inference_steps=NUM_INFERENCE_STEPS,
+ seed=SEED,
+ )
+ frames = to_uint8_frames(output)
+
+ def pcc(a, b):
+ a = np.asarray(a, dtype=np.float64).ravel()
+ b = np.asarray(b, dtype=np.float64).ravel()
+ return float(np.corrcoef(a, b)[0, 1])
+
+ prepared = np.asarray(prepare_keyframe_image(fractal, HEIGHT, WIDTH, True))
+ to_keyframe = pcc(frames[0], prepared)
+ to_t2va = pcc(frames[0], np.asarray(_gated_keyframe()))
+ tail = pcc(frames[-1], prepared)
+
+ logger.info(
+ f"fl2va keyframe-drives-generation: frame 0 vs fractal keyframe {to_keyframe:.4f}, "
+ f"frame 0 vs t2va's own frame 0 {to_t2va:.4f}, frame -1 vs fractal keyframe {tail:.4f}"
+ )
+ artifacts = artifact_dir("h3_t2va_artifacts")
+ for index in (0, 17, 62, align_num_frames(NUM_FRAMES) - 1):
+ Image.fromarray(frames[index]).save(artifacts / f"fl2va_fractal_frame_{index}.png")
+
+ # (1) frame 0 follows the supplied keyframe.
+ assert to_keyframe > ANCHOR_PCC_FLOOR, (
+ f"decoded frame 0 correlates only {to_keyframe:.3f} with the fractal keyframe it was "
+ "conditioned on; the keyframe is not reaching the DiT"
+ )
+ # (2) the margin rules out the t2va confound; a no-op pipeline inverts this comparison.
+ assert to_keyframe > to_t2va + 0.30, (
+ f"frame 0 resembles the fractal keyframe ({to_keyframe:.3f}) barely more than it resembles "
+ f"t2va's own frame 0 ({to_t2va:.3f}); conditioning may be a no-op"
+ )
+ # (3) it is still a video, not 124 copies of the keyframe.
+ assert tail < to_keyframe - 0.20, (
+ f"the last frame still correlates {tail:.3f} with the keyframe against frame 0's "
+ f"{to_keyframe:.3f}; the clip may be pinned throughout rather than anchored at one end"
+ )
diff --git a/code/models/tt_dit/tests/models/minimax_h3/test_pipeline_minimax_h3.py b/code/models/tt_dit/tests/models/minimax_h3/test_pipeline_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..170ab1802a5eba820d05af931bd16b7e2d5616af
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/test_pipeline_minimax_h3.py
@@ -0,0 +1,237 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""End-to-end `t2va` perf + quality gate, swept over the published working points.
+
+Six aspect ratios (21:9 .. 9:16) x three durations (5 / 10 / 15 s), 50 steps. The canvas comes from
+`resolve_canvas_size` -- short edge 768 from 16:9 through 9:16, ~1 MPix for wider -- and the frame
+count from `align_num_frames`, so neither is tabulated here. Each case writes its own artifact stem,
+so a sweep does not overwrite itself.
+"""
+
+from __future__ import annotations
+
+import os
+
+import pytest
+from loguru import logger
+
+import ttnn
+
+from ....pipelines.minimax_h3.packing import MINIMAX_H3_FPS, align_num_frames, resolve_canvas_size
+from ....pipelines.minimax_h3.pipeline_minimax_h3 import (
+ MiniMaxH3Pipeline,
+ _requested_audio_t_factor,
+ _resolve_audio_t_shard,
+)
+from ..wan2_2.common import check_output_sanity
+from .common import GALAXY_MESHES
+from .common_av import (
+ CALIBRATED_FOX_PROMPT,
+ artifact_dir,
+ check_audio_sanity,
+ check_av_sync,
+ check_spatial_seams,
+ check_written_file,
+ gate_clip,
+ gate_vbench,
+ log_spectral_flatness,
+ log_timing_table,
+ run_warm_generation,
+ to_uint8_frames,
+ weights_dir,
+ write_artifacts,
+)
+
+NUM_INFERENCE_STEPS = 50
+SEED = 0
+
+# The published working points: short edge 768 from 16:9 through 9:16, and ~1 MPix for anything
+# wider. `resolve_canvas_size` already implements exactly that (768 short edge, area capped at
+# 768*1344, both axes snapped to 32), so the canvas is derived rather than tabulated here --
+# 21:9 lands on 672x1536, which is the documented example.
+ASPECT_RATIOS = [(21, 9), (16, 9), (4, 3), (1, 1), (3, 4), (9, 16)]
+DURATIONS_S = [5, 10, 15]
+
+# Loose did-something-collapse bar, per second of video rather than absolute: the 400 s figure was
+# set for one 5.17 s clip, and 10 s / 15 s at 1 MPix cost proportionally more. 77 s of compute per
+# video second is the same allowance (400 / 5.167), i.e. still ~5x looser than the 13.4x realtime
+# factor measured at 16:9 / 5 s.
+EXPECTED_S_PER_VIDEO_SECOND = 77.0
+
+
+# calibrated 2026-08-04, fox prompt, seed 0 (single sample; margins are generous)
+VBENCH_THRESHOLDS = {
+ "subject_consistency": 0.95,
+ "background_consistency": 0.95,
+ "motion_smoothness": 0.97,
+ "dynamic_degree": 1.0,
+ "imaging_quality": 0.64,
+}
+CLIP_THRESHOLD = 33.0 # measured mean 37.05 (2026-08-04, fox prompt, seed 0)
+
+# tier-6 thresholds are calibrated against this exact prompt; swapping it invalidates both bars.
+PROMPT = CALIBRATED_FOX_PROMPT
+
+SWEEP = [
+ pytest.param(ratio, seconds, id=f"{ratio[0]}x{ratio[1]}_{seconds}s")
+ for seconds in DURATIONS_S
+ for ratio in ASPECT_RATIOS
+]
+
+
+# Pure (no-device) coverage for the audio T-shard resolver: axis selection, tie-break, 8->4->1 fallback.
+@pytest.mark.parametrize(
+ ("requested", "mesh_shape", "tp_axis", "sp_axis", "expected"),
+ [
+ (8, (4, 8), 0, 1, (8, 1)), # factor 8 -> size-8 SP axis
+ (8, (8, 4), 1, 0, (8, 0)), # reversed 8x4 -> size-8 axis 0
+ (4, (4, 8), 0, 1, (4, 0)), # factor 4 -> size-4 TP axis
+ (8, (4, 32), 0, 1, (4, 0)), # no size-8 axis -> fall back to 4
+ (8, (1, 1), 0, 1, (1, None)), # single device -> unsharded
+ (4, (4, 4), 0, 1, (4, 0)), # equal axes -> prefer TP
+ (1, (4, 8), 0, 1, (1, None)), # explicit unsharded
+ (2, (4, 8), 0, 1, (1, None)), # below the 4/8 chain -> unsharded
+ (32, (4, 32), 0, 1, (32, 1)), # opt-in 32 on a quad -> the 32-wide inter-host axis
+ (32, (4, 8), 0, 1, (8, 1)), # requesting 32 on a single galaxy -> falls to 8
+ ],
+)
+def test_resolve_audio_t_shard(requested, mesh_shape, tp_axis, sp_axis, expected):
+ assert _resolve_audio_t_shard(requested, mesh_shape, tp_axis, sp_axis) == expected
+
+
+def test_requested_audio_t_factor_precedence(monkeypatch, expect_error):
+ # explicit kwarg wins over the env var
+ monkeypatch.setenv("MINIMAX_H3_AUDIO_T_FACTOR", "32")
+ assert _requested_audio_t_factor(4) == (4, False)
+ # env var fills the default when the kwarg is None
+ assert _requested_audio_t_factor(None) == (32, True)
+ # no kwarg, no env -> default 8
+ monkeypatch.delenv("MINIMAX_H3_AUDIO_T_FACTOR", raising=False)
+ assert _requested_audio_t_factor(None) == (8, False)
+ # a non-integer env value is a clear error, not a silent fallback
+ monkeypatch.setenv("MINIMAX_H3_AUDIO_T_FACTOR", "thirty-two")
+ with expect_error(ValueError, "MINIMAX_H3_AUDIO_T_FACTOR"):
+ _requested_audio_t_factor(None)
+
+
+@pytest.mark.timeout(7200)
+@pytest.mark.parametrize(("aspect_ratio", "duration_s"), SWEEP)
+@pytest.mark.parametrize(("mesh_device", "device_params"), GALAXY_MESHES, indirect=["mesh_device", "device_params"])
+def test_t2va_end_to_end(mesh_device, reset_seeds, aspect_ratio, duration_s):
+ weights = weights_dir("transformer", "text_encoder", "vae", "audio_vae")
+ artifacts = artifact_dir("h3_t2va_artifacts")
+ prompt = PROMPT
+
+ HEIGHT, WIDTH = resolve_canvas_size(*aspect_ratio)
+ NUM_FRAMES = align_num_frames(round(duration_s * MINIMAX_H3_FPS))
+ # One artifact per working point, so a sweep does not overwrite itself.
+ stem = f"t2va_{aspect_ratio[0]}x{aspect_ratio[1]}_{WIDTH}x{HEIGHT}_{duration_s}s"
+ logger.info(f"working point: {aspect_ratio[0]}:{aspect_ratio[1]} -> {WIDTH}x{HEIGHT}, {NUM_FRAMES} frames, {stem}")
+
+ # A missing dependency must report SKIPPED before the long generation, never silently pass as green.
+ pytest.importorskip("open_clip", reason="the CLIP gate needs open_clip, which is not installed")
+
+ if not os.environ.get("TT_DIT_CACHE_DIR"):
+ logger.warning(
+ "TT_DIT_CACHE_DIR is unset, so every weight load reads safetensors. Prepares are excluded "
+ "from the total either way, but the run will take far longer than the reported compute."
+ )
+
+ pipeline = MiniMaxH3Pipeline.create_pipeline(mesh_device=mesh_device, weights_dir=weights)
+
+ output = run_warm_generation(
+ pipeline,
+ prompt,
+ num_frames=NUM_FRAMES,
+ height=HEIGHT,
+ width=WIDTH,
+ num_inference_steps=NUM_INFERENCE_STEPS,
+ seed=SEED,
+ )
+
+ expected_frames = align_num_frames(NUM_FRAMES)
+ logger.info(
+ f"generated {output.num_frames} frames ({output.video_seconds:.3f} s) and "
+ f"{output.audio_seconds:.3f} s of audio at {output.sampling_rate} Hz"
+ )
+
+ num_forwards = NUM_INFERENCE_STEPS - 1
+ video_seconds = expected_frames / MINIMAX_H3_FPS
+ log_timing_table(
+ pipeline,
+ "t2va",
+ num_forwards=num_forwards,
+ video_seconds=video_seconds,
+ expected_total_s=EXPECTED_S_PER_VIDEO_SECOND * video_seconds,
+ extra=(
+ f" | {aspect_ratio[0]}:{aspect_ratio[1]} {WIDTH}x{HEIGHT}, {expected_frames} frames "
+ f"@ {MINIMAX_H3_FPS} fps ({video_seconds:.2f} s), {num_forwards} forwards, "
+ f"padded_len {pipeline.last_padded_len}"
+ ),
+ )
+
+ frames = to_uint8_frames(output)
+
+ check_output_sanity(frames, num_frames=expected_frames, height=HEIGHT, width=WIDTH)
+ check_audio_sanity(
+ output.audio,
+ sampling_rate=output.sampling_rate,
+ expected_seconds=expected_frames / MINIMAX_H3_FPS,
+ )
+ sync = check_av_sync(
+ frames,
+ output.audio,
+ sampling_rate=output.sampling_rate,
+ fps=MINIMAX_H3_FPS,
+ )
+ log_spectral_flatness(output.audio, sampling_rate=output.sampling_rate)
+
+ # Boundaries from the VAE's own tile grid; re-deriving them with a wrong overlap gates non-boundaries.
+ ratio = pipeline.vae_config.spatial_compression_ratio
+ (y_starts, _, _), (x_starts, _, _) = pipeline.vae.decode_tile_grid(HEIGHT // ratio, WIDTH // ratio)
+ check_spatial_seams(frames, vertical_boundaries=x_starts[1:], horizontal_boundaries=y_starts[1:])
+
+ # Rank 0 alone writes and scores, and its outcome is held rather than raised: every other rank
+ # goes straight to the barrier below, so a gate failure, a VBench `pytest.skip` or an artifact I/O
+ # error raised here would leave them blocked and skip the trace release. `pytest.skip.Exception`
+ # is named alongside `Exception` because `Skipped` does not derive from it.
+ outcome: BaseException | None = None
+ is_distributed = ttnn.using_distributed_env()
+ if not is_distributed or int(ttnn.distributed_context_get_rank()) == 0:
+ try:
+ paths = write_artifacts(frames, output.audio.cpu().numpy(), output.sampling_rate, artifacts, stem=stem)
+ check_written_file(paths, expected_frames, height=HEIGHT, width=WIDTH)
+
+ # CLIP_THRESHOLD was measured at 16:9 / 5 s. Applying it across the sweep is an extrapolation,
+ # but a generous one: the calibrated point measures ~37 against a bar of 33, and the score is a
+ # prompt-alignment number rather than a resolution-dependent one. A prompt change would
+ # invalidate it outright -- recalibrate before swapping PROMPT.
+ gate_clip(frames, prompt, CLIP_THRESHOLD, stem)
+ # RUN_VBENCH=0 drops the VBench gate. It needs its own interpreter (~/vbench_env, pinned to
+ # numpy<2 / transformers 4.33), and without one `run_vbench` skips -- which marks the whole
+ # test SKIPPED *after* the full generation, hiding the perf and A/V results behind a
+ # non-result. Off means "not measured": everything above still gates.
+ if os.environ.get("RUN_VBENCH", "1") not in ("0", "false", "False"):
+ gate_vbench(paths, prompt, VBENCH_THRESHOLDS, stem)
+ else:
+ logger.warning("RUN_VBENCH=0, so the VBench gate did not run; generative quality is UNMEASURED")
+
+ logger.info(f"artifacts in {artifacts}: {sorted(p.name for p in artifacts.iterdir())}")
+ except (Exception, pytest.skip.Exception) as exc:
+ outcome = exc
+ if is_distributed:
+ ttnn.distributed_context_barrier()
+ logger.info(
+ "REMINDER: read the artifact rubric against these frames -- the seam and flicker scores above "
+ "are statistics, and only looking at the output catches what they average away"
+ )
+ # Every rank, and before the fixture closes the mesh: a captured trace holds device buffers
+ # for the request. Only the 4x32 preset traces, so this is a no-op at 4x8.
+ pipeline.release_traces()
+ # Now that every rank has resynchronised and released its trace, rank 0 reports. mpirun surfaces
+ # a single failing rank as a failing job, so the run fails rather than hanging.
+ if outcome is not None:
+ raise outcome
+ assert sync["video_seconds"] > 0
diff --git a/code/models/tt_dit/tests/models/minimax_h3/test_pipeline_ref2va_minimax_h3.py b/code/models/tt_dit/tests/models/minimax_h3/test_pipeline_ref2va_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..76ae9238e92b699b1520a38c5f0966c2ea352d6f
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/test_pipeline_ref2va_minimax_h3.py
@@ -0,0 +1,274 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""MiniMax-H3 ``ref2va`` e2e on the ``mixed`` case (largest shape, all three reference modalities).
+``test_ref2va_conditioning_is_not_a_no_op`` is the discriminating gate, judged against a measured run-to-run floor.
+Separate file so it runs in its own process: ref2va padded lengths are 1.2-3.0x t2va's, a memory risk."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import numpy as np
+import pytest
+import torch
+from loguru import logger
+from PIL import Image
+
+from ....pipelines.minimax_h3.packing_ref2va import MiniMaxH3Reference, reference_from_video_file
+from ....pipelines.minimax_h3.pipeline_minimax_h3 import MiniMaxH3Pipeline
+from .common import GALAXY_MESHES, create_fractal_image
+from .common_av import (
+ artifact_dir,
+ check_audio_sanity,
+ check_av_sync,
+ check_spatial_seams,
+ gate_clip,
+ gate_vbench,
+ log_timing_table,
+ run_warm_generation,
+ to_uint8_frames,
+ weights_dir,
+ write_artifacts,
+)
+
+WIDTH, HEIGHT, NUM_FRAMES, STEPS, SEED = 1344, 768, 124, 50, 0
+FPS = 24
+
+PROMPT = "a slow push-in through a quiet room as afternoon light moves across the floor"
+
+# Bars set below the minimum measured across the three ref2va cases; t2va's bars do NOT transfer.
+REF2VA_CLIP_THRESHOLD = 25.0 # min measured 29.05
+REF2VA_VBENCH_THRESHOLDS = {
+ "subject_consistency": 0.90,
+ "background_consistency": 0.89,
+ "motion_smoothness": 0.97, # t2va's, and ref2va measures *better* than t2va on it
+ "dynamic_degree": 1.0, # binary over one video: 1.0 or the clip is frozen
+ "imaging_quality": 0.44,
+}
+
+# 16384, not the other gates' 65536: the video VAE's taps=3 encoder (only ref2va reaches it) clashes with L1 above it.
+_L1_SMALL = 16384
+
+# Ring collectives require FABRIC_1D_RING; only `l1_small_size` differs from the other gates.
+# The shared shapes, but this suite runs a smaller L1_SMALL pool than they carry and reports it in
+# the measurement line, so the size is overridden rather than inherited.
+MESHES = [
+ pytest.param(shape, {**params, "l1_small_size": _L1_SMALL}, id=param.id)
+ for param in GALAXY_MESHES
+ for shape, params in [param.values]
+]
+
+
+REFERENCE_MEDIA = Path.home() / "h3_fl2va_artifacts" / "fl2va_first.mp4"
+
+
+def reference_video() -> Path:
+ if not REFERENCE_MEDIA.is_file():
+ pytest.skip(f"no reference video at {REFERENCE_MEDIA}; place a clip with a soundtrack there")
+ return REFERENCE_MEDIA
+
+
+def ref2va_references(case: str) -> list[MiniMaxH3Reference]:
+ """The reference set per e2e case (a fractal: content nothing in the prompt could produce)."""
+ if case == "one_image":
+ return [MiniMaxH3Reference(image=create_fractal_image(1024, 1024))]
+ if case == "video_with_sound":
+ return [reference_from_video_file(reference_video())]
+ if case == "mixed":
+ # image, then SILENT video, then bare audio: stresses split_condition_blocks' per-modality row cursors.
+ sounded = reference_from_video_file(reference_video())
+ return [
+ MiniMaxH3Reference(image=create_fractal_image(1024, 1024)),
+ reference_from_video_file(reference_video(), with_audio=False),
+ MiniMaxH3Reference(audio=sounded.audio, sample_rate=sounded.sample_rate),
+ ]
+ raise ValueError(case)
+
+
+def _real_frame_image() -> Image.Image:
+ from ....pipelines.minimax_h3.packing_ref2va import decode_reference_video
+
+ frames, _, _ = decode_reference_video(reference_video())
+ return Image.fromarray(frames[0])
+
+
+def _inverted(image: Image.Image) -> Image.Image:
+ """Colour-inverted copy: same size, so the packed layout and noise stream are unchanged."""
+ return Image.fromarray(255 - np.asarray(image.convert("RGB")))
+
+
+def _clip_resemblance(output, image: Image.Image, num_frames: int = 8) -> float:
+ from ...dataset_eval.clip_encoder import CLIPEncoder
+
+ encoder = CLIPEncoder()
+ frames = to_uint8_frames(output)
+ indices = np.linspace(0, frames.shape[0] - 1, num_frames).round().astype(int)
+
+ with torch.no_grad():
+ reference_features = encoder.model.encode_image(encoder.preprocess(image).unsqueeze(0)).float()
+ reference_features /= reference_features.norm(dim=-1, keepdim=True)
+ scores = []
+ for index in indices:
+ frame = encoder.preprocess(Image.fromarray(frames[index])).unsqueeze(0)
+ features = encoder.model.encode_image(frame).float()
+ features /= features.norm(dim=-1, keepdim=True)
+ scores.append(float((features @ reference_features.T).squeeze()))
+ return float(np.mean(scores))
+
+
+def _colour_distance(output, image: Image.Image) -> float:
+ """Euclidean distance between mean RGBs, over [0, 1]. Logged only, never asserted."""
+ output_mean = to_uint8_frames(output).reshape(-1, 3).mean(axis=0) / 255.0
+ reference_mean = np.asarray(image.convert("RGB")).reshape(-1, 3).mean(axis=0) / 255.0
+ return float(np.linalg.norm(output_mean - reference_mean))
+
+
+def _divergence(a: torch.Tensor, b: torch.Tensor) -> float:
+ return float((a.float() - b.float()).abs().mean())
+
+
+def _write(output, stem: str) -> dict:
+ directory = artifact_dir("h3_ref2va_artifacts")
+ frames = to_uint8_frames(output)
+ for index in (0, 17, NUM_FRAMES // 2, NUM_FRAMES - 1):
+ Image.fromarray(frames[index]).save(directory / f"{stem}_frame_{index}.png")
+ paths = write_artifacts(frames, output.audio.cpu().numpy(), output.sampling_rate, directory, stem=stem)
+ logger.info(f"{stem}: artifacts in {directory} ({sorted(paths)})")
+ return paths
+
+
+def _record_quality(frames: np.ndarray, paths: dict, case: str) -> None:
+ """CLIP prompt alignment and the five VBench dimensions, against the ref2va-calibrated bars."""
+ gate_clip(frames, PROMPT, REF2VA_CLIP_THRESHOLD, f"QUALITY ref2va[{case}]")
+ # A missing muxed mp4 skips rather than fails here, unlike t2va.
+ gate_vbench(paths, PROMPT, REF2VA_VBENCH_THRESHOLDS, f"QUALITY ref2va[{case}]", skip_without_mp4=True)
+
+
+def _pipeline(mesh_device) -> MiniMaxH3Pipeline:
+ """Pipeline bound to the `transformer_ref` partition (62 GB, fixed at construction)."""
+ return MiniMaxH3Pipeline.create_pipeline(
+ mesh_device=mesh_device, weights_dir=weights_dir("transformer_ref"), task="ref2va"
+ )
+
+
+CASE = "mixed"
+EXPECTED_PADDED_LEN = (
+ 89856 # measured e2e (a host estimate says 90112); asserted so the case cannot drift off the probed shape
+)
+
+
+@pytest.mark.timeout(10800)
+@pytest.mark.parametrize(("mesh_device", "device_params"), MESHES, indirect=["mesh_device", "device_params"])
+def test_ref2va_end_to_end(mesh_device, reset_seeds):
+ """Full ``mixed`` generation: geometry agreement across every reference path, plus warm latency."""
+ case = CASE
+ references = ref2va_references(case)
+ pipeline = _pipeline(mesh_device)
+
+ # Warmup must use the SAME references: `padded_len` depends on them, and the helper asserts agreement.
+ output = run_warm_generation(
+ pipeline,
+ PROMPT,
+ references=references,
+ num_frames=NUM_FRAMES,
+ height=HEIGHT,
+ width=WIDTH,
+ num_inference_steps=STEPS,
+ seed=SEED,
+ )
+
+ assert output.video.shape == (1, 3, NUM_FRAMES, HEIGHT, WIDTH), tuple(output.video.shape)
+ assert output.video.min() >= 0.0 and output.video.max() <= 1.0, "decoded video must be in [0, 1]"
+ assert torch.isfinite(output.video).all() and torch.isfinite(output.audio).all()
+ assert (
+ pipeline.last_padded_len == EXPECTED_PADDED_LEN
+ ), f"{case} ran at padded_len {pipeline.last_padded_len}, not the probed {EXPECTED_PADDED_LEN}"
+
+ frames = to_uint8_frames(output)
+ # Artifacts before the checks, so a failing check still leaves frames to inspect.
+ paths = _write(output, f"ref2va_{case}")
+
+ num_forwards = STEPS - 1
+ log_timing_table(
+ pipeline,
+ f"ref2va[{case}]",
+ num_forwards=num_forwards,
+ video_seconds=NUM_FRAMES / FPS,
+ extra=(
+ f", l1_small_size {_L1_SMALL} | {WIDTH}x{HEIGHT}, {NUM_FRAMES} frames @ {FPS} fps "
+ f"({NUM_FRAMES / FPS:.2f} s), {num_forwards} forwards, padded_len {pipeline.last_padded_len}"
+ ),
+ )
+
+ check_audio_sanity(
+ output.audio, sampling_rate=output.sampling_rate, expected_seconds=NUM_FRAMES / FPS, tolerance_seconds=0.05
+ )
+ check_av_sync(frames, output.audio, sampling_rate=output.sampling_rate, fps=FPS)
+ # Horizontal bar is 3.0: `video_with_sound` measures 2.29x there from scene content, not a seam.
+ check_spatial_seams(frames, vertical_boundaries=(448, 896), horizontal_boundaries=(), max_ratio=2.0)
+ check_spatial_seams(frames, vertical_boundaries=(), horizontal_boundaries=(384,), max_ratio=3.0)
+
+ _record_quality(frames, paths, case)
+
+
+@pytest.mark.timeout(9000)
+@pytest.mark.parametrize(("mesh_device", "device_params"), MESHES, indirect=["mesh_device", "device_params"])
+def test_ref2va_conditioning_is_not_a_no_op(mesh_device, reset_seeds):
+ """Discriminator: a same-size inverted reference keeps the noise bit-identical, so any output
+ divergence above the measured run-to-run floor is attributable to reference content alone."""
+ pipeline = _pipeline(mesh_device)
+ normal = _real_frame_image()
+ inverted = _inverted(normal)
+
+ def generate(image):
+ return pipeline(
+ PROMPT,
+ references=[MiniMaxH3Reference(image=image)],
+ num_frames=NUM_FRAMES,
+ height=HEIGHT,
+ width=WIDTH,
+ num_inference_steps=STEPS,
+ seed=SEED,
+ )
+
+ a = generate(normal)
+ a_repeat = generate(normal)
+ b = generate(inverted)
+
+ floor = _divergence(a.video, a_repeat.video)
+ signal = _divergence(a.video, b.video)
+ to_normal = (_clip_resemblance(a, normal), _clip_resemblance(b, normal))
+ to_inverted = (_clip_resemblance(a, inverted), _clip_resemblance(b, inverted))
+ colour = (_colour_distance(a, normal), _colour_distance(b, inverted))
+ crossed = (_colour_distance(a, inverted), _colour_distance(b, normal))
+
+ logger.info(
+ f"ref2va discriminator: run-to-run floor {floor:.6f}, reference-swap signal {signal:.6f} "
+ f"(ratio {signal / max(floor, 1e-9):.1f}x) | CLIP to normal A={to_normal[0]:.4f} "
+ f"B={to_normal[1]:.4f} | CLIP to inverted A={to_inverted[0]:.4f} B={to_inverted[1]:.4f} | "
+ f"colour distance to own reference A={colour[0]:.4f} B={colour[1]:.4f}, "
+ f"to the other A={crossed[0]:.4f} B={crossed[1]:.4f}"
+ )
+ for stem, output in (("discriminate_normal", a), ("discriminate_inverted", b)):
+ _write(output, stem)
+
+ assert floor < 0.02, f"the same request twice diverged by {floor:.6f}; the pipeline is not reproducible"
+
+ # The null hypothesis (reference ignored) scores signal == floor exactly, the noise being bit-identical.
+ assert signal > 10 * floor, (
+ f"swapping the reference moved the output by {signal:.6f} against a run-to-run floor of "
+ f"{floor:.6f}: the reference is not conditioning the output"
+ )
+ assert signal > 0.01, (
+ f"swapping the reference moved the output by only {signal:.6f} mean absolute pixel value; "
+ "the effect is present but too small to call conditioning"
+ )
+
+ # Direction is logged, not asserted: no known instrument here can fail on a correct pipeline only.
+ logger.info(
+ f"ref2va direction (recorded, not asserted): CLIP own-vs-other "
+ f"A {to_normal[0]:.4f} vs {to_inverted[0]:.4f}, B {to_inverted[1]:.4f} vs {to_normal[1]:.4f}; "
+ f"colour own-vs-other A {colour[0]:.4f} vs {crossed[0]:.4f}, B {colour[1]:.4f} vs {crossed[1]:.4f}"
+ )
diff --git a/code/models/tt_dit/tests/models/minimax_h3/test_references_minimax_h3.py b/code/models/tt_dit/tests/models/minimax_h3/test_references_minimax_h3.py
new file mode 100644
index 0000000000000000000000000000000000000000..19a218444123dcd10851a48fcf2eb66402c74b71
--- /dev/null
+++ b/code/models/tt_dit/tests/models/minimax_h3/test_references_minimax_h3.py
@@ -0,0 +1,809 @@
+# SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC
+
+# SPDX-License-Identifier: Apache-2.0
+
+"""MiniMax-H3 ``ref2va`` reference conditioning, gated against the reference implementation's
+own path (``diffusers.modular_pipelines.minimax_h3`` at PR #14355): host gates are bit-exact
+(drift is silent), the device VAE encode is PCC on real media. importorskips that branch."""
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+from types import SimpleNamespace
+
+import numpy as np
+import pytest
+import torch
+from loguru import logger
+
+from ....pipelines.minimax_h3 import packing as p
+from ....pipelines.minimax_h3 import packing_ref2va as rp
+from ....pipelines.minimax_h3 import references as R
+from ....utils.check import assert_quality
+from ....utils.test import ring_params_req_exact_devices
+
+# Base `packing` too: the rotary-clock gate pins both `_temporal_position_span` implementations.
+reference_before_encoder, reference_packing, reference_encoders, reference_base_packing = (
+ pytest.importorskip(module, reason="requires the minimax-h3 diffusers branch")
+ for module in (
+ "diffusers.modular_pipelines.minimax_h3.before_encoder",
+ "diffusers.modular_pipelines.minimax_h3.packing_ref2va",
+ "diffusers.modular_pipelines.minimax_h3.encoders",
+ "diffusers.modular_pipelines.minimax_h3.packing",
+ )
+)
+
+AUDIO_RATE = 32000
+TARGET_FRAMES = 124
+DURATION = TARGET_FRAMES / p.MINIMAX_H3_FPS # 5.1667 s
+
+TARGET_HEIGHT, TARGET_WIDTH = 768, 1344
+VAE_RATIO = 16
+AUDIO_HOP = 800
+PATCH_SIZE = (1, 2, 2)
+
+
+def _components():
+ return SimpleNamespace(audio_sampling_rate=AUDIO_RATE)
+
+
+def _rng():
+ return np.random.default_rng(0)
+
+
+def _image(width: int, height: int) -> np.ndarray:
+ """Structured content, not flat: a resize of a constant field cannot show an error."""
+ rng = _rng()
+ return (rng.random((height, width, 3)) * 255).astype(np.uint8)
+
+
+def _video(width: int, height: int, num_frames: int) -> np.ndarray:
+ rng = _rng()
+ return (rng.random((num_frames, height, width, 3)) * 255).astype(np.uint8)
+
+
+def _waveform(seconds: float, sample_rate: int = AUDIO_RATE, channels: int = 2) -> torch.Tensor:
+ torch.manual_seed(0)
+ return torch.randn(channels, int(seconds * sample_rate)) * 0.1
+
+
+def _pair(specs: list[dict]):
+ ours = [rp.MiniMaxH3Reference(**spec) for spec in specs]
+ theirs = [reference_packing.MiniMaxH3Reference(**spec) for spec in specs]
+ return ours, theirs
+
+
+SPECS = {
+ "image": dict(image=_image(1024, 1024)),
+ "image_16to9": dict(image=_image(1920, 1080)),
+ "video_24fps_at_canvas": dict(video=_video(1344, 768, TARGET_FRAMES), fps=24.0),
+ "video_30fps_off_canvas": dict(video=_video(1920, 1080, 60), fps=30.0),
+ "video_with_sound": dict(video=_video(1344, 768, TARGET_FRAMES), fps=24.0, audio=_waveform(DURATION)),
+ "audio": dict(audio=_waveform(DURATION)),
+ "audio_44k_mono": dict(audio=_waveform(DURATION, 44100, 1), sample_rate=44100),
+}
+
+
+def test_reference_limits_match_reference_and_are_enforced(expect_error):
+ assert rp.MINIMAX_H3_MAX_REFERENCE_IMAGES == reference_packing.MINIMAX_H3_MAX_REFERENCE_IMAGES == 9
+ assert rp.MINIMAX_H3_MAX_REFERENCE_VIDEOS == reference_packing.MINIMAX_H3_MAX_REFERENCE_VIDEOS == 3
+ assert rp.MINIMAX_H3_MAX_REFERENCE_AUDIOS == reference_packing.MINIMAX_H3_MAX_REFERENCE_AUDIOS == 3
+ assert rp.MINIMAX_H3_MAX_REFERENCES == reference_packing.MINIMAX_H3_MAX_REFERENCES == 12
+ assert rp.MINIMAX_H3_REFERENCE_IMAGE_SHORT_EDGE == reference_packing.MINIMAX_H3_REFERENCE_IMAGE_SHORT_EDGE == 2048
+
+ image = rp.MiniMaxH3Reference(image=_image(256, 256))
+ video = rp.MiniMaxH3Reference(video=_video(256, 256, 10), fps=24.0)
+ audio = rp.MiniMaxH3Reference(audio=_waveform(DURATION))
+
+ assert R.check_references([image, video, audio]) == ["image", "video", "audio"]
+
+ with expect_error(ValueError, "at least one reference"):
+ R.check_references([])
+ with expect_error(ValueError, "at most 9 image"):
+ R.check_references([image] * 10)
+ with expect_error(ValueError, "at most 3 video"):
+ R.check_references([video] * 4)
+ with expect_error(ValueError, "at most 3 audio"):
+ R.check_references([image, audio, audio, audio, audio])
+ with expect_error(ValueError, "at most 12 references"):
+ R.check_references([image] * 9 + [video] * 3 + [audio])
+ with expect_error(ValueError, "paired with at least one image or video"):
+ R.check_references([audio])
+
+
+@pytest.mark.parametrize("name", list(SPECS), ids=list(SPECS))
+def test_prepare_references_matches_reference(name):
+ spec = SPECS[name]
+ # audio may not be alone; pair with an image
+ specs = [spec] if "audio" not in name else [SPECS["image"], spec]
+ ours, theirs = _pair(specs)
+
+ got, got_frames = R.prepare_references(ours, TARGET_FRAMES, AUDIO_RATE)
+ want, want_frames = reference_before_encoder.MiniMaxH3Ref2VASetupStep.prepare_references(
+ _components(), theirs, TARGET_FRAMES
+ )
+
+ assert got_frames == want_frames == TARGET_FRAMES
+ assert len(got) == len(want)
+ for index, (a, b) in enumerate(zip(got, want)):
+ assert a.kind == b.kind, index
+ assert a.has_audio == b.has_audio, index
+ if a.kind == "image":
+ assert np.array_equal(np.asarray(a.image), np.asarray(b.image)), f"prepared image {index} differs"
+ assert a.image.size == b.image.size
+ if a.kind == "video":
+ assert np.array_equal(a.frames, b.frames), f"prepared frames {index} differ"
+ if a.has_audio:
+ assert torch.equal(a.waveform, b.waveform), f"prepared waveform {index} differs"
+ assert a.waveform.shape[0] == p.MINIMAX_H3_AUDIO_CHANNELS
+
+
+def test_prepare_references_uses_each_references_own_resolution():
+ references = [
+ rp.MiniMaxH3Reference(image=_image(1024, 1024)),
+ rp.MiniMaxH3Reference(video=_video(1920, 1080, 30), fps=24.0),
+ ]
+ prepared, _ = R.prepare_references(references, TARGET_FRAMES, AUDIO_RATE)
+
+ assert prepared[0].image.size == (2048, 2048)
+ assert min(prepared[0].image.size) == rp.MINIMAX_H3_REFERENCE_IMAGE_SHORT_EDGE
+ assert prepared[1].frames.shape[1:3] == (768, 1344)
+ assert min(prepared[1].frames.shape[1:3]) == p.MINIMAX_H3_SHORT_EDGE
+ assert min(prepared[0].image.size) > min(prepared[1].frames.shape[1:3])
+
+
+def test_prepare_references_truncates_a_video_and_its_soundtrack_to_the_target():
+ long_frames = _video(1344, 768, TARGET_FRAMES + 60)
+ references = [rp.MiniMaxH3Reference(video=long_frames, fps=24.0, audio=_waveform(10.0))]
+ prepared, num_frames = R.prepare_references(references, TARGET_FRAMES, AUDIO_RATE)
+
+ assert num_frames == TARGET_FRAMES
+ assert prepared[0].frames.shape[0] == TARGET_FRAMES
+ assert prepared[0].waveform.shape[1] == int(DURATION * AUDIO_RATE)
+
+
+def test_num_frames_may_be_derived_from_a_single_audio_bearing_reference():
+ references = [rp.MiniMaxH3Reference(image=_image(512, 512)), rp.MiniMaxH3Reference(audio=_waveform(6.0))]
+ _, num_frames = R.prepare_references(references, None, AUDIO_RATE)
+ assert num_frames == p.align_num_frames(round(6.0 * p.MINIMAX_H3_FPS))
+ assert num_frames % p.MINIMAX_H3_FRAMES_PER_CHUNK == p.MINIMAX_H3_LATENTS_PER_CHUNK
+ theirs = [
+ reference_packing.MiniMaxH3Reference(image=_image(512, 512)),
+ reference_packing.MiniMaxH3Reference(audio=_waveform(6.0)),
+ ]
+ _, want = reference_before_encoder.MiniMaxH3Ref2VASetupStep.prepare_references(_components(), theirs, None)
+ assert num_frames == want
+
+
+def test_num_frames_is_ambiguous_with_two_soundtracks(expect_error):
+ references = [
+ rp.MiniMaxH3Reference(video=_video(768, 768, 24), fps=24.0, audio=_waveform(6.0)),
+ rp.MiniMaxH3Reference(audio=_waveform(7.0)),
+ ]
+ with expect_error(ValueError, "exactly one of them carries audio"):
+ R.prepare_references(references, None, AUDIO_RATE)
+
+
+@pytest.mark.parametrize("seconds", [4.0, 16.0])
+def test_a_derived_duration_outside_the_models_range_is_rejected(seconds, expect_error):
+ references = [rp.MiniMaxH3Reference(image=_image(512, 512)), rp.MiniMaxH3Reference(audio=_waveform(seconds))]
+ with expect_error(ValueError, "seconds"):
+ R.prepare_references(references, None, AUDIO_RATE)
+
+
+@pytest.mark.parametrize("seconds", [DURATION, 5.0, 8.0, 15.0])
+def test_pad_waveform_to_hop_reproduces_the_reference_latent_count(seconds):
+ waveform = _waveform(seconds)
+ padded = R.pad_waveform_to_hop(waveform)
+
+ assert padded.shape[-1] % R.MINIMAX_H3_AUDIO_HOP == 0
+ assert padded.shape[-1] - waveform.shape[-1] < R.MINIMAX_H3_AUDIO_HOP
+ assert torch.equal(padded[:, : waveform.shape[-1]], waveform), "padding must not disturb the samples"
+ assert (padded[:, waveform.shape[-1] :] == 0).all(), "the pad is zeros, as F.pad's default is"
+ assert padded.shape[-1] // R.MINIMAX_H3_AUDIO_HOP == int(np.ceil(waveform.shape[-1] / R.MINIMAX_H3_AUDIO_HOP))
+
+
+def test_production_soundtrack_is_not_a_whole_number_of_hops():
+ """The case that makes the padding load-bearing rather than defensive."""
+ samples = int(DURATION * AUDIO_RATE)
+ assert samples == 165333
+ assert samples % R.MINIMAX_H3_AUDIO_HOP != 0
+ assert R.pad_waveform_to_hop(_waveform(DURATION)).shape[-1] // R.MINIMAX_H3_AUDIO_HOP == 207
+
+
+def test_span_matches_both_reference_summation_orders():
+ for n in range(1, 61):
+ assert p._temporal_position_span(n) == reference_base_packing._temporal_position_span(
+ n
+ ), f"pairwise span disagrees with the reference at n={n}"
+ assert rp._temporal_position_span(n) == reference_packing._temporal_position_span(
+ n
+ ), f"sequential span disagrees with the reference at n={n}"
+
+
+def test_the_two_span_orders_actually_differ():
+ assert p._temporal_position_span(15) == rp._temporal_position_span(15), "expected agreement below 16"
+ assert p._temporal_position_span(16) != rp._temporal_position_span(16), "expected divergence from 16 latent frames"
+
+ production = p.video_latent_num_frames(TARGET_FRAMES)
+ assert production == 37
+ pairwise, sequential = p._temporal_position_span(production), rp._temporal_position_span(production)
+ assert pairwise != sequential, "the two orders must differ at the production frame count"
+ assert rp._temporal_position_span(16) > p._temporal_position_span(16)
+ assert sequential < pairwise
+
+
+def _target():
+ return (
+ p.video_latent_num_frames(TARGET_FRAMES),
+ TARGET_HEIGHT // VAE_RATIO,
+ TARGET_WIDTH // VAE_RATIO,
+ p.audio_latent_num_frames(TARGET_FRAMES),
+ )
+
+
+def _image_geometry(source_width: int, source_height: int):
+ height, width = rp.resolve_reference_image_size(source_width, source_height)
+ return dict(num_latent_frames=1, latent_height=height // VAE_RATIO, latent_width=width // VAE_RATIO)
+
+
+def _video_geometry(source_width: int, source_height: int, num_frames: int = TARGET_FRAMES):
+ frames = np.zeros((num_frames, source_height, source_width, 3), dtype=np.uint8)
+ frames = rp.prepare_reference_frames(frames, TARGET_FRAMES)
+ trimmed = rp.trim_reference_num_frames(frames.shape[0])
+ return dict(
+ num_latent_frames=p.video_latent_num_frames(trimmed),
+ latent_height=frames.shape[1] // VAE_RATIO,
+ latent_width=frames.shape[2] // VAE_RATIO,
+ )
+
+
+def _audio_latents(seconds: float) -> int:
+ return int(np.ceil(int(seconds * AUDIO_RATE) / AUDIO_HOP))
+
+
+def _spec_to_prepared(spec, dataclass):
+ kind = spec["kind"]
+ geometry = {}
+ if kind == "image":
+ geometry = _image_geometry(*spec["source"])
+ elif kind == "video":
+ geometry = _video_geometry(*spec["source"], spec.get("num_frames", TARGET_FRAMES))
+ audio_seconds = spec.get("audio_seconds")
+ return dataclass(
+ kind=kind,
+ has_audio=audio_seconds is not None,
+ num_audio_latents=0 if audio_seconds is None else _audio_latents(audio_seconds),
+ **geometry,
+ )
+
+
+def _both(spec_list):
+ ours = [_spec_to_prepared(spec, rp.MiniMaxH3PreparedReference) for spec in spec_list]
+ theirs = [_spec_to_prepared(spec, reference_packing.MiniMaxH3PreparedReference) for spec in spec_list]
+ return ours, theirs
+
+
+IMAGE_1TO1 = dict(kind="image", source=(1024, 1024))
+IMAGE_16TO9 = dict(kind="image", source=(1920, 1080))
+VIDEO_SOUND = dict(kind="video", source=(1344, 768), audio_seconds=DURATION)
+# 1:1 sounded video: its soundtrack rows must pin to its OWN width grid, not the target's.
+VIDEO_SOUND_1TO1 = dict(kind="video", source=(768, 768), audio_seconds=DURATION)
+VIDEO_SILENT = dict(kind="video", source=(1344, 768))
+AUDIO_ONLY = dict(kind="audio", audio_seconds=DURATION)
+
+# Awkward order on purpose: exercises the label counters, soundtrack-before-video order and the
+# shared rotary clock; the reversed copy is the host order-sensitivity gate (order is semantic).
+_NINE_MIXED = [
+ VIDEO_SOUND,
+ AUDIO_ONLY,
+ VIDEO_SILENT,
+ IMAGE_1TO1,
+ AUDIO_ONLY,
+ IMAGE_16TO9,
+ VIDEO_SOUND,
+ IMAGE_1TO1,
+ AUDIO_ONLY,
+]
+
+CASES = {
+ "one_image": [IMAGE_1TO1],
+ "one_audio": [IMAGE_1TO1, AUDIO_ONLY], # audio may never be the only reference
+ "video_with_sound": [VIDEO_SOUND],
+ "video_with_sound_1to1": [VIDEO_SOUND_1TO1],
+ "video_without_sound": [VIDEO_SILENT],
+ "nine_mixed_awkward": _NINE_MIXED,
+ "nine_mixed_reversed": list(reversed(_NINE_MIXED)),
+}
+
+TEXT_LEN = 1024
+# A reference's vision block lands inside the text rows tagged as VIDEO, not text.
+VISION_BLOCK = slice(30, 120)
+
+
+def _text_tags():
+ tags = torch.ones(TEXT_LEN, dtype=torch.long)
+ tags[VISION_BLOCK] = p.MINIMAX_H3_VIDEO_TAG
+ return tags
+
+
+@pytest.mark.parametrize("case", list(CASES), ids=list(CASES))
+def test_layout_matches_reference(case):
+ ours, theirs = _both(CASES[case])
+ num_latent_frames, latent_height, latent_width, num_audio_latents = _target()
+ args = (num_latent_frames, latent_height, latent_width, num_audio_latents, PATCH_SIZE)
+
+ got = rp.build_ref2va_packed_sequence(_text_tags(), ours, *args)
+ want = reference_packing.build_ref2va_packed_sequence(_text_tags(), theirs, *args)
+
+ assert got.sequence_length == want.sequence_length
+ assert got.position_ids.dtype == torch.float64
+ assert torch.equal(got.position_ids, want.position_ids), "the fp64 rotary grid is not bit-exact"
+ assert torch.equal(got.token_tags, want.token_tags)
+ assert torch.equal(got.video_indices, want.video_indices)
+ assert torch.equal(got.audio_indices, want.audio_indices)
+ assert torch.equal(got.text_indices, want.text_indices)
+ assert got.num_condition_video_rows == want.num_condition_video_rows
+ assert got.num_condition_audio_rows == want.num_condition_audio_rows
+
+
+# `(width, height)` in, `(height, width)` out; both orientations listed so a transpose cannot pass.
+@pytest.mark.parametrize(
+ "source,expected",
+ [
+ ((1024, 1024), (2048, 2048)),
+ ((1920, 1080), (2048, 3648)),
+ ((1080, 1920), (3648, 2048)),
+ ((4096, 1024), (2048, 8192)),
+ ],
+)
+def test_resolve_reference_image_size(source, expected):
+ assert rp.resolve_reference_image_size(*source) == expected
+ assert rp.resolve_reference_image_size(*source) == reference_packing.resolve_reference_image_size(*source)
+ height, width = expected
+ assert height % p.MINIMAX_H3_CANVAS_MULTIPLE == 0 and width % p.MINIMAX_H3_CANVAS_MULTIPLE == 0
+ assert min(height, width) == rp.MINIMAX_H3_REFERENCE_IMAGE_SHORT_EDGE
+
+
+def test_resolve_reference_image_size_rejects_out_of_range(expect_error):
+ with expect_error(ValueError, "within 1:4 and 4:1"):
+ rp.resolve_reference_image_size(5000, 1000)
+ with expect_error(ValueError, "positive size"):
+ rp.resolve_reference_image_size(0, 100)
+
+
+@pytest.mark.parametrize("fps", [24.0, 30.0, 25.0, 12.0, 60.0, 23.976])
+def test_resample_reference_frames_matches_reference(fps):
+ frames = np.arange(50 * 2 * 2 * 3, dtype=np.uint8).reshape(50, 2, 2, 3)
+ got = rp.resample_reference_frames(frames, fps)
+ want = reference_packing.resample_reference_frames(frames, fps)
+ assert got.shape == want.shape
+ assert np.array_equal(got, want)
+ if fps == 24.0:
+ assert got is frames
+
+
+def test_prepare_reference_frames_uses_its_own_aspect_canvas():
+ frames = np.zeros((10, 480, 480, 3), dtype=np.uint8)
+ prepared = rp.prepare_reference_frames(frames, TARGET_FRAMES)
+ assert prepared.shape[1:3] == p.resolve_canvas_size(480, 480) == (768, 768)
+ assert np.array_equal(prepared, reference_packing.prepare_reference_frames(frames, TARGET_FRAMES))
+
+ # `shares_memory`, not `is`: the frame-count cap slices first, so a view comes back.
+ at_canvas = np.zeros((10, 768, 1344, 3), dtype=np.uint8)
+ passthrough = rp.prepare_reference_frames(at_canvas, TARGET_FRAMES)
+ assert np.shares_memory(passthrough, at_canvas)
+
+
+@pytest.mark.parametrize("num_frames", [1, 5, 12, 24, 25, 124, 192])
+def test_sample_reference_video_frames_matches_reference(num_frames):
+ frames = np.zeros((num_frames, 4, 4, 3), dtype=np.uint8)
+ got_frames, got_ts = rp.sample_reference_video_frames(frames)
+ want_frames, want_ts = reference_packing.sample_reference_video_frames(frames)
+ assert len(got_frames) == len(want_frames)
+ assert got_ts == want_ts
+ assert len(got_ts) == -(-len(got_frames) // rp.MINIMAX_H3_QWEN_TEMPORAL_PATCH)
+
+
+def test_block_timestamps_round_half_to_even():
+ frames = np.zeros((124, 4, 4, 3), dtype=np.uint8)
+ _, timestamps = rp.sample_reference_video_frames(frames)
+ assert timestamps[0] == 0.25
+ assert f"{timestamps[0]:.1f}" == "0.2"
+
+
+@pytest.mark.parametrize("num_frames", [1, 5, 22, 39, 124, 125, 130])
+def test_trim_reference_num_frames_matches_reference(num_frames):
+ got = rp.trim_reference_num_frames(num_frames)
+ assert got == reference_packing.trim_reference_num_frames(num_frames)
+ assert got % p.MINIMAX_H3_FRAMES_PER_CHUNK == p.MINIMAX_H3_LATENTS_PER_CHUNK
+
+
+def test_trim_reference_num_frames_has_a_22_frame_floor():
+ """1..23 frames all map to 22 -- not a plain round-down; a "fixed" floor of 5 silently changes the latent count."""
+ for num_frames in (1, 5, 10, 22, 23):
+ assert rp.trim_reference_num_frames(num_frames) == 22
+ assert rp.trim_reference_num_frames(num_frames) == reference_packing.trim_reference_num_frames(num_frames)
+ assert rp.trim_reference_num_frames(39) == 39
+ assert rp.trim_reference_num_frames(55) == 39
+ assert rp.trim_reference_num_frames(125) == 124
+
+
+@pytest.mark.parametrize("channels", [1, 2])
+@pytest.mark.parametrize("sample_rate", [32000, 44100])
+def test_prepare_reference_waveform_matches_reference(channels, sample_rate):
+ torch.manual_seed(0)
+ waveform = torch.randn(channels, int(6.0 * sample_rate))
+ got = rp.prepare_reference_waveform(waveform, sample_rate, AUDIO_RATE, DURATION)
+ want = reference_packing.prepare_reference_waveform(waveform, sample_rate, AUDIO_RATE, DURATION)
+ assert got.shape == want.shape
+ assert torch.equal(got, want)
+ assert got.shape[0] == p.MINIMAX_H3_AUDIO_CHANNELS
+ if sample_rate == AUDIO_RATE:
+ assert got.shape[1] == int(DURATION * sample_rate)
+
+
+def test_reference_media_to_uint8_matches_reference():
+ torch.manual_seed(0)
+ tensor = torch.rand(3, 8, 6)
+ array = (np.random.default_rng(0).random((8, 6, 3)) * 255).astype(np.uint8)
+ for media in (tensor, array, [tensor, tensor]):
+ got = rp.reference_media_to_uint8(media)
+ want = reference_packing.reference_media_to_uint8(media)
+ assert got.dtype == np.uint8
+ assert np.array_equal(got, want)
+
+
+def test_reference_requires_exactly_one_medium(expect_error):
+ """A video may carry audio; nothing else may carry two media."""
+ image = np.zeros((4, 4, 3), dtype=np.uint8)
+ video = np.zeros((2, 4, 4, 3), dtype=np.uint8)
+ waveform = torch.zeros(2, 800)
+
+ assert rp.MiniMaxH3Reference(image=image).kind == "image"
+ assert rp.MiniMaxH3Reference(video=video).kind == "video"
+ assert rp.MiniMaxH3Reference(audio=waveform).kind == "audio"
+ sounded = rp.MiniMaxH3Reference(video=video, audio=waveform)
+ assert sounded.kind == "video" and sounded.has_audio
+
+ with expect_error(ValueError, "exactly one"):
+ rp.MiniMaxH3Reference(image=image, video=video)
+ with expect_error(ValueError, "exactly one"):
+ rp.MiniMaxH3Reference(image=image, audio=waveform)
+ with expect_error(ValueError, "exactly one"):
+ rp.MiniMaxH3Reference()
+
+
+def test_reference_refuses_a_path(expect_error):
+ with expect_error(ValueError, "never opens media files"):
+ rp.MiniMaxH3Reference(image="subject.png")
+
+
+def test_reference_defaults_fps_to_the_models_own():
+ video = np.zeros((2, 4, 4, 3), dtype=np.uint8)
+ assert rp.MiniMaxH3Reference(video=video).fps == float(p.MINIMAX_H3_FPS)
+
+
+def _prepared(kind, *, frames=1, height=48, width=84, audio_latents=0):
+ return rp.MiniMaxH3PreparedReference(
+ kind=kind,
+ has_audio=audio_latents > 0,
+ num_latent_frames=frames,
+ latent_height=height if kind != "audio" else 0,
+ latent_width=width if kind != "audio" else 0,
+ num_audio_latents=audio_latents,
+ )
+
+
+def test_split_condition_blocks_is_in_packed_order():
+ """Per-modality order would keep every row count right and desynchronize the whole region."""
+ references = [
+ _prepared("video", frames=37, audio_latents=207),
+ _prepared("audio", audio_latents=100),
+ _prepared("image"),
+ ]
+ video_rows = torch.arange(sum(r.num_video_rows for r in references if r.kind != "audio")).float()[:, None]
+ audio_rows = torch.arange(sum(r.num_audio_rows for r in references)).float()[:, None] + 10000
+
+ blocks = R.split_condition_blocks(references, video_rows, audio_rows)
+ assert [kind for _, kind in blocks] == ["audio", "video", "audio", "video"]
+ assert [block.shape[0] for block, _ in blocks] == [
+ references[0].num_audio_rows,
+ references[0].num_video_rows,
+ references[1].num_audio_rows,
+ references[2].num_video_rows,
+ ]
+
+ assert torch.equal(blocks[0][0], audio_rows[: references[0].num_audio_rows])
+ assert torch.equal(blocks[1][0], video_rows[: references[0].num_video_rows])
+ assert torch.equal(blocks[2][0], audio_rows[references[0].num_audio_rows :])
+ assert torch.equal(blocks[3][0], video_rows[references[0].num_video_rows :])
+
+
+def test_split_condition_blocks_row_count_matches_the_layout():
+ references = [
+ _prepared("image", height=128, width=128),
+ _prepared("video", frames=37, audio_latents=207),
+ _prepared("audio", audio_latents=207),
+ ]
+ layout = rp.build_ref2va_packed_sequence(torch.ones(64, dtype=torch.long), references, 37, 48, 84, 207, (1, 2, 2))
+ video_rows = torch.zeros(layout.num_condition_video_rows, 96)
+ audio_rows = torch.zeros(layout.num_condition_audio_rows, 32)
+
+ blocks = R.split_condition_blocks(references, video_rows, audio_rows)
+ assert sum(block.shape[0] for block, _ in blocks) == (
+ layout.num_condition_video_rows + layout.num_condition_audio_rows
+ )
+
+
+def test_split_condition_blocks_rejects_a_row_count_mismatch(expect_error):
+ references = [_prepared("image")]
+ with expect_error(ValueError, "consumed"):
+ R.split_condition_blocks(references, torch.zeros(references[0].num_video_rows + 7, 96), None)
+
+
+def test_reference_condition_shapes_skips_audio_references():
+ references = [_prepared("image"), _prepared("audio", audio_latents=50), _prepared("video", frames=37)]
+ shapes = R.reference_condition_shapes(references)
+ assert shapes == ((1, 48, 84), (37, 48, 84))
+
+
+def test_normalize_reference_pixels_uses_imagenet_statistics():
+ """Not [-1, 1]: H3 conditions on VLM-style normalized pixels."""
+ frames = np.full((1, 32, 32, 3), 128, dtype=np.uint8)
+ pixels = R.normalize_reference_pixels(frames)
+ assert pixels.shape == (1, 3, 1, 32, 32)
+ expected = [(128 / 255.0 - m) / s for m, s in zip(R.MINIMAX_H3_PIXEL_MEAN, R.MINIMAX_H3_PIXEL_STD)]
+ for channel, value in enumerate(expected):
+ assert torch.allclose(pixels[0, channel], torch.full((1, 32, 32), value), atol=1e-6)
+ assert R.normalize_reference_pixels(np.zeros((5, 8, 8, 3), dtype=np.uint8)).shape == (1, 3, 5, 8, 8)
+
+
+REFERENCE_MEDIA = Path.home() / "h3_fl2va_artifacts" / "fl2va_first.mp4"
+
+
+def _real_media() -> Path:
+ if not REFERENCE_MEDIA.is_file():
+ pytest.skip(f"no reference media at {REFERENCE_MEDIA}; place a video with a soundtrack there")
+ return REFERENCE_MEDIA
+
+
+def _weights_dir() -> Path:
+ directory = Path(os.environ.get("MINIMAX_H3_MODEL_PATH", ""))
+ if not directory.is_dir():
+ pytest.skip("set MINIMAX_H3_MODEL_PATH to a diffusers snapshot of the checkpoint")
+ return directory
+
+
+_L1_SMALL = 16384 # 65536 and 32768 clash with the taps=3 encoder CBs
+
+# The e2e ring fabric params: a LINE config happens to pass here but production never runs it.
+MESH_4X8 = [
+ pytest.param(
+ (4, 8),
+ {**ring_params_req_exact_devices, "l1_small_size": _L1_SMALL},
+ id="4x8",
+ )
+]
+
+REFERENCE_PCC = 0.99 # floor set by the encoder's ttnn.group_norm calls (no fp32 path)
+# 17*1 + 5: smallest multi-chunk count (7 latent frames); the reference video VAE encode is CPU-hours at 124.
+VIDEO_FRAMES = 22
+
+
+def _reference_components(weights: Path, device: str = "cpu"):
+ from diffusers.models import AutoencoderKLMiniMaxH3, AutoencoderKLMiniMaxH3Audio
+
+ vae = AutoencoderKLMiniMaxH3.from_pretrained(str(weights), subfolder="vae").to(device).eval()
+ audio_vae = AutoencoderKLMiniMaxH3Audio.from_pretrained(str(weights), subfolder="audio_vae").to(device).eval()
+ return SimpleNamespace(
+ vae=vae,
+ audio_vae=audio_vae,
+ patch_size=(1, 2, 2),
+ audio_latent_channels=audio_vae.config.latent_channels,
+ _execution_device=torch.device(device),
+ )
+
+
+def _real_frame() -> np.ndarray:
+ """Not randn: the fp16 round trip is only exercised on natural image statistics."""
+ frames, _, _ = rp.decode_reference_video(_real_media())
+ return frames[0]
+
+
+def _reference_case(case: str):
+ """Audio pairs with an image (audio-only is forbidden); the pairing is inert for the comparison."""
+ media = _real_media()
+ frames, fps, soundtrack = rp.decode_reference_video(media)
+ waveform, sample_rate = soundtrack
+
+ if case == "image":
+ specs = [dict(image=_real_frame())]
+ elif case == "video":
+ specs = [dict(video=frames[:VIDEO_FRAMES], fps=fps)]
+ elif case == "audio":
+ specs = [dict(image=_real_frame()), dict(audio=waveform, sample_rate=sample_rate)]
+ else:
+ raise ValueError(case)
+
+ ours, _ = R.prepare_references([rp.MiniMaxH3Reference(**s) for s in specs], VIDEO_FRAMES, AUDIO_RATE)
+ theirs, _ = reference_before_encoder.MiniMaxH3Ref2VASetupStep.prepare_references(
+ _components(), [reference_packing.MiniMaxH3Reference(**s) for s in specs], VIDEO_FRAMES
+ )
+ return ours, theirs
+
+
+@pytest.mark.timeout(7200)
+@pytest.mark.parametrize(("mesh_device", "device_params"), MESH_4X8, indirect=["mesh_device", "device_params"])
+@pytest.mark.parametrize("case", ["image", "audio", "video"])
+def test_encode_references_matches_reference(mesh_device, case, reset_seeds):
+ """Per modality (the three encode paths fail independently); geometry asserted too, since the layout is built from it."""
+ from ....pipelines.minimax_h3.pipeline_minimax_h3 import MiniMaxH3Pipeline
+
+ weights = _weights_dir()
+ ours, theirs = _reference_case(case)
+
+ pipeline = MiniMaxH3Pipeline.create_pipeline(mesh_device=mesh_device, weights_dir=weights, task="ref2va")
+ needs_video = any(reference.kind == "video" for reference in ours)
+ vae = pipeline._prepare_vae()
+ vae = pipeline._prepare_vae(encode_shape=(1, vae.tile_size, vae.tile_size), encode_taps=1)
+ if needs_video:
+ vae = pipeline._prepare_vae(
+ encode_shape=(pipeline.vae_config.clip_length, vae.tile_size, vae.tile_size), encode_taps=3
+ )
+ audio_encoder = pipeline._prepare_audio_encoder() if any(r.has_audio for r in ours) else None
+
+ got_video, got_audio = R.encode_references(
+ ours,
+ encode_clip=vae.encode_clip,
+ encode_video=vae.encode if needs_video else None,
+ encode_audio=(lambda waveform: audio_encoder(waveform)[0]) if audio_encoder else None,
+ latents_mean=pipeline.vae_config.latents_mean,
+ latents_std=pipeline.vae_config.latents_std,
+ audio_latents_mean=pipeline.audio_config["latents_mean"],
+ audio_latents_std=pipeline.audio_config["latents_std"],
+ patch_size=pipeline.patch_size,
+ audio_latent_channels=pipeline.audio_config["latent_channels"],
+ )
+
+ components = _reference_components(weights)
+ with torch.no_grad():
+ want_video, want_audio = reference_encoders.MiniMaxH3Ref2VAReferenceEncoderStep.encode_references(
+ components, theirs, device=torch.device("cpu")
+ )
+
+ for index, (a, b) in enumerate(zip(ours, theirs)):
+ assert (a.num_latent_frames, a.latent_height, a.latent_width) == (
+ b.num_latent_frames,
+ b.latent_height,
+ b.latent_width,
+ ), f"reference {index} resolved to different visual latent geometry"
+ assert a.num_audio_latents == b.num_audio_latents, f"reference {index} resolved a different audio length"
+ logger.info(
+ f"[{case}] resolved geometry: "
+ + ", ".join(
+ f"{r.kind}({r.num_latent_frames}x{r.latent_height}x{r.latent_width}, audio {r.num_audio_latents})"
+ for r in ours
+ )
+ )
+
+ assert got_video.shape == want_video.shape, f"{tuple(got_video.shape)} != {tuple(want_video.shape)}"
+ logger.info(f"[{case}] video condition rows {tuple(got_video.shape)}")
+ assert_quality(want_video, got_video, pcc=REFERENCE_PCC)
+
+ if got_audio is not None:
+ assert got_audio.shape == want_audio.shape, f"{tuple(got_audio.shape)} != {tuple(want_audio.shape)}"
+ logger.info(f"[{case}] audio condition rows {tuple(got_audio.shape)}")
+ # The soundtrack takes the posterior MEAN, so it carries no sampling noise at all.
+ assert_quality(want_audio, got_audio, pcc=REFERENCE_PCC)
+ else:
+ assert want_audio is None, "the reference produced audio rows where we produced none"
+
+
+def _tokenizer():
+ weights = os.environ.get("MINIMAX_H3_MODEL_PATH")
+ if not weights or not (Path(weights) / "tokenizer").is_dir():
+ pytest.skip("needs MINIMAX_H3_MODEL_PATH pointing at a diffusers snapshot with tokenizer/")
+ from transformers import AutoTokenizer
+
+ return AutoTokenizer.from_pretrained(str(weights), subfolder="tokenizer")
+
+
+@pytest.mark.parametrize("case", list(CASES), ids=list(CASES))
+def test_presentation_matches_reference(case):
+ """The tags are the load-bearing half: H3 tags the WHOLE vision block video, sentinels included."""
+ tokenizer = _tokenizer()
+ ours, theirs = _both(CASES[case])
+ image_counts = [4096 for reference in ours if reference.kind == "image"]
+ video_counts = [1008 for reference in ours if reference.kind == "video"]
+ for prepared in (ours, theirs):
+ for reference in prepared:
+ if reference.kind == "video":
+ reference.block_timestamps = [0.25, 1.25, 2.25, 3.25, 4.25, 5.0]
+
+ prompt = "a red fox stepping through wet grass at dawn"
+ got_ids, got_tags = rp.build_ref2va_presentation(tokenizer, prompt, ours, image_counts, video_counts)
+ want_ids, want_tags = reference_packing.build_ref2va_presentation(
+ tokenizer, prompt, theirs, image_counts, video_counts
+ )
+ assert got_ids == want_ids
+ assert got_tags == want_tags
+ assert len(got_tags) == len(got_ids)
+
+
+class _ProcessorStub:
+ def __init__(self, weights: Path):
+ from transformers import AutoImageProcessor, AutoTokenizer, AutoVideoProcessor
+
+ self.tokenizer = AutoTokenizer.from_pretrained(str(weights), subfolder="tokenizer")
+ self.image_processor = AutoImageProcessor.from_pretrained(str(weights), subfolder="text_encoder")
+ self.video_processor = AutoVideoProcessor.from_pretrained(str(weights), subfolder="text_encoder")
+
+
+def _build(stub, prompt, references):
+ from ....pipelines.minimax_h3.pipeline_minimax_h3 import MiniMaxH3Pipeline
+
+ return MiniMaxH3Pipeline._build_ref2va_presentation(stub, prompt, references)
+
+
+def test_presentation_orders_vision_patches_by_reference_not_by_batch():
+ """`_scatter_rows` consumes tower rows in run order, so processor-batch order would swap blocks."""
+ stub = _ProcessorStub(_weights_dir())
+ video = rp.reference_from_video_file(_real_media(), with_audio=False)
+ image = rp.MiniMaxH3Reference(image=_image(1024, 1024))
+
+ prepared, _ = R.prepare_references([video, image], TARGET_FRAMES, AUDIO_RATE)
+ input_ids, tags, type_ids, pixel_values, grid_thw, kinds = _build(stub, "a prompt", prepared)
+
+ assert kinds == ["video", "image"], f"vision entries are in {kinds}, not presentation order"
+ assert int(grid_thw[0][0]) == len(prepared[0].block_timestamps) > 1
+ assert tuple(grid_thw[1].tolist()) == (1, 128, 128)
+ assert pixel_values.shape[0] == sum(int(grid.prod()) for grid in grid_thw)
+
+ reversed_prepared, _ = R.prepare_references([image, video], TARGET_FRAMES, AUDIO_RATE)
+ _, _, _, _, reversed_grid, reversed_kinds = _build(stub, "a prompt", reversed_prepared)
+ assert reversed_kinds == ["image", "video"]
+ assert tuple(reversed_grid[0].tolist()) == (1, 128, 128)
+
+
+def test_presentation_vision_runs_line_up_with_the_towers_rows():
+ from ....encoders.qwen3vl.model_qwen3vl import vision_token_runs
+
+ stub = _ProcessorStub(_weights_dir())
+ references = [
+ rp.MiniMaxH3Reference(image=_image(1024, 1024)),
+ rp.reference_from_video_file(_real_media(), with_audio=False),
+ rp.MiniMaxH3Reference(audio=_waveform(DURATION)),
+ ]
+ prepared, _ = R.prepare_references(references, TARGET_FRAMES, AUDIO_RATE)
+ input_ids, tags, type_ids, pixel_values, grid_thw, kinds = _build(stub, "a prompt", prepared)
+
+ pad_ids = [stub.tokenizer.convert_tokens_to_ids(t) for t in ("<|image_pad|>", "<|video_pad|>")]
+ runs = vision_token_runs(input_ids, pad_ids)
+ merge = stub.image_processor.merge_size**2
+
+ assert len(runs) == sum(int(grid[0]) for grid in grid_thw)
+ assert sum(length for _, length in runs) == sum(int(grid.prod()) for grid in grid_thw) // merge
+ # Runs are sorted and disjoint, which `_scatter_rows` requires outright.
+ assert all(a[0] + a[1] <= b[0] for a, b in zip(runs, runs[1:]))
+ assert stub.tokenizer.decode(input_ids[0]).count("