Feature abot dit#22
Conversation
In Resample::forward (upsample3d), the first latent-frame chunk skipped time_conv entirely and never seeded the temporal feat cache, so the second chunk ran time_conv against zero padding instead of chunk-0 context. The reference implementation (Wan vae2_2.py, "Rep" cache semantics) runs time_conv with causal zero padding on chunk 0, doubles the frames, trims the first one, and stores the input tail in the cache for the next chunk. Effect on Wan2.2 VAE decode vs the PyTorch reference (5-frame 480x832, CPU backend, f16 weights): cosine 0.9959 / 27.2 dB PSNR before, cosine 1.000000 / 79.0 dB after. Output was visually near-identical, so this only shows up in numeric parity testing. Encode and TAEHV paths are unaffected.
…ction adapter) ABot-World-0-5B-LF (acvlab, Apache-2.0) is a causal/interactive derivative of Wan2.2-TI2V-5B with a keyboard-action conditioning adapter. This adds: - VERSION_ABOT_WORLD: detected via the act_control_adapter.conv.weight tensor; helpers sd_version_is_abot_world() / sd_version_is_wan_ti2v_family() (the latter now gates the shared TI2V 48ch/16x latent-space branches in vae.hpp, tae.hpp and stable-diffusion.cpp instead of == VERSION_WAN2_2_TI2V) - WAN::ActControlAdapter block (SimpleAdapter in the reference impl: conv 2x2/s2 over pixel-unshuffled action planes + one ReLU residual block) registered by WAN::Wan when WanParams.abot_world is set, so all 831 checkpoint tensors (F16/Q8_0 GGUF or safetensors) map and load - WanRunner: ABot-World-5B config (Wan2.2-TI2V-5B dims + abot_world flag) - generate_video(): explicit, documented rejection for ABot-World models -- the causal interactive session (KV cache, per-block actions, 4-step distilled schedule) lands in a follow-up; running these weights through the batch path executes the wrong recipe by design No behavior change for any existing model version: every new branch is gated on VERSION_ABOT_WORLD, and sd_version_is_wan_ti2v_family(v) == (v == VERSION_WAN2_2_TI2V) for all previously existing versions. Verified: ABot Q8_0 GGUF (5.86 GB, 831 tensors incl. adapter) detects as "ABot-World", instantiates ABot-World-5B, and completes tensor loading; generation attempt fails fast with the explanatory error.
sd-abot-decode: a native, Python-free example that loads the taehv (taew2_2)
decoder GGUF and a fixed-scene "walk" of precomputed latents (raw f32), decodes
them on the ggml engine, and writes a video. This proves the native taehv
decode path and the S3 -> GPU-runner -> build -> video -> validate CI pipeline
end to end; the DiT causal denoise replaces the "load latents" input in the
follow-up (docs/abot_world_mvp_spec.md).
- examples/abot-decode/{main.cpp,CMakeLists.txt}: standalone decoder (mirrors
the upscaler.cpp standalone-runner pattern); loads TinyVideoAutoEncoder for
VERSION_ABOT_WORLD, decodes video latents, writes MJPG-AVI.
- examples/abot-decode/data/walk_latents.{bin,json}: 12-frame golden latent
walk (from the reference pipeline) as the CI decode input.
- .github/workflows/build.yml: temporary abot-walk-mvp-gpu job on
qvac-ubuntu2204-x64-gpu — Vulkan build, pull models from S3 (tether-ai-dev via
OIDC), run the decode, validate the video, upload it as an artifact. Appended
to the existing workflow so a feature-branch PR can trigger it; to be split
into abot-walk-mvp.yml at merge.
Builds locally (MinGW, CPU). GPU/S3 path validated on CI.
GPU testing of ABot moves to the diffusion addon (monorepo) via a vcpkg overlay port, so the engine PR no longer needs the fork-side GPU workflow or the standalone taehv-decode example: - revert build.yml to the pristine matrix (drop the abot-walk-mvp-gpu job that had no available self-hosted runner in this fork) - delete .github/workflows/abot-walk-mvp.yml scaffold - delete examples/abot-decode/ (native taehv decode + committed latent fixture) This PR is now scoped to engine support only: recognize + load ABot-World (VERSION_ABOT_WORLD, action-adapter block, TI2V loading) with batch generate_video() guarded. Docs (docs/abot_world*.md) and the load-validation script (script/validate_abot_world.sh) are kept.
…test Builds the diffusion addon against the unmerged sd.cpp PR tetherto/qvac-ext-stable-diffusion.cpp#22 (ABot-World support) using a vcpkg overlay port, so ABot can be GPU-tested through the addon's existing integration CI without merging the engine PR or bumping the shared registry. - vcpkg/overlay-ports/stable-diffusion-cpp: verbatim copy of the registry port (2026-07-03, port-version 5) with REF + SHA512 repinned to PR #22's head (da3f6b26). vcpkg-configuration.json gains overlay-ports so it wins over the registry port. Temporary — removed when the engine PR merges + the registry port is bumped. - scripts/download-model-abot.sh: fetch the ABot GGUF set (DiT q8_0/f16, wan2.2 VAE, taew2_2) from corp S3 (tether-ai-dev, OIDC) with SHA256 check. - test/integration/abot-world.test.js: skip-guarded. Asserts the ABot set loads via the addon and that batch generate is rejected with the documented "interactive session not implemented" error (ABot is causal/interactive, not one-shot). Auto-discovered by test:integration, so it runs on the existing qvac-*-gpu integration workflow. Replaced by a real walk assertion when the causal core lands.
Drop the internal implementation-planning spec (belongs with the consuming project, not this public repo) and remove a reference to non-public tooling from abot_world.md. No code changes.
| *num_frames_out = 0; | ||
| } | ||
| int64_t t0 = ggml_time_ms(); | ||
| if (sd_version_is_abot_world(sd_ctx->sd->version)) { |
There was a problem hiding this comment.
This fail-fast guard only covers one of the two doors into generation, and the capability layer disagrees with it.
1. generate_image() is unguarded. sd_version_is_wan() was (correctly) extended to include VERSION_ABOT_WORLD, so ABot travels through all the shared Wan latent/VAE/sampling paths used by both generate_video() and generate_image()/generate_image_internal(). But the rejection was added only here. A caller that loads an ABot GGUF and calls generate_image() runs the bidirectional/one-shot Wan recipe on a causal model with no check at all — silently corrupted output, or a raw GGML_ASSERT abort if it hits a shape assumption that was never true for ABot. That's exactly the hazard this guard exists to prevent.
2. The capability query now lies. sd_version_supports_video_generation() (line 3104) is ... || sd_version_is_wan(version) || ..., so it now reports ABot as video-capable. The example server's /vid_gen route (examples/server/routes_sdcpp.cpp) uses that query to reject unsupported models cleanly before reaching the engine — it will instead wave ABot through to this return false, bypassing the intended clean-rejection path.
Suggested fix: reject in one shared choke point instead of per-entrypoint, and keep the capability query honest:
- exclude ABot from
sd_version_supports_video_generation()(and any image-capability query), e.g.... && !sd_version_is_abot_world(version); - move the actual rejection to a point both entrypoints pass through (
GenerationRequest/SamplePlan::resolve()), or reject atnew_sd_ctx()load time when the version isVERSION_ABOT_WORLDand no causal-session API was requested.
Loading still works; generation is then refused everywhere with the same message, and the server pre-screen agrees with the engine.
There was a problem hiding this comment.
Both points confirmed and fixed in 23d8fd5 — thanks, this was a real gap.
- Shared choke point: the rejection moved into
GenerationRequest::resolve()(same pattern asvalidate_ideogram4_uncond_model), sogenerate_image()andgenerate_video()are both covered by one check; the per-entrypoint guard here was removed. Message now names both entrypoints. - Honest capability queries:
sd_version_supports_video_generation()andsd_version_supports_image_generation()both returnfalsefor ABot (image could not simply stay!video, or it would have flipped totrue). script/validate_abot_world.shgrew animg_genlane asserting the image path rejects too; verified locally against the ABot Q8_0 GGUF (detection + full load unchanged, both batch paths reject, stock-Wan lane unaffected).
On load-time rejection: kept loading permitted intentionally — downstream (tetherto/qvac#3352) exercises load on GPU CI ahead of the causal session API, and the capability queries now give front-ends the clean pre-screen.
…apability queries Review feedback (#22): the fail-fast guard covered only generate_video(), while generate_image() ran the batch recipe on the causal model unchecked, and sd_version_supports_video_generation() still reported ABot as video-capable, so front-ends pre-screening on the capability query would wave it through. - Move the rejection into GenerationRequest::resolve() (the stage both generate_image() and generate_video() pass through, same pattern as validate_ideogram4_uncond_model): one check now covers every batch door. Message updated to name both entrypoints. - sd_version_supports_video_generation() and sd_version_supports_image_generation() both return false for ABot-World (image could not simply mirror !video, or it would flip to true). - Drop the now-redundant guard in generate_video(). - script/validate_abot_world.sh: assert the image path is rejected too; docs/abot_world.md wording updated. Verified locally against the ABot Q8_0 GGUF: detection + full tensor load unchanged; vid_gen and img_gen both reject with the shared message; stock-Wan regression lane unchanged.
Implements the actual interactive walk for ABot-World models (the batch
generate_image()/generate_video() APIs keep rejecting them):
- src/abot_world.hpp: causal walk core using the recompute formulation --
each denoise step re-derives attention over [ref tokens | clean history
| current block] with a per-row trailing-window mask, reproducing the
reference KV cache exactly (including eviction). Absolute-counter RoPE
with negative-time reference ids; per-frame timestep modulation via a
row-indexed e0 table; PixelUnshuffle'd keyboard-action planes through
the act_control_adapter. Includes a minimal scene-pack reader
(safetensors F32), a seed-stable RNG, and AbotWalkSession combining the
4-step distilled flow-matching block loop with taehv pixel decoding
(stateless per-block decode with a 3-latent-frame overlap standing in
for the reference's streaming decoder cache).
- include/stable-diffusion.h, src/stable-diffusion.cpp: public C API --
sd_abot_session_{params_init,new,step,frames_free,free}. A session is
standalone (own DiT + taehv + scene pack, no sd_ctx_t); step() takes an
8-key action mask and returns the next block's decoded RGB frames.
- src/wan.hpp: WanAttentionBlock::forward takes an optional additive
self-attention mask (defaulted, no behavior change for existing paths).
- src/tae.hpp: patchify/unpatchify marked inline (they now live in a
header included from more than one translation unit).
Validation vs the PyTorch reference (F16 GGUF, coastal scene goldens):
- Single-step parity across every regime -- first block, all timesteps,
history recompute, window onset, deep eviction: 7/7 gates at cosine
0.9996-0.9999 (gate 0.999).
- Native taehv decode vs reference streaming decode on identical latents:
PSNR mean 46.4 dB, min 33.5 dB.
Three small executables used to prove the causal walk core against the PyTorch reference and to exercise the public session C API end to end: - examples/abot-parity: runs one golden denoise step (dumped by the reference implementation) through the causal forward and writes the block x0 prediction for offline cosine comparison. - examples/abot-walk: full block-loop walk over a fixed scene; replays the reference's recorded noise for deterministic full-chain validation against the golden per-block final latents, or free-walks with a seeded RNG. Emits raw latents. - examples/abot-session: drives the public sd_abot_session_* C API for a fixed-scene walk, writing decoded frames as PNGs; --mode decode is a taehv-only lane for PSNR cross-checks against the reference decoder on identical latents.
The abot-parity/walk/session examples link against engine internals (ModelLoader, SDBackendManager, the runner classes) that are not exported from the shared library, so -DSD_BUILD_SHARED_LIBS=ON lanes failed to link them. Gate the three harnesses to static builds; the public sd_abot_session_* C API itself is exported like every other sd_* symbol and is unaffected.
What
Adds ABot-World support to the engine, in two increments:
VERSION_ABOT_WORLDdetection (via theact_control_adapter.conv.weighttensor), theActControlAdapterblock (keyboard-action conditioning), Wan2.2-TI2V-5B-family loading, and a guard that rejects ABot models in the batchgenerate_image()/generate_video()APIs at the sharedGenerationRequest::resolve()choke point (ABot is a causal interactive model, not a one-shot generator). Both capability queries reportfalsefor ABot.src/abot_world.hpp— walk core using a recompute formulation: each denoise step re-derives attention over[reference tokens | clean history | current block]with a per-row trailing-window mask that reproduces the reference implementation's KV cache exactly (including eviction). Covers the action adapter (PixelUnshuffle'd key planes), zero-masked reference tokens, negative-time RoPE ids, per-frame timestep modulation, the 4-step distilled flow-matching schedule, and taehv pixel decoding (stateless per-block decode with a 3-latent-frame overlap standing in for the reference's streaming decoder cache).include/stable-diffusion.h—sd_abot_session_{params_init,new,step,frames_free,free}: a standalone session (own DiT + taehv + scene pack, nosd_ctx_t);step(action_mask)generates the next block under the held keys (bits 0..7 = W,A,S,D,I,J,K,L) and returns its decoded RGB frames.examples/abot-{parity,walk,session}— validation harnesses (static builds only; they link engine internals that the shared library does not export).Validation (vs the PyTorch reference, F16 GGUF, fixed-scene goldens)
Regression safety
New code is only reachable for
VERSION_ABOT_WORLDmodels or through the newsd_abot_*entry points. Existing paths are untouched except: an optional defaultedattn_maskparameter onWanAttentionBlock::forward, andinlineon two free functions intae.hpp(ODR fix — the header is now included from more than one translation unit). Batch-generation rejection for ABot is unchanged and covered by the monorepo integration test.