From faf1ae89d3b893047f3a946220468d5a1c6f6953 Mon Sep 17 00:00:00 2001 From: TestIntel Date: Thu, 3 Sep 2026 02:27:15 -0700 Subject: [PATCH 01/14] testintel: add missing test for Decoder3d.forward Candidate cand:c45125ae274daf08773e3cdcf1dd30c1 at 2f01706b3fed. --- .../diffusion/test_wan_vae_decoder.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py new file mode 100644 index 000000000..7c3f7bab1 --- /dev/null +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -0,0 +1,92 @@ +############################################################################### +# Copyright (c) 2025, Advanced Micro Devices, Inc. +# +# See LICENSE for license information. +############################################################################### + +import torch + +from primus.backends.diffusion.models.wan.vae2_1 import Decoder3d, count_conv3d + + +def _make_decoder(): + torch.manual_seed(0) + decoder = Decoder3d( + dim=8, + z_dim=4, + dim_mult=[1, 2], + num_res_blocks=1, + attn_scales=[], + temperal_upsample=[True], + dropout=0.0, + ) + decoder.eval() + return decoder + + +def test_forward_without_cache_upsamples_spatially_and_projects_to_rgb(): + decoder = _make_decoder() + x = torch.randn(1, 4, 2, 4, 4) + + with torch.no_grad(): + out = decoder(x) + + # Without a feature cache, temporal upsampling is skipped (it only kicks + # in once cached history is available), but the spatial upsample3d/2d + # stages still double H and W, and the head projects back to 3 channels. + assert out.shape == (1, 3, 2, 8, 8) + assert torch.isfinite(out).all() + + +def test_forward_is_deterministic_in_eval_mode(): + decoder = _make_decoder() + x = torch.randn(1, 4, 2, 4, 4) + + with torch.no_grad(): + out1 = decoder(x) + out2 = decoder(x) + + assert torch.equal(out1, out2) + + +def test_forward_with_feat_cache_grows_temporal_dimension_across_chunks(): + decoder = _make_decoder() + x = torch.randn(1, 4, 2, 4, 4) + + conv_num = count_conv3d(decoder) + feat_map = [None] * conv_num + + outputs = [] + with torch.no_grad(): + for i in range(x.shape[2]): + feat_idx = [0] + frame_out = decoder(x[:, :, i : i + 1, :, :], feat_cache=feat_map, feat_idx=feat_idx) + # Every CausalConv3d call consumes one cache slot. + assert feat_idx[0] == conv_num + outputs.append(frame_out) + + cached_out = torch.cat(outputs, dim=2) + + # The first chunk is a "Rep" placeholder (no temporal doubling yet); every + # subsequent chunk doubles its own temporal contribution once the + # upsample3d time_conv has cached history to work with. + assert cached_out.shape == (1, 3, 3, 8, 8) + assert torch.isfinite(cached_out).all() + # All cache slots should be populated after a full pass. + assert all(slot is not None for slot in feat_map) + + +def test_forward_defaults_feat_idx_when_not_provided(): + decoder = _make_decoder() + x = torch.randn(1, 4, 2, 4, 4) + + conv_num = count_conv3d(decoder) + feat_map = [None] * conv_num + + with torch.no_grad(): + # feat_idx=None should be treated the same as passing [0]. + out = decoder(x[:, :, :1, :, :], feat_cache=feat_map, feat_idx=None) + + assert out.shape[0] == 1 + assert out.shape[1] == 3 + assert torch.isfinite(out).all() From 171123e39b776e95ce800f72e361972ab6582e20 Mon Sep 17 00:00:00 2001 From: Doug J Date: Thu, 3 Sep 2026 02:36:57 -0700 Subject: [PATCH 02/14] test: rewrite Decoder3d streaming test against vae2_2 production protocol Per review: import from vae2_2 (matches the actual Wan 2.2 production decode path), drop the no-cache full-sequence comparisons (not a valid oracle once temporal upsampling is involved), stop asserting 3 output channels (direct Decoder3d output is 12 channels; RGB projection happens later via unpatchify), and replay WanVAE_.decode's real per-frame protocol: one latent frame per call, one feat_cache list reused across calls, feat_idx reset to [0] per frame, first_chunk=True only on the first call, feat_idx[0] == count_conv3d(decoder) after every call, and the temporal-upsample cache slot's "Rep" sentinel transitioning to a real tensor after the second chunk. Also verifies first-chunk vs later chunk frame counts and deterministic replay after reinitializing the cache. --- .../diffusion/test_wan_vae_decoder.py | 93 +------------------ 1 file changed, 1 insertion(+), 92 deletions(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index 7c3f7bab1..e21a1e382 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -1,92 +1 @@ -############################################################################### -# Copyright (c) 2025, Advanced Micro Devices, Inc. -# -# See LICENSE for license information. -############################################################################### - -import torch - -from primus.backends.diffusion.models.wan.vae2_1 import Decoder3d, count_conv3d - - -def _make_decoder(): - torch.manual_seed(0) - decoder = Decoder3d( - dim=8, - z_dim=4, - dim_mult=[1, 2], - num_res_blocks=1, - attn_scales=[], - temperal_upsample=[True], - dropout=0.0, - ) - decoder.eval() - return decoder - - -def test_forward_without_cache_upsamples_spatially_and_projects_to_rgb(): - decoder = _make_decoder() - x = torch.randn(1, 4, 2, 4, 4) - - with torch.no_grad(): - out = decoder(x) - - # Without a feature cache, temporal upsampling is skipped (it only kicks - # in once cached history is available), but the spatial upsample3d/2d - # stages still double H and W, and the head projects back to 3 channels. - assert out.shape == (1, 3, 2, 8, 8) - assert torch.isfinite(out).all() - - -def test_forward_is_deterministic_in_eval_mode(): - decoder = _make_decoder() - x = torch.randn(1, 4, 2, 4, 4) - - with torch.no_grad(): - out1 = decoder(x) - out2 = decoder(x) - - assert torch.equal(out1, out2) - - -def test_forward_with_feat_cache_grows_temporal_dimension_across_chunks(): - decoder = _make_decoder() - x = torch.randn(1, 4, 2, 4, 4) - - conv_num = count_conv3d(decoder) - feat_map = [None] * conv_num - - outputs = [] - with torch.no_grad(): - for i in range(x.shape[2]): - feat_idx = [0] - frame_out = decoder(x[:, :, i : i + 1, :, :], feat_cache=feat_map, feat_idx=feat_idx) - # Every CausalConv3d call consumes one cache slot. - assert feat_idx[0] == conv_num - outputs.append(frame_out) - - cached_out = torch.cat(outputs, dim=2) - - # The first chunk is a "Rep" placeholder (no temporal doubling yet); every - # subsequent chunk doubles its own temporal contribution once the - # upsample3d time_conv has cached history to work with. - assert cached_out.shape == (1, 3, 3, 8, 8) - assert torch.isfinite(cached_out).all() - # All cache slots should be populated after a full pass. - assert all(slot is not None for slot in feat_map) - - -def test_forward_defaults_feat_idx_when_not_provided(): - decoder = _make_decoder() - x = torch.randn(1, 4, 2, 4, 4) - - conv_num = count_conv3d(decoder) - feat_map = [None] * conv_num - - with torch.no_grad(): - # feat_idx=None should be treated the same as passing [0]. - out = decoder(x[:, :, :1, :, :], feat_cache=feat_map, feat_idx=None) - - assert out.shape[0] == 1 - assert out.shape[1] == 3 - assert torch.isfinite(out).all() +IyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMKIyBDb3B5cmlnaHQgKGMpIDIwMjUsIEFkdmFuY2VkIE1pY3JvIERldmljZXMsIEluYy4KIwojIFNlZSBMSUNFTlNFIGZvciBsaWNlbnNlIGluZm9ybWF0aW9uLgojIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMKCmltcG9ydCB0b3JjaAoKZnJvbSBwcmltdXMuYmFja2VuZHMuZGlmZnVzaW9uLm1vZGVscy53YW4udmFlMl8yIGltcG9ydCBEZWNvZGVyM2QsIGNvdW50X2NvbnYzZAoKCmRlZiBfbWFrZV9kZWNvZGVyKCk6CiAgICAjIEEgdGlueSBkZWNvZGVyIHdpdGggYSBzaW5nbGUgdGVtcG9yYWwtdXBzYW1wbGUgc3RhZ2U6IGVub3VnaCB0byBleGVyY2lzZQogICAgIyBjYWNoZSBpbmRleGluZywgdGhlICJSZXAiIHNlbnRpbmVsLCBhbmQgZmlyc3RfY2h1bmsgcHJvcGFnYXRpb24gd2l0aG91dAogICAgIyBhIGNoYW5uZWwtY2hhbmdpbmcgcmVzaWR1YWwgc2hvcnRjdXQgKHdoaWNoIHZhZTJfMidzIFJlc2lkdWFsQmxvY2sgZG9lcwogICAgIyBub3Qgcm91dGUgdGhyb3VnaCB0aGUgZmVhdF9jYWNoZSwgYW5kIHdvdWxkIGRlc3luYyBmZWF0X2lkeCBmcm9tCiAgICAjIGNvdW50X2NvbnYzZChkZWNvZGVyKSkuCiAgICB0b3JjaC5tYW51YWxfc2VlZCgwKQogICAgZGVjb2RlciA9IERlY29kZXIzZCgKICAgICAgICBkaW09OCwKICAgICAgICB6X2RpbT00LAogICAgICAgIGRpbV9tdWx0PVsxLCAxXSwKICAgICAgICBudW1fcmVzX2Jsb2Nrcz0xLAogICAgICAgIGF0dG5fc2NhbGVzPVtdLAogICAgICAgIHRlbXBlcmFsX3Vwc2FtcGxlPVtUcnVlXSwKICAgICAgICBkcm9wb3V0PTAuMCwKICAgICkKICAgIGRlY29kZXIuZXZhbCgpCiAgICByZXR1cm4gZGVjb2RlcgoKCmRlZiBfZGVjb2RlX3N0cmVhbWluZyhkZWNvZGVyLCB4LCBjb252X251bSk6CiAgICAiIiJSZXBsYXkgV2FuVkFFXy5kZWNvZGUncyBwZXItZnJhbWUgY2h1bmtlZC1kZWNvZGUgcHJvdG9jb2w6IG9uZSBsYXRlbnQKICAgIGZyYW1lIHBlciBjYWxsLCBhIHNpbmdsZSBmZWF0X2NhY2hlIGxpc3QgcmV1c2VkIGFjcm9zcyBjYWxscywgZmVhdF9pZHgKICAgIHJlc2V0IHRvIFswXSBmb3IgZXZlcnkgZnJhbWUsIGFuZCBmaXJzdF9jaHVuaz1UcnVlIG9ubHkgb24gdGhlIGZpcnN0CiAgICBjYWxsLiIiIgogICAgZmVhdF9tYXAgPSBbTm9uZV0gKiBjb252X251bQogICAgb3V0cHV0cyA9IFtdCiAgICBmb3IgaSBpbiByYW5nZSh4LnNoYXBlWzJdKToKICAgICAgICBmZWF0X2lkeCA9IFswXQogICAgICAgIGZyYW1lX291dCA9IGRlY29kZXIoCiAgICAgICAgICAgIHhbOiwgOiwgaSA6IGkgKyAxLCA6LCA6XSwKICAgICAgICAgICAgZmVhdF9jYWNoZT1mZWF0X21hcCwKICAgICAgICAgICAgZmVhdF9pZHg9ZmVhdF9pZHgsCiAgICAgICAgICAgIGZpcnN0X2NodW5rPShpID09IDApLAogICAgICAgICkKICAgICAgICAjIEV2ZXJ5IENhdXNhbENvbnYzZCBvbiB0aGUgY2FjaGVkIHBhdGggY29uc3VtZXMgZXhhY3RseSBvbmUgc2xvdC4KICAgICAgICBhc3NlcnQgZmVhdF9pZHhbMF0gPT0gY29udl9udW0KICAgICAgICBvdXRwdXRzLmFwcGVuZChmcmFtZV9vdXQpCiAgICByZXR1cm4gb3V0cHV0cywgZmVhdF9tYXAKCgpkZWYgdGVzdF9mb3J3YXJkX3N0cmVhbWluZ19kZWNvZGVfbWF0Y2hlc193YW4yMl9jaHVua2VkX3Byb3RvY29sKCk6CiAgICAjIERpcmVjdCBEZWNvZGVyM2Qgb3V0cHV0IGlzIDEyIGNoYW5uZWxzIChwYXRjaGlmaWVkIGxhdGVudCBzcGFjZSk7IHRoZQogICAgIyBjb252ZXJzaW9uIHRvIDMgUkdCIGNoYW5uZWxzIGhhcHBlbnMgbGF0ZXIsIGluIHVucGF0Y2hpZnkuCiAgICBkZWNvZGVyID0gX21ha2VfZGVjb2RlcigpCiAgICBjb252X251bSA9IGNvdW50X2NvbnYzZChkZWNvZGVyKQogICAgeCA9IHRvcmNoLnJhbmRuKDEsIDQsIDMsIDQsIDQpCiAgICBmZWF0X21hcCA9IFtOb25lXSAqIGNvbnZfbnVtCgogICAgb3V0cHV0cyA9IFtdCiAgICB3aXRoIHRvcmNoLm5vX2dyYWQoKToKICAgICAgICBmb3IgaSBpbiByYW5nZSh4LnNoYXBlWzJdKToKICAgICAgICAgICAgZmVhdF9pZHggPSBbMF0KICAgICAgICAgICAgZnJhbWVfb3V0ID0gZGVjb2RlcigKICAgICAgICAgICAgICAgIHhbOiwgOiwgaSA6IGkgKyAxLCA6LCA6XSwKICAgICAgICAgICAgICAgIGZlYXRfY2FjaGU9ZmVhdF9tYXAsCiAgICAgICAgICAgICAgICBmZWF0X2lkeD1mZWF0X2lkeCwKICAgICAgICAgICAgICAgIGZpcnN0X2NodW5rPShpID09IDApLAogICAgICAgICAgICApCiAgICAgICAgICAgICMgRXZlcnkgQ2F1c2FsQ29udjNkIG9uIHRoZSBjYWNoZWQgcGF0aCBjb25zdW1lcyBleGFjdGx5IG9uZSBzbG90LgogICAgICAgICAgICBhc3NlcnQgZmVhdF9pZHhbMF0gPT0gY29udl9udW0KCiAgICAgICAgICAgIGlmIGkgPT0gMDoKICAgICAgICAgICAgICAgICMgQWZ0ZXIgdGhlIGZpcnN0IGNodW5rIGV2ZXJ5IGNhY2hlIHNsb3QgaXMgcG9wdWxhdGVkOyB0aGUKICAgICAgICAgICAgICAgICMgdGVtcG9yYWwtdXBzYW1wbGUgc3RhZ2UncyBzbG90IGhvbGRzIHRoZSAiUmVwIiBzZW50aW5lbAogICAgICAgICAgICAgICAgIyB1bnRpbCBhIHNlY29uZCBjaHVuayBnaXZlcyBpdCByZWFsIGhpc3RvcnkgdG8gd29yayB3aXRoLgogICAgICAgICAgICAgICAgYXNzZXJ0IGFsbChzbG90IGlzIG5vdCBOb25lIGZvciBzbG90IGluIGZlYXRfbWFwKQogICAgICAgICAgICAgICAgYXNzZXJ0IGFueShzbG90ID09ICJSZXAiIGZvciBzbG90IGluIGZlYXRfbWFwKQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgIyBPbmNlIHJlYWwgaGlzdG9yeSBpcyBhdmFpbGFibGUsICJSZXAiIG11c3QgaGF2ZSBiZWVuCiAgICAgICAgICAgICAgICAjIHJlcGxhY2VkIGJ5IGFuIGFjdHVhbCBjYWNoZWQgdGVuc29yLgogICAgICAgICAgICAgICAgYXNzZXJ0IG5vdCBhbnkoc2xvdCA9PSAiUmVwIiBmb3Igc2xvdCBpbiBmZWF0X21hcCkKCiAgICAgICAgICAgIG91dHB1dHMuYXBwZW5kKGZyYW1lX291dCkKCiAgICAjIEZpcnN0IGNodW5rIGhhcyBubyBjYWNoZWQgaGlzdG9yeSB5ZXQsIHNvIGl0cyB1cHNhbXBsZTNkIHN0YWdlIGNhbiBvbmx5CiAgICAjIGVtaXQgaXRzIG93biBmcmFtZTsgbGF0ZXIgY2h1bmtzIGhhdmUgaGlzdG9yeSBhbmQgZG91YmxlIHRoZWlyCiAgICAjIHRlbXBvcmFsIGNvbnRyaWJ1dGlvbi4KICAgIGFzc2VydCBvdXRwdXRzWzBdLnNoYXBlID09ICgxLCAxMiwgMSwgOCwgOCkKICAgIGFzc2VydCBvdXRwdXRzWzFdLnNoYXBlID09ICgxLCAxMiwgMiwgOCwgOCkKICAgIGFzc2VydCBvdXRwdXRzWzJdLnNoYXBlID09ICgxLCAxMiwgMiwgOCwgOCkKCiAgICBvdXQgPSB0b3JjaC5jYXQob3V0cHV0cywgZGltPTIpCiAgICBhc3NlcnQgb3V0LnNoYXBlID09ICgxLCAxMiwgNSwgOCwgOCkKICAgIGFzc2VydCB0b3JjaC5pc2Zpbml0ZShvdXQpLmFsbCgpCgoKZGVmIHRlc3RfZm9yd2FyZF9zdHJlYW1pbmdfZGVjb2RlX2lzX2RldGVybWluaXN0aWNfYWZ0ZXJfY2FjaGVfcmVzZXQoKToKICAgIGRlY29kZXIgPSBfbWFrZV9kZWNvZGVyKCkKICAgIGNvbnZfbnVtID0gY291bnRfY29udjNkKGRlY29kZXIpCiAgICB4ID0gdG9yY2gucmFuZG4oMSwgNCwgMywgNCwgNCkKCiAgICB3aXRoIHRvcmNoLm5vX2dyYWQoKToKICAgICAgICBvdXRwdXRzXzEsIF8gPSBfZGVjb2RlX3N0cmVhbWluZyhkZWNvZGVyLCB4LCBjb252X251bSkKICAgICAgICBvdXRwdXRzXzIsIF8gPSBfZGVjb2RlX3N0cmVhbWluZyhkZWNvZGVyLCB4LCBjb252X251bSkKCiAgICBhc3NlcnQgdG9yY2guZXF1YWwodG9yY2guY2F0KG91dHB1dHNfMSwgZGltPTIpLCB0b3JjaC5jYXQob3V0cHV0c18yLCBkaW09MikpCg== \ No newline at end of file From a8b203050f8a7f7274ba2638df913ad72e0eec5e Mon Sep 17 00:00:00 2001 From: Doug J Date: Thu, 3 Sep 2026 02:43:20 -0700 Subject: [PATCH 03/14] test(wan_vae): fix test file committed as base64-encoded blob The file was committed as a single Base64-encoded line, which is not valid Python and raises a SyntaxError on collection (flagged by Copilot review). Restore the decoded, unencoded test source; the content itself is unchanged. --- .../diffusion/test_wan_vae_decoder.py | 109 +++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index e21a1e382..50214c6d2 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -1 +1,108 @@ -IyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMKIyBDb3B5cmlnaHQgKGMpIDIwMjUsIEFkdmFuY2VkIE1pY3JvIERldmljZXMsIEluYy4KIwojIFNlZSBMSUNFTlNFIGZvciBsaWNlbnNlIGluZm9ybWF0aW9uLgojIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMKCmltcG9ydCB0b3JjaAoKZnJvbSBwcmltdXMuYmFja2VuZHMuZGlmZnVzaW9uLm1vZGVscy53YW4udmFlMl8yIGltcG9ydCBEZWNvZGVyM2QsIGNvdW50X2NvbnYzZAoKCmRlZiBfbWFrZV9kZWNvZGVyKCk6CiAgICAjIEEgdGlueSBkZWNvZGVyIHdpdGggYSBzaW5nbGUgdGVtcG9yYWwtdXBzYW1wbGUgc3RhZ2U6IGVub3VnaCB0byBleGVyY2lzZQogICAgIyBjYWNoZSBpbmRleGluZywgdGhlICJSZXAiIHNlbnRpbmVsLCBhbmQgZmlyc3RfY2h1bmsgcHJvcGFnYXRpb24gd2l0aG91dAogICAgIyBhIGNoYW5uZWwtY2hhbmdpbmcgcmVzaWR1YWwgc2hvcnRjdXQgKHdoaWNoIHZhZTJfMidzIFJlc2lkdWFsQmxvY2sgZG9lcwogICAgIyBub3Qgcm91dGUgdGhyb3VnaCB0aGUgZmVhdF9jYWNoZSwgYW5kIHdvdWxkIGRlc3luYyBmZWF0X2lkeCBmcm9tCiAgICAjIGNvdW50X2NvbnYzZChkZWNvZGVyKSkuCiAgICB0b3JjaC5tYW51YWxfc2VlZCgwKQogICAgZGVjb2RlciA9IERlY29kZXIzZCgKICAgICAgICBkaW09OCwKICAgICAgICB6X2RpbT00LAogICAgICAgIGRpbV9tdWx0PVsxLCAxXSwKICAgICAgICBudW1fcmVzX2Jsb2Nrcz0xLAogICAgICAgIGF0dG5fc2NhbGVzPVtdLAogICAgICAgIHRlbXBlcmFsX3Vwc2FtcGxlPVtUcnVlXSwKICAgICAgICBkcm9wb3V0PTAuMCwKICAgICkKICAgIGRlY29kZXIuZXZhbCgpCiAgICByZXR1cm4gZGVjb2RlcgoKCmRlZiBfZGVjb2RlX3N0cmVhbWluZyhkZWNvZGVyLCB4LCBjb252X251bSk6CiAgICAiIiJSZXBsYXkgV2FuVkFFXy5kZWNvZGUncyBwZXItZnJhbWUgY2h1bmtlZC1kZWNvZGUgcHJvdG9jb2w6IG9uZSBsYXRlbnQKICAgIGZyYW1lIHBlciBjYWxsLCBhIHNpbmdsZSBmZWF0X2NhY2hlIGxpc3QgcmV1c2VkIGFjcm9zcyBjYWxscywgZmVhdF9pZHgKICAgIHJlc2V0IHRvIFswXSBmb3IgZXZlcnkgZnJhbWUsIGFuZCBmaXJzdF9jaHVuaz1UcnVlIG9ubHkgb24gdGhlIGZpcnN0CiAgICBjYWxsLiIiIgogICAgZmVhdF9tYXAgPSBbTm9uZV0gKiBjb252X251bQogICAgb3V0cHV0cyA9IFtdCiAgICBmb3IgaSBpbiByYW5nZSh4LnNoYXBlWzJdKToKICAgICAgICBmZWF0X2lkeCA9IFswXQogICAgICAgIGZyYW1lX291dCA9IGRlY29kZXIoCiAgICAgICAgICAgIHhbOiwgOiwgaSA6IGkgKyAxLCA6LCA6XSwKICAgICAgICAgICAgZmVhdF9jYWNoZT1mZWF0X21hcCwKICAgICAgICAgICAgZmVhdF9pZHg9ZmVhdF9pZHgsCiAgICAgICAgICAgIGZpcnN0X2NodW5rPShpID09IDApLAogICAgICAgICkKICAgICAgICAjIEV2ZXJ5IENhdXNhbENvbnYzZCBvbiB0aGUgY2FjaGVkIHBhdGggY29uc3VtZXMgZXhhY3RseSBvbmUgc2xvdC4KICAgICAgICBhc3NlcnQgZmVhdF9pZHhbMF0gPT0gY29udl9udW0KICAgICAgICBvdXRwdXRzLmFwcGVuZChmcmFtZV9vdXQpCiAgICByZXR1cm4gb3V0cHV0cywgZmVhdF9tYXAKCgpkZWYgdGVzdF9mb3J3YXJkX3N0cmVhbWluZ19kZWNvZGVfbWF0Y2hlc193YW4yMl9jaHVua2VkX3Byb3RvY29sKCk6CiAgICAjIERpcmVjdCBEZWNvZGVyM2Qgb3V0cHV0IGlzIDEyIGNoYW5uZWxzIChwYXRjaGlmaWVkIGxhdGVudCBzcGFjZSk7IHRoZQogICAgIyBjb252ZXJzaW9uIHRvIDMgUkdCIGNoYW5uZWxzIGhhcHBlbnMgbGF0ZXIsIGluIHVucGF0Y2hpZnkuCiAgICBkZWNvZGVyID0gX21ha2VfZGVjb2RlcigpCiAgICBjb252X251bSA9IGNvdW50X2NvbnYzZChkZWNvZGVyKQogICAgeCA9IHRvcmNoLnJhbmRuKDEsIDQsIDMsIDQsIDQpCiAgICBmZWF0X21hcCA9IFtOb25lXSAqIGNvbnZfbnVtCgogICAgb3V0cHV0cyA9IFtdCiAgICB3aXRoIHRvcmNoLm5vX2dyYWQoKToKICAgICAgICBmb3IgaSBpbiByYW5nZSh4LnNoYXBlWzJdKToKICAgICAgICAgICAgZmVhdF9pZHggPSBbMF0KICAgICAgICAgICAgZnJhbWVfb3V0ID0gZGVjb2RlcigKICAgICAgICAgICAgICAgIHhbOiwgOiwgaSA6IGkgKyAxLCA6LCA6XSwKICAgICAgICAgICAgICAgIGZlYXRfY2FjaGU9ZmVhdF9tYXAsCiAgICAgICAgICAgICAgICBmZWF0X2lkeD1mZWF0X2lkeCwKICAgICAgICAgICAgICAgIGZpcnN0X2NodW5rPShpID09IDApLAogICAgICAgICAgICApCiAgICAgICAgICAgICMgRXZlcnkgQ2F1c2FsQ29udjNkIG9uIHRoZSBjYWNoZWQgcGF0aCBjb25zdW1lcyBleGFjdGx5IG9uZSBzbG90LgogICAgICAgICAgICBhc3NlcnQgZmVhdF9pZHhbMF0gPT0gY29udl9udW0KCiAgICAgICAgICAgIGlmIGkgPT0gMDoKICAgICAgICAgICAgICAgICMgQWZ0ZXIgdGhlIGZpcnN0IGNodW5rIGV2ZXJ5IGNhY2hlIHNsb3QgaXMgcG9wdWxhdGVkOyB0aGUKICAgICAgICAgICAgICAgICMgdGVtcG9yYWwtdXBzYW1wbGUgc3RhZ2UncyBzbG90IGhvbGRzIHRoZSAiUmVwIiBzZW50aW5lbAogICAgICAgICAgICAgICAgIyB1bnRpbCBhIHNlY29uZCBjaHVuayBnaXZlcyBpdCByZWFsIGhpc3RvcnkgdG8gd29yayB3aXRoLgogICAgICAgICAgICAgICAgYXNzZXJ0IGFsbChzbG90IGlzIG5vdCBOb25lIGZvciBzbG90IGluIGZlYXRfbWFwKQogICAgICAgICAgICAgICAgYXNzZXJ0IGFueShzbG90ID09ICJSZXAiIGZvciBzbG90IGluIGZlYXRfbWFwKQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgIyBPbmNlIHJlYWwgaGlzdG9yeSBpcyBhdmFpbGFibGUsICJSZXAiIG11c3QgaGF2ZSBiZWVuCiAgICAgICAgICAgICAgICAjIHJlcGxhY2VkIGJ5IGFuIGFjdHVhbCBjYWNoZWQgdGVuc29yLgogICAgICAgICAgICAgICAgYXNzZXJ0IG5vdCBhbnkoc2xvdCA9PSAiUmVwIiBmb3Igc2xvdCBpbiBmZWF0X21hcCkKCiAgICAgICAgICAgIG91dHB1dHMuYXBwZW5kKGZyYW1lX291dCkKCiAgICAjIEZpcnN0IGNodW5rIGhhcyBubyBjYWNoZWQgaGlzdG9yeSB5ZXQsIHNvIGl0cyB1cHNhbXBsZTNkIHN0YWdlIGNhbiBvbmx5CiAgICAjIGVtaXQgaXRzIG93biBmcmFtZTsgbGF0ZXIgY2h1bmtzIGhhdmUgaGlzdG9yeSBhbmQgZG91YmxlIHRoZWlyCiAgICAjIHRlbXBvcmFsIGNvbnRyaWJ1dGlvbi4KICAgIGFzc2VydCBvdXRwdXRzWzBdLnNoYXBlID09ICgxLCAxMiwgMSwgOCwgOCkKICAgIGFzc2VydCBvdXRwdXRzWzFdLnNoYXBlID09ICgxLCAxMiwgMiwgOCwgOCkKICAgIGFzc2VydCBvdXRwdXRzWzJdLnNoYXBlID09ICgxLCAxMiwgMiwgOCwgOCkKCiAgICBvdXQgPSB0b3JjaC5jYXQob3V0cHV0cywgZGltPTIpCiAgICBhc3NlcnQgb3V0LnNoYXBlID09ICgxLCAxMiwgNSwgOCwgOCkKICAgIGFzc2VydCB0b3JjaC5pc2Zpbml0ZShvdXQpLmFsbCgpCgoKZGVmIHRlc3RfZm9yd2FyZF9zdHJlYW1pbmdfZGVjb2RlX2lzX2RldGVybWluaXN0aWNfYWZ0ZXJfY2FjaGVfcmVzZXQoKToKICAgIGRlY29kZXIgPSBfbWFrZV9kZWNvZGVyKCkKICAgIGNvbnZfbnVtID0gY291bnRfY29udjNkKGRlY29kZXIpCiAgICB4ID0gdG9yY2gucmFuZG4oMSwgNCwgMywgNCwgNCkKCiAgICB3aXRoIHRvcmNoLm5vX2dyYWQoKToKICAgICAgICBvdXRwdXRzXzEsIF8gPSBfZGVjb2RlX3N0cmVhbWluZyhkZWNvZGVyLCB4LCBjb252X251bSkKICAgICAgICBvdXRwdXRzXzIsIF8gPSBfZGVjb2RlX3N0cmVhbWluZyhkZWNvZGVyLCB4LCBjb252X251bSkKCiAgICBhc3NlcnQgdG9yY2guZXF1YWwodG9yY2guY2F0KG91dHB1dHNfMSwgZGltPTIpLCB0b3JjaC5jYXQob3V0cHV0c18yLCBkaW09MikpCg== \ No newline at end of file +########################################################################## +# Copyright (c) 2025, Advanced Micro Devices, Inc. +# +# See LICENSE for license information. +########################################################################### + +import torch + +from primus.backends.diffusion.models.wan.vae2_2 import Decoder3d, count_conv3d + + +def _make_decoder(): + # A tiny decoder with a single temporal-upsample stage: enough to exercise + # cache indexing, the "Rep" sentinel, and first_chunk propagation without + # a channel-changing residual shortcut (which vae2_2's ResidualBlock does + # not route through the feat_cache, and would desync feat_idx from + # count_conv3d(decoder)). + torch.manual_seed(0) + decoder = Decoder3d( + dim=8, + z_dim=4, + dim_mult=[1, 1], + num_res_blocks=1, + attn_scales=[], + temperal_upsample=[True], + dropout=0.0, + ) + decoder.eval() + return decoder + + +def _decode_streaming(decoder, x, conv_num): + """Replay WanVAE_.decode's per-frame chunked-decode protocol: one latent + frame per call, a single feat_cache list reused across calls, feat_idx + reset to [0] for every frame, and first_chunk=True only on the first + call.""" + feat_map = [None] * conv_num + outputs = [] + for i in range(x.shape[2]): + feat_idx = [0] + frame_out = decoder( + x[:, :, i : i + 1, :, :], + feat_cache=feat_map, + feat_idx=feat_idx, + first_chunk=(i == 0), + ) + # Every CausalConv3d on the cached path consumes exactly one slot. + assert feat_idx[0] == conv_num + outputs.append(frame_out) + return outputs, feat_map + + +def test_forward_streaming_decode_matches_wan22_chunked_protocol(): + # Direct Decoder3d output is 12 channels (patchified latent space); the + # conversion to 3 RGB channels happens later, in unpatchify. + decoder = _make_decoder() + conv_num = count_conv3d(decoder) + x = torch.randn(1, 4, 3, 4, 4) + feat_map = [None] * conv_num + + outputs = [] + with torch.no_grad(): + for i in range(x.shape[2]): + feat_idx = [0] + frame_out = decoder( + x[:, :, i : i + 1, :, :], + feat_cache=feat_map, + feat_idx=feat_idx, + first_chunk=(i == 0), + ) + # Every CausalConv3d on the cached path consumes exactly one slot. + assert feat_idx[0] == conv_num + + if i == 0: + # After the first chunk every cache slot is populated; the + # temporal-upsample stage's slot holds the "Rep" sentinel + # until a second chunk gives it real history to work with. + assert all(slot is not None for slot in feat_map) + assert any(slot == "Rep" for slot in feat_map) + else: + # Once real history is available, "Rep" must have been + # replaced by an actual cached tensor. + assert not any(slot == "Rep" for slot in feat_map) + + outputs.append(frame_out) + + # First chunk has no cached history yet, so its upsample3d stage can only + # emit its own frame; later chunks have history and double their + # temporal contribution. + assert outputs[0].shape == (1, 12, 1, 8, 8) + assert outputs[1].shape == (1, 12, 2, 8, 8) + assert outputs[2].shape == (1, 12, 2, 8, 8) + + out = torch.cat(outputs, dim=2) + assert out.shape == (1, 12, 5, 8, 8) + assert torch.isfinite(out).all() + + +def test_forward_streaming_decode_is_deterministic_after_cache_reset(): + decoder = _make_decoder() + conv_num = count_conv3d(decoder) + x = torch.randn(1, 4, 3, 4, 4) + + with torch.no_grad(): + outputs_1, _ = _decode_streaming(decoder, x, conv_num) + outputs_2, _ = _decode_streaming(decoder, x, conv_num) + + assert torch.equal(torch.cat(outputs_1, dim=2), torch.cat(outputs_2, dim=2)) From c58bd517cbaddcfe05df317a2220ffcc539b9269 Mon Sep 17 00:00:00 2001 From: Doug J Date: Thu, 3 Sep 2026 02:54:28 -0700 Subject: [PATCH 04/14] test: guard "Rep" sentinel check against tensor slots Copilot review (discussion_r3923254187): once a cache slot holds a tensor, `slot == "Rep"` raises TypeError (PyTorch tensor vs str comparison) instead of returning False. Gate the comparison on isinstance(slot, str) so it only fires for the sentinel value itself. --- .../unit_tests/backends/diffusion/test_wan_vae_decoder.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index 50214c6d2..b8391ce02 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -76,11 +76,15 @@ def test_forward_streaming_decode_matches_wan22_chunked_protocol(): # temporal-upsample stage's slot holds the "Rep" sentinel # until a second chunk gives it real history to work with. assert all(slot is not None for slot in feat_map) - assert any(slot == "Rep" for slot in feat_map) + assert any( + isinstance(slot, str) and slot == "Rep" for slot in feat_map + ) else: # Once real history is available, "Rep" must have been # replaced by an actual cached tensor. - assert not any(slot == "Rep" for slot in feat_map) + assert not any( + isinstance(slot, str) and slot == "Rep" for slot in feat_map + ) outputs.append(frame_out) From a7c14cf71493c2298a2b15f0afd0cb4d04cac7dd Mon Sep 17 00:00:00 2001 From: Doug J Date: Thu, 3 Sep 2026 03:02:02 -0700 Subject: [PATCH 05/14] style: apply black formatting to satisfy code-lint code-lint (3.12) failed because black wants the two "Rep" sentinel assertions collapsed to single lines (they fit within the line-length limit once written as one line). Applying the exact reformatting from the failed job's diff; no test logic changed. --- .../unit_tests/backends/diffusion/test_wan_vae_decoder.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index b8391ce02..9309410d0 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -76,15 +76,11 @@ def test_forward_streaming_decode_matches_wan22_chunked_protocol(): # temporal-upsample stage's slot holds the "Rep" sentinel # until a second chunk gives it real history to work with. assert all(slot is not None for slot in feat_map) - assert any( - isinstance(slot, str) and slot == "Rep" for slot in feat_map - ) + assert any(isinstance(slot, str) and slot == "Rep" for slot in feat_map) else: # Once real history is available, "Rep" must have been # replaced by an actual cached tensor. - assert not any( - isinstance(slot, str) and slot == "Rep" for slot in feat_map - ) + assert not any(isinstance(slot, str) and slot == "Rep" for slot in feat_map) outputs.append(frame_out) From fb3db9409c918354ef55749ab83ff6a575b4d53c Mon Sep 17 00:00:00 2001 From: Doug J Date: Thu, 3 Sep 2026 03:06:34 -0700 Subject: [PATCH 06/14] test: use torch.testing.assert_close for determinism check Copilot flagged torch.equal as an overly strict/potentially flaky comparison for floating-point tensors across backends. Switch the replay-determinism assertion to torch.testing.assert_close with a tight tolerance, which still catches real cache/index regressions without risking backend-dependent bit-exactness flakiness. --- tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index 9309410d0..10cc8586d 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -105,4 +105,8 @@ def test_forward_streaming_decode_is_deterministic_after_cache_reset(): outputs_1, _ = _decode_streaming(decoder, x, conv_num) outputs_2, _ = _decode_streaming(decoder, x, conv_num) - assert torch.equal(torch.cat(outputs_1, dim=2), torch.cat(outputs_2, dim=2)) + torch.testing.assert_close( + torch.cat(outputs_1, dim=2), + torch.cat(outputs_2, dim=2), + rtol=0, + atol=1e-6, From 40f74e0041b3e868bf36c2fdd16c3f903aeb92e5 Mon Sep 17 00:00:00 2001 From: Doug J Date: Thu, 3 Sep 2026 03:13:27 -0700 Subject: [PATCH 07/14] fix: restore truncated torch.testing.assert_close call The previous commit (fb3db94) that switched the replay-determinism assertion from torch.equal to torch.testing.assert_close was pushed with the file truncated mid-statement (missing the closing paren and the rest of the file), which black's parser correctly rejected with "Cannot parse: 113:0: EOF in multi-line statement" and failed code-lint (3.12). Restoring the intended, complete call - verified with `black --line-length 110 --target-version py38 --check` and `py_compile` locally before pushing. --- tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index 10cc8586d..8f965cf69 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -110,3 +110,4 @@ def test_forward_streaming_decode_is_deterministic_after_cache_reset(): torch.cat(outputs_2, dim=2), rtol=0, atol=1e-6, + ) From b3b68f95143bde798bce28656ea943dc979b9d8f Mon Sep 17 00:00:00 2001 From: Doug J Date: Thu, 3 Sep 2026 03:17:27 -0700 Subject: [PATCH 08/14] test: isolate RNG seeding in _make_decoder with fork_rng Copilot flagged that torch.manual_seed(0) in _make_decoder() mutates global RNG state, which can leak into other tests run afterward in the same process. Wrap the seeding in torch.random.fork_rng() so the global generator is restored once the decoder's weights are initialized. --- .../diffusion/test_wan_vae_decoder.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index 8f965cf69..ed8d44bb9 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -15,21 +15,21 @@ def _make_decoder(): # a channel-changing residual shortcut (which vae2_2's ResidualBlock does # not route through the feat_cache, and would desync feat_idx from # count_conv3d(decoder)). - torch.manual_seed(0) - decoder = Decoder3d( - dim=8, - z_dim=4, - dim_mult=[1, 1], - num_res_blocks=1, - attn_scales=[], - temperal_upsample=[True], - dropout=0.0, - ) + # Seed only within a forked RNG scope so this doesn't mutate global RNG + # state and leak into other tests run later in the same process. + with torch.random.fork_rng(): + torch.manual_seed(0) + decoder = Decoder3d( + dim=8, + z_dim=4, + dim_mult=[1, 1], + num_res_blocks=1, + attn_scales=[], + temperal_upsample=[True], + dropout=0.0, + ) decoder.eval() return decoder - - -def _decode_streaming(decoder, x, conv_num): """Replay WanVAE_.decode's per-frame chunked-decode protocol: one latent frame per call, a single feat_cache list reused across calls, feat_idx reset to [0] for every frame, and first_chunk=True only on the first From 6d74fb546c21881bc1b6b4ef028719e70348b5e9 Mon Sep 17 00:00:00 2001 From: Doug J Date: Thu, 3 Sep 2026 03:18:16 -0700 Subject: [PATCH 09/14] fix: restore _decode_streaming function header dropped by previous edit The previous commit's line-numbered patch miscounted and clobbered the blank lines plus the `def _decode_streaming(...)` header between _make_decoder and its docstring. Restoring them; no logic change. --- tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index ed8d44bb9..136b3ca0d 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -30,14 +30,14 @@ def _make_decoder(): ) decoder.eval() return decoder + + +def _decode_streaming(decoder, x, conv_num): """Replay WanVAE_.decode's per-frame chunked-decode protocol: one latent frame per call, a single feat_cache list reused across calls, feat_idx reset to [0] for every frame, and first_chunk=True only on the first call.""" feat_map = [None] * conv_num - outputs = [] - for i in range(x.shape[2]): - feat_idx = [0] frame_out = decoder( x[:, :, i : i + 1, :, :], feat_cache=feat_map, From 207b44d9b6cf2e0c1aae14b511cf72cadf5edc4d Mon Sep 17 00:00:00 2001 From: Doug J Date: Thu, 3 Sep 2026 03:18:53 -0700 Subject: [PATCH 10/14] fix: repair file corrupted by prior line-numbered patch attempts The last two automated patches miscounted line ranges and clobbered the _decode_streaming body (dropped outputs=[]/for-loop/feat_idx=[0]). Restoring the full, correct file: the only intended change from the last known-good version (40f74e0) is wrapping _make_decoder's seeding in torch.random.fork_rng() per Copilot's global-RNG-mutation finding. No other logic changed. --- tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index 136b3ca0d..1c4d9c40c 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -38,6 +38,9 @@ def _decode_streaming(decoder, x, conv_num): reset to [0] for every frame, and first_chunk=True only on the first call.""" feat_map = [None] * conv_num + outputs = [] + for i in range(x.shape[2]): + feat_idx = [0] frame_out = decoder( x[:, :, i : i + 1, :, :], feat_cache=feat_map, From 8204f01a8601c2e46301311e3e18394beb985969 Mon Sep 17 00:00:00 2001 From: Doug J Date: Fri, 4 Sep 2026 09:37:13 -0700 Subject: [PATCH 11/14] test: fork CPU-only RNG state in wan vae decoder test Address Copilot review comment: torch.random.fork_rng() defaults to forking all CUDA devices when available, which can trigger GPU runtime initialization in this CPU-only unit test. Pass devices=[] to fork only CPU RNG state. --- tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index 1c4d9c40c..c69d6b22b 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -17,7 +17,7 @@ def _make_decoder(): # count_conv3d(decoder)). # Seed only within a forked RNG scope so this doesn't mutate global RNG # state and leak into other tests run later in the same process. - with torch.random.fork_rng(): + with torch.random.fork_rng(devices=[]): torch.manual_seed(0) decoder = Decoder3d( dim=8, From bb2afaad8699a4682e148aeb38a726566aef73d8 Mon Sep 17 00:00:00 2001 From: Doug J Date: Sun, 6 Sep 2026 21:25:54 -0700 Subject: [PATCH 12/14] test: fix copyright year in new test file Newly added file should use 2026, not 2025, per review comment. --- tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index c69d6b22b..0d2017438 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -1,5 +1,5 @@ ########################################################################## -# Copyright (c) 2025, Advanced Micro Devices, Inc. +# Copyright (c) 2026, Advanced Micro Devices, Inc. # # See LICENSE for license information. ########################################################################### From a7805ce375ae007d467925633956edd04232c1f4 Mon Sep 17 00:00:00 2001 From: Doug J Date: Sun, 6 Sep 2026 21:31:37 -0700 Subject: [PATCH 13/14] test: fork RNG for streaming-decode input generation Copilot review (2026-09-07) flagged that both tests still draw the decoder input `x` with torch.randn() outside the forked RNG scope, which advances global RNG state and undermines the earlier fix that scoped decoder-init seeding to a forked RNG. Wrap the input draws in torch.random.fork_rng(devices=[]) with a fixed seed too, so runs are reproducible and isolated from other tests in the same process. --- .../backends/diffusion/test_wan_vae_decoder.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index 0d2017438..52de6b6aa 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -32,6 +32,15 @@ def _make_decoder(): return decoder +def _make_input(): + # Same rationale as _make_decoder: keep the random draw inside a forked, + # seeded RNG scope so it is reproducible and never leaks into the global + # RNG state that other tests may depend on. + with torch.random.fork_rng(devices=[]): + torch.manual_seed(1) + return torch.randn(1, 4, 3, 4, 4) + + def _decode_streaming(decoder, x, conv_num): """Replay WanVAE_.decode's per-frame chunked-decode protocol: one latent frame per call, a single feat_cache list reused across calls, feat_idx @@ -58,7 +67,7 @@ def test_forward_streaming_decode_matches_wan22_chunked_protocol(): # conversion to 3 RGB channels happens later, in unpatchify. decoder = _make_decoder() conv_num = count_conv3d(decoder) - x = torch.randn(1, 4, 3, 4, 4) + x = _make_input() feat_map = [None] * conv_num outputs = [] @@ -102,7 +111,7 @@ def test_forward_streaming_decode_matches_wan22_chunked_protocol(): def test_forward_streaming_decode_is_deterministic_after_cache_reset(): decoder = _make_decoder() conv_num = count_conv3d(decoder) - x = torch.randn(1, 4, 3, 4, 4) + x = _make_input() with torch.no_grad(): outputs_1, _ = _decode_streaming(decoder, x, conv_num) From 3bf4791aedc1a01824bf1f53a3cab404ff00746e Mon Sep 17 00:00:00 2001 From: Doug J Date: Sun, 6 Sep 2026 21:42:26 -0700 Subject: [PATCH 14/14] test: scope RNG fork to available CUDA devices to avoid state leak Copilot flagged that torch.manual_seed() also seeds CUDA/ROCm RNG state when a GPU runtime is available, so fork_rng(devices=[]) doesn't restore that CUDA RNG afterward and can leak into other tests on GPU-capable runners. Fork torch.cuda.device_count() devices when CUDA is available, and no devices when it isn't (avoiding CUDA runtime init on CPU-only runners, per the earlier Copilot comment this replaces). --- .../backends/diffusion/test_wan_vae_decoder.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py index 52de6b6aa..bae9ccf54 100644 --- a/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py +++ b/tests/unit_tests/backends/diffusion/test_wan_vae_decoder.py @@ -9,6 +9,16 @@ from primus.backends.diffusion.models.wan.vae2_2 import Decoder3d, count_conv3d +def _fork_rng_devices(): + # torch.manual_seed() also seeds CUDA/ROCm RNG state when a GPU runtime + # is available, so on GPU-capable runners we must fork (and thus + # restore) that CUDA RNG state too, or the seed call leaks into other + # tests. On CPU-only runners we deliberately fork no devices, since + # torch.cuda.is_available() is False there and passing any CUDA device + # index would force CUDA runtime initialization in a CPU-only test. + return list(range(torch.cuda.device_count())) if torch.cuda.is_available() else [] + + def _make_decoder(): # A tiny decoder with a single temporal-upsample stage: enough to exercise # cache indexing, the "Rep" sentinel, and first_chunk propagation without @@ -17,7 +27,7 @@ def _make_decoder(): # count_conv3d(decoder)). # Seed only within a forked RNG scope so this doesn't mutate global RNG # state and leak into other tests run later in the same process. - with torch.random.fork_rng(devices=[]): + with torch.random.fork_rng(devices=_fork_rng_devices()): torch.manual_seed(0) decoder = Decoder3d( dim=8, @@ -36,7 +46,7 @@ def _make_input(): # Same rationale as _make_decoder: keep the random draw inside a forked, # seeded RNG scope so it is reproducible and never leaks into the global # RNG state that other tests may depend on. - with torch.random.fork_rng(devices=[]): + with torch.random.fork_rng(devices=_fork_rng_devices()): torch.manual_seed(1) return torch.randn(1, 4, 3, 4, 4)