From 74f5b14d18de3f14278eb6cc1488e9a36b0aa05e Mon Sep 17 00:00:00 2001 From: BFlat Date: Wed, 5 Mar 2025 10:23:48 +0700 Subject: [PATCH 1/8] added loading indicator text (#439) added signal for loading indicator text change --- .../SignaledLoadingIndicator.tsx | 4 +++- .../src/signals/uiEvents/loadingIndicator.ts | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/frontend/realtime/src/KonvaRootComponent/SignaledLoadingIndicator.tsx b/frontend/realtime/src/KonvaRootComponent/SignaledLoadingIndicator.tsx index f67ec2a66d..d2ea3b72c1 100644 --- a/frontend/realtime/src/KonvaRootComponent/SignaledLoadingIndicator.tsx +++ b/frontend/realtime/src/KonvaRootComponent/SignaledLoadingIndicator.tsx @@ -3,6 +3,7 @@ import { LoadingBar, LoadingBarStatus } from "~/components/ui"; import { isLoadingVisible, loadingProgress, + loadingText, } from "~/signals/uiEvents/loadingIndicator"; export const SignaledLoadingIndicator = () => { @@ -10,6 +11,7 @@ export const SignaledLoadingIndicator = () => { useSignalEffect(() => { isLoadingVisible.value; loadingProgress.value; + loadingText.value; }); if (!isLoadingVisible.value) { @@ -47,7 +49,7 @@ export const SignaledLoadingIndicator = () => { className="text-white/90" > - Downloading models - {loadingProgress.value}% + {loadingText.value} - {loadingProgress.value}% ); diff --git a/frontend/realtime/src/signals/uiEvents/loadingIndicator.ts b/frontend/realtime/src/signals/uiEvents/loadingIndicator.ts index 2a5ceae646..23b49d59c1 100644 --- a/frontend/realtime/src/signals/uiEvents/loadingIndicator.ts +++ b/frontend/realtime/src/signals/uiEvents/loadingIndicator.ts @@ -28,13 +28,28 @@ const onLoadingProgressChanged = (callback: (progress: number) => void) => { }); }; +// Loading text signal +export const DEFAULT_LOADING_TEXT = "Downloading"; +export const loadingText = signal(DEFAULT_LOADING_TEXT); +const setLoadingText = (text: string) => { + loadingText.value = text; +}; + +const onLoadingTextChanged = (callback: (text: string) => void) => { + effect(() => { + callback(loadingText.value); + }); +}; + // EXPORTS export const dispatchers = { setIsLoadingVisible, setLoadingProgress, + setLoadingText, }; export const events = { onIsLoadingVisibleChanged, onLoadingProgressChanged, + onLoadingTextChanged, }; From 4ab6ca1caaa3b7afce9bfab8a8d41954fff4b34b Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 5 Mar 2025 23:21:11 +0700 Subject: [PATCH 2/8] disabled annoying focus ring --- frontend/realtime/src/components/features/ToolbarButton.tsx | 2 +- frontend/realtime/src/components/ui/Button.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/realtime/src/components/features/ToolbarButton.tsx b/frontend/realtime/src/components/features/ToolbarButton.tsx index a55724f2e3..e8694ccf03 100644 --- a/frontend/realtime/src/components/features/ToolbarButton.tsx +++ b/frontend/realtime/src/components/features/ToolbarButton.tsx @@ -44,7 +44,7 @@ export const ToolbarButton = ({ } const mergedButtonClasses = twMerge( - "rounded-lg hover:bg-white/15 transition-all duration-100 border-2 border-transparent text-white/80 px-2", + "rounded-lg hover:bg-white/15 transition-all duration-100 border-2 border-transparent text-white/80 px-2 focus-visible:ring-0 focus-visible:ring-offset-0", children ? "w-fit flex items-center gap-2.5 text-nowrap" : "size-10", active && "bg-primary/30 border-2 border-primary hover:bg-primary/30", disabled && "pointer-events-none text-secondary-300", diff --git a/frontend/realtime/src/components/ui/Button.tsx b/frontend/realtime/src/components/ui/Button.tsx index 5e3deb69f6..be875daaf2 100644 --- a/frontend/realtime/src/components/ui/Button.tsx +++ b/frontend/realtime/src/components/ui/Button.tsx @@ -51,7 +51,7 @@ export const Button = ({ "text-[15px] font-semibold rounded-lg px-3 py-2 shadow-sm", "flex gap-2 items-center justify-center", "transition-all duration-150", - "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2", + "focus-visible:ring-0 focus-visible:ring-offset-0", getVariantClassNames(propsVariant), propsClassName, disabledClass, From 3556dcbfd3a33d1b7bd5bf45974183a231801d19 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 5 Mar 2025 23:40:33 +0700 Subject: [PATCH 3/8] removed annoying focus on buttons 2 --- frontend/realtime/src/app/global.css | 15 +++++++++++++++ .../src/components/features/ToolbarButton.tsx | 8 +++++++- frontend/realtime/src/components/ui/Button.tsx | 9 ++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/frontend/realtime/src/app/global.css b/frontend/realtime/src/app/global.css index 077379a221..3c0abd0ee7 100644 --- a/frontend/realtime/src/app/global.css +++ b/frontend/realtime/src/app/global.css @@ -6,6 +6,21 @@ :root { @apply select-none bg-ui-background font-sans text-white; } + + /* Remove all focus styles */ + button { + @apply outline-none ring-0; + -webkit-tap-highlight-color: transparent !important; + outline: none !important; + box-shadow: none !important; + } + + button:focus { + @apply outline-none ring-0; + outline: none !important; + box-shadow: none !important; + } + .pegboard { background-image: radial-gradient(#ffffff25 1px, transparent 0); background-color: #1b1b1f; diff --git a/frontend/realtime/src/components/features/ToolbarButton.tsx b/frontend/realtime/src/components/features/ToolbarButton.tsx index e8694ccf03..fadc860c35 100644 --- a/frontend/realtime/src/components/features/ToolbarButton.tsx +++ b/frontend/realtime/src/components/features/ToolbarButton.tsx @@ -44,7 +44,7 @@ export const ToolbarButton = ({ } const mergedButtonClasses = twMerge( - "rounded-lg hover:bg-white/15 transition-all duration-100 border-2 border-transparent text-white/80 px-2 focus-visible:ring-0 focus-visible:ring-offset-0", + "rounded-lg hover:bg-white/15 transition-all duration-100 border-2 border-transparent text-white/80 px-2", children ? "w-fit flex items-center gap-2.5 text-nowrap" : "size-10", active && "bg-primary/30 border-2 border-primary hover:bg-primary/30", disabled && "pointer-events-none text-secondary-300", @@ -58,6 +58,12 @@ export const ToolbarButton = ({ className={mergedButtonClasses} disabled={disabled} autoFocus={false} + tabIndex={-1} + style={{ + outline: "none", + boxShadow: "none", + WebkitTapHighlightColor: "transparent", + }} {...restButtonProps} onClick={(e) => { e.preventDefault(); diff --git a/frontend/realtime/src/components/ui/Button.tsx b/frontend/realtime/src/components/ui/Button.tsx index be875daaf2..fe83115ce5 100644 --- a/frontend/realtime/src/components/ui/Button.tsx +++ b/frontend/realtime/src/components/ui/Button.tsx @@ -28,17 +28,17 @@ export const Button = ({ function getVariantClassNames(variant: string) { switch (variant) { case "tertiary": { - return "bg-tertiary hover:bg-tertiary-400 text-white focus-visible:outline-tertiary-600"; + return "bg-tertiary hover:bg-tertiary-400 text-white"; } case "secondary": { - return "bg-ui-controls hover:bg-ui-controls/80 text-white focus-visible:outline-secondary"; + return "bg-ui-controls hover:bg-ui-controls/80 text-white"; } case "action": { - return " bg-action hover:bg-action-500 text-white focus-visible:outline-action"; + return "bg-action hover:bg-action-500 text-white"; } case "primary": default: { - return "bg-primary hover:bg-primary-600 text-white focus-visible:outline-primary-600"; + return "bg-primary hover:bg-primary-600 text-white"; } } } @@ -51,7 +51,6 @@ export const Button = ({ "text-[15px] font-semibold rounded-lg px-3 py-2 shadow-sm", "flex gap-2 items-center justify-center", "transition-all duration-150", - "focus-visible:ring-0 focus-visible:ring-offset-0", getVariantClassNames(propsVariant), propsClassName, disabledClass, From ada1f46e626b0d062d153ac9c8b8576bd1fe96ab Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 5 Mar 2025 18:24:05 -0500 Subject: [PATCH 4/8] model downloading (wip) --- Cargo.lock | 90 +++++++++++-------- crates/desktop/src-tauri/Cargo.toml | 1 + .../src/endpoints/download_models.rs | 17 ++++ .../src-tauri/src/endpoints/image_endpoint.rs | 18 ++-- crates/desktop/src-tauri/src/endpoints/mod.rs | 1 + crates/desktop/src-tauri/src/lib.rs | 2 + .../src/ml/downloads/download_all_models.rs | 8 ++ .../downloads/download_model_registry_file.rs | 21 +++++ .../desktop/src-tauri/src/ml/downloads/mod.rs | 2 + crates/desktop/src-tauri/src/ml/mod.rs | 4 +- .../src-tauri/src/ml/model_registry.rs | 25 ++++++ .../infer_clip_text_embeddings.rs | 13 ++- .../stable_diffusion_pipeline.rs | 5 +- 13 files changed, 160 insertions(+), 47 deletions(-) create mode 100644 crates/desktop/src-tauri/src/endpoints/download_models.rs create mode 100644 crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs create mode 100644 crates/desktop/src-tauri/src/ml/downloads/download_model_registry_file.rs create mode 100644 crates/desktop/src-tauri/src/ml/downloads/mod.rs create mode 100644 crates/desktop/src-tauri/src/ml/model_registry.rs diff --git a/Cargo.lock b/Cargo.lock index 820da85465..9702fccefc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -672,6 +672,7 @@ dependencies = [ "memory_caching", "once_cell", "rand 0.8.5", + "reqwest 0.12.12", "serde", "serde_json", "serde_yaml", @@ -1079,7 +1080,7 @@ dependencies = [ "rustversion", "serde", "sync_wrapper 0.1.2", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", ] @@ -3316,7 +3317,7 @@ dependencies = [ "google_drive_common", "hashing", "hostname 0.4.0", - "hyper 1.3.1", + "hyper 1.6.0", "jobs_common", "log", "mimetypes", @@ -4762,7 +4763,7 @@ dependencies = [ "errors", "jwt-simple", "my-workspace-hack", - "reqwest 0.12.7", + "reqwest 0.12.12", "serde", "serde_derive", "serde_json", @@ -4988,7 +4989,7 @@ dependencies = [ "native-tls", "num_cpus", "rand 0.8.5", - "reqwest 0.12.7", + "reqwest 0.12.12", "serde", "serde_json", "thiserror 2.0.11", @@ -5192,9 +5193,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -5234,9 +5235,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes 1.10.0", "futures-channel", @@ -5260,7 +5261,7 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.6.0", "hyper-util", "rustls 0.23.13", "rustls-pki-types", @@ -5303,7 +5304,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes 1.10.0", "http-body-util", - "hyper 1.3.1", + "hyper 1.6.0", "hyper-util", "native-tls", "tokio", @@ -5313,20 +5314,19 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes 1.10.0", "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.3.1", + "hyper 1.6.0", "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] @@ -5563,7 +5563,7 @@ dependencies = [ "rand 0.8.5", "redis_common", "redis_schema", - "reqwest 0.12.7", + "reqwest 0.12.12", "serde", "serde_derive", "serde_json", @@ -6852,7 +6852,7 @@ dependencies = [ "heck 0.4.1", "hmac 0.12.1", "hyper 0.14.28", - "hyper 1.3.1", + "hyper 1.6.0", "hyper-util", "indexmap 2.2.6", "itertools 0.12.1", @@ -6878,7 +6878,7 @@ dependencies = [ "regex", "regex-automata", "regex-syntax", - "reqwest 0.12.7", + "reqwest 0.12.12", "rsa", "rustls-pki-types", "semver 1.0.23", @@ -6904,7 +6904,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "tower", + "tower 0.4.13", "tracing", "unicode-bidi", "unicode-normalization", @@ -9269,9 +9269,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.7" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64 0.22.1", "bytes 1.10.0", @@ -9283,7 +9283,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.3.1", + "hyper 1.6.0", "hyper-rustls", "hyper-tls 0.6.0", "hyper-util", @@ -9308,6 +9308,7 @@ dependencies = [ "tokio-native-tls", "tokio-rustls", "tokio-util", + "tower 0.5.2", "tower-service", "url", "wasm-bindgen", @@ -9502,7 +9503,7 @@ dependencies = [ "minidom", "percent-encoding", "quick-xml 0.36.2", - "reqwest 0.12.7", + "reqwest 0.12.12", "serde", "serde_derive", "serde_json", @@ -10833,7 +10834,7 @@ dependencies = [ "redis_common", "redis_schema", "regex", - "reqwest 0.12.7", + "reqwest 0.12.12", "reusable_types", "rust-s3", "serde", @@ -11597,7 +11598,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.12.7", + "reqwest 0.12.12", "serde", "serde_json", "serde_repr", @@ -11945,7 +11946,7 @@ dependencies = [ "easyenv", "log", "my-workspace-hack", - "reqwest 0.12.7", + "reqwest 0.12.12", "serde_json", ] @@ -12399,7 +12400,7 @@ dependencies = [ "prost", "tokio", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -12435,16 +12436,31 @@ dependencies = [ ] [[package]] -name = "tower-layer" -version = "0.3.2" +name = "tower" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.1", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -12581,7 +12597,7 @@ dependencies = [ "google_drive_common", "hashing", "hostname 0.4.0", - "hyper 1.3.1", + "hyper 1.6.0", "jobs_common", "log", "my-workspace-hack", @@ -12619,7 +12635,7 @@ dependencies = [ "my-workspace-hack", "mysql_queries", "r2d2_redis", - "reqwest 0.12.7", + "reqwest 0.12.12", "serde", "serde_derive", "serde_json", @@ -12979,7 +12995,7 @@ dependencies = [ "actix-web", "mime_guess", "regex", - "reqwest 0.12.7", + "reqwest 0.12.12", "rust-embed", "serde", "serde_json", @@ -13108,7 +13124,7 @@ dependencies = [ "google_drive_common", "hashing", "hostname 0.4.0", - "hyper 1.3.1", + "hyper 1.6.0", "jobs_common", "log", "my-workspace-hack", @@ -13138,7 +13154,7 @@ dependencies = [ "filesys", "google_drive_common", "hostname 0.4.0", - "hyper 1.3.1", + "hyper 1.6.0", "jobs_common", "log", "my-workspace-hack", @@ -13315,7 +13331,7 @@ dependencies = [ "log", "my-workspace-hack", "once_cell", - "reqwest 0.12.7", + "reqwest 0.12.12", "rss", "scraper", "serde", diff --git a/crates/desktop/src-tauri/Cargo.toml b/crates/desktop/src-tauri/Cargo.toml index 23a42e61ed..c06ccc1160 100644 --- a/crates/desktop/src-tauri/Cargo.toml +++ b/crates/desktop/src-tauri/Cargo.toml @@ -39,6 +39,7 @@ serde_yaml = "0.9" log = "0.4" once_cell = "1.20.3" rand = "0.8" +reqwest = "0.12.12" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tauri = { version = "2.2.4", features = [] } diff --git a/crates/desktop/src-tauri/src/endpoints/download_models.rs b/crates/desktop/src-tauri/src/endpoints/download_models.rs new file mode 100644 index 0000000000..ae85380048 --- /dev/null +++ b/crates/desktop/src-tauri/src/endpoints/download_models.rs @@ -0,0 +1,17 @@ +use crate::ml::downloads::download_all_models::download_all_models; +use crate::state::app_config::AppConfig; +use tauri::{AppHandle, State}; + +#[tauri::command] +pub async fn download_models( + model_config: State<'_, AppConfig>, + app: AppHandle, +) -> Result { + + download_all_models().await + .map_err(|err| { + "error downloading models".to_string() + })?; + + Ok(("downloaded".to_string())) +} diff --git a/crates/desktop/src-tauri/src/endpoints/image_endpoint.rs b/crates/desktop/src-tauri/src/endpoints/image_endpoint.rs index a202e396ba..3336a54896 100644 --- a/crates/desktop/src-tauri/src/endpoints/image_endpoint.rs +++ b/crates/desktop/src-tauri/src/endpoints/image_endpoint.rs @@ -10,6 +10,7 @@ use log::{error, info}; use std::io::Cursor; use std::path::PathBuf; use tauri::{AppHandle, State}; +use crate::ml::downloads::download_all_models::download_all_models; const PROMPT_FILENAME : &str = "prompt.txt"; const PROMPT: &str = "A beautiful landscape with mountains and a lake"; @@ -40,7 +41,14 @@ pub async fn infer_image( let image = hydrate_base64_image(image) .map_err(|err| format!("Couldn't hydrate image from base64: {}", err))?; - let result = do_infer_image(&prompt, image, strength, &model_config, &model_cache, prompt_cache, app); + // TODO(bt): Move this to another endpoint + download_all_models().await + .map_err(|err| { + error!("couldn't download models: {:?}", err); + "Couldn't download models".to_string() + })?; + + let result = do_infer_image(&prompt, image, strength, &model_config, &model_cache, prompt_cache, app).await; if let Err(err) = result.as_deref() { error!("There was an error: {:?}", err); @@ -49,22 +57,20 @@ pub async fn infer_image( result } -fn do_infer_image( +async fn do_infer_image( prompt: &str, image: DynamicImage, strength: Option, config: &AppConfig, model_cache: &ModelCache, - prompt_cache: State, + prompt_cache: State<'_, PromptCache>, app: AppHandle, ) -> Result { - println!("infer_image called; generating image with SDXL Turbo..."); - + let args = Args { image: &image, prompt: prompt.to_string(), uncond_prompt: NEGATIVE_PROMPT.to_string(), - //guidance_scale: Some(0.0), model_cache, configs: config, prompt_cache: &prompt_cache, diff --git a/crates/desktop/src-tauri/src/endpoints/mod.rs b/crates/desktop/src-tauri/src/endpoints/mod.rs index 4872b3088f..fc4c6dac2a 100644 --- a/crates/desktop/src-tauri/src/endpoints/mod.rs +++ b/crates/desktop/src-tauri/src/endpoints/mod.rs @@ -1,3 +1,4 @@ +pub mod download_models; pub mod flip_image; pub mod image_endpoint; pub mod test_counter; diff --git a/crates/desktop/src-tauri/src/lib.rs b/crates/desktop/src-tauri/src/lib.rs index 7f656b907b..524a98e151 100644 --- a/crates/desktop/src-tauri/src/lib.rs +++ b/crates/desktop/src-tauri/src/lib.rs @@ -6,6 +6,7 @@ pub mod state; use crate::ml::model_cache::ModelCache; use crate::ml::prompt_cache::PromptCache; use crate::state::app_config::AppConfig; +use endpoints::download_models::download_models; use endpoints::image_endpoint::infer_image; use endpoints::test_counter::test_counter; use tauri_plugin_log::Target; @@ -50,6 +51,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ test_counter, infer_image, + download_models, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs b/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs new file mode 100644 index 0000000000..3a916b6122 --- /dev/null +++ b/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs @@ -0,0 +1,8 @@ +use crate::ml::downloads::download_model_registry_file::download_model_registry_file; +use crate::ml::model_registry::ModelRegistry; + +pub async fn download_all_models() -> anyhow::Result<()> { + download_model_registry_file(ModelRegistry::Clip).await?; + + Ok(()) +} diff --git a/crates/desktop/src-tauri/src/ml/downloads/download_model_registry_file.rs b/crates/desktop/src-tauri/src/ml/downloads/download_model_registry_file.rs new file mode 100644 index 0000000000..375e88352d --- /dev/null +++ b/crates/desktop/src-tauri/src/ml/downloads/download_model_registry_file.rs @@ -0,0 +1,21 @@ +use std::fs::File; +use std::path::{Path, PathBuf}; +use crate::ml::model_registry::ModelRegistry; + +pub async fn download_model_registry_file(model_type: ModelRegistry) -> anyhow::Result { + let path = PathBuf::from(model_type.get_filename()); + if path.exists() { + return Ok(path); + } + + let body = reqwest::get(model_type.get_download_url()) + .await? + .bytes() + .await?; + + let _file = File::create(&path)?; + + std::fs::write(&path, body.as_ref())?; + + Ok(path) +} \ No newline at end of file diff --git a/crates/desktop/src-tauri/src/ml/downloads/mod.rs b/crates/desktop/src-tauri/src/ml/downloads/mod.rs new file mode 100644 index 0000000000..0ca6ed312a --- /dev/null +++ b/crates/desktop/src-tauri/src/ml/downloads/mod.rs @@ -0,0 +1,2 @@ +pub mod download_model_registry_file; +pub mod download_all_models; diff --git a/crates/desktop/src-tauri/src/ml/mod.rs b/crates/desktop/src-tauri/src/ml/mod.rs index 87a2bc1e4c..42ca9d128b 100644 --- a/crates/desktop/src-tauri/src/ml/mod.rs +++ b/crates/desktop/src-tauri/src/ml/mod.rs @@ -3,4 +3,6 @@ pub mod model_cache; pub mod model_file; pub mod models; pub mod prompt_cache; -pub mod stable_diffusion; \ No newline at end of file +pub mod stable_diffusion; +pub mod model_registry; +pub mod downloads; \ No newline at end of file diff --git a/crates/desktop/src-tauri/src/ml/model_registry.rs b/crates/desktop/src-tauri/src/ml/model_registry.rs new file mode 100644 index 0000000000..5204bad425 --- /dev/null +++ b/crates/desktop/src-tauri/src/ml/model_registry.rs @@ -0,0 +1,25 @@ + +#[derive(Copy,Clone)] +pub enum ModelRegistry { + Clip +} + +impl ModelRegistry { + pub fn get_hf_id(&self) -> Option<&'static str> { + match self { + Self::Clip => Some("laion/CLIP-ViT-bigG-14-laion2B-39B-b160k"), + } + } + + pub fn get_download_url(&self) -> &'static str { + match self { + Self::Clip => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/tokenizer.json", + } + } + + pub fn get_filename(&self) -> &'static str { + match self { + Self::Clip => "clip_vit_bigg_14_lion2b_39b_b160k.tokenizer.json", + } + } +} diff --git a/crates/desktop/src-tauri/src/ml/stable_diffusion/infer_clip_text_embeddings.rs b/crates/desktop/src-tauri/src/ml/stable_diffusion/infer_clip_text_embeddings.rs index 8a4836f729..80cade0ff3 100644 --- a/crates/desktop/src-tauri/src/ml/stable_diffusion/infer_clip_text_embeddings.rs +++ b/crates/desktop/src-tauri/src/ml/stable_diffusion/infer_clip_text_embeddings.rs @@ -3,6 +3,7 @@ extern crate accelerate_src; #[cfg(feature = "mkl")] extern crate intel_mkl_src; +use std::path::PathBuf; use candle_transformers::models::stable_diffusion; use crate::ml::model_file::{ModelFile, StableDiffusionVersion}; @@ -10,6 +11,7 @@ use anyhow::Error as E; use candle_core::{DType, Device, Module, Tensor, D}; use log::info; use tokenizers::Tokenizer; +use crate::ml::model_registry::ModelRegistry; #[allow(clippy::too_many_arguments)] pub fn infer_clip_text_embeddings( @@ -76,12 +78,21 @@ fn do_infer_clip_text_embeddings( ) -> anyhow::Result { let tokenizer_file = if first { + info!("ModelFile::Tokenizer"); ModelFile::Tokenizer } else { + info!("ModelFile::Tokenizer2"); ModelFile::Tokenizer2 }; - let tokenizer = tokenizer_file.get(tokenizer, sd_version, use_f16)?; + + info!("Loading clip from download"); + //let tokenizer = tokenizer_file.get(tokenizer, sd_version, use_f16)?; + let tokenizer = PathBuf::from(ModelRegistry::Clip.get_filename()); + + info!("Tokenizer path: {:?}", tokenizer); + let tokenizer = Tokenizer::from_file(tokenizer).map_err(E::msg)?; + let pad_id = match &sd_config.clip.pad_with { Some(padding) => *tokenizer.get_vocab(true).get(padding.as_str()).unwrap(), None => *tokenizer.get_vocab(true).get("<|endoftext|>").unwrap(), diff --git a/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs b/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs index 16f45200a7..ea8f624bd7 100644 --- a/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs +++ b/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs @@ -1,9 +1,12 @@ use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use crate::events::notification_event::{ModelType, NotificationEvent}; use crate::ml::image::dynamic_image_to_tensor::dynamic_image_to_tensor; use crate::ml::image::tensor_to_image_buffer::{tensor_to_image_buffer, RgbImage}; use crate::ml::model_cache::ModelCache; use crate::ml::model_file::StableDiffusionVersion; +use crate::ml::model_registry::ModelRegistry; +use crate::ml::models::unet_model::UNetModel; use crate::ml::prompt_cache::PromptCache; use crate::ml::stable_diffusion::get_vae_scale::get_vae_scale; use crate::ml::stable_diffusion::infer_clip_text_embeddings::infer_clip_text_embeddings; @@ -15,8 +18,6 @@ use image::DynamicImage; use log::info; use rand::Rng; use tauri::{AppHandle, Emitter}; -use crate::events::notification_event::{ModelType, NotificationEvent}; -use crate::ml::models::unet_model::UNetModel; pub struct Args<'a> { pub image: &'a DynamicImage, From 706becfd7e978b292835a3815d27731f6e0c0b30 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 5 Mar 2025 19:32:41 -0500 Subject: [PATCH 5/8] download unet --- .../src/ml/downloads/download_all_models.rs | 1 + .../ml/downloads/download_model_registry_file.rs | 3 +++ crates/desktop/src-tauri/src/ml/model_registry.rs | 12 ++++++++---- .../stable_diffusion/stable_diffusion_pipeline.rs | 14 ++++---------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs b/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs index 3a916b6122..6699daf7a5 100644 --- a/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs +++ b/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs @@ -3,6 +3,7 @@ use crate::ml::model_registry::ModelRegistry; pub async fn download_all_models() -> anyhow::Result<()> { download_model_registry_file(ModelRegistry::Clip).await?; + download_model_registry_file(ModelRegistry::SdxlTurboUnet).await?; Ok(()) } diff --git a/crates/desktop/src-tauri/src/ml/downloads/download_model_registry_file.rs b/crates/desktop/src-tauri/src/ml/downloads/download_model_registry_file.rs index 375e88352d..0e8cc0fb14 100644 --- a/crates/desktop/src-tauri/src/ml/downloads/download_model_registry_file.rs +++ b/crates/desktop/src-tauri/src/ml/downloads/download_model_registry_file.rs @@ -1,10 +1,13 @@ use std::fs::File; use std::path::{Path, PathBuf}; +use log::info; use crate::ml::model_registry::ModelRegistry; pub async fn download_model_registry_file(model_type: ModelRegistry) -> anyhow::Result { + info!("downloading model: {:?}", model_type); let path = PathBuf::from(model_type.get_filename()); if path.exists() { + info!("model already exists: {:?}", path); return Ok(path); } diff --git a/crates/desktop/src-tauri/src/ml/model_registry.rs b/crates/desktop/src-tauri/src/ml/model_registry.rs index 5204bad425..144431f4fd 100644 --- a/crates/desktop/src-tauri/src/ml/model_registry.rs +++ b/crates/desktop/src-tauri/src/ml/model_registry.rs @@ -1,25 +1,29 @@ -#[derive(Copy,Clone)] +#[derive(Copy, Clone, Debug)] pub enum ModelRegistry { - Clip + Clip, + SdxlTurboUnet, } impl ModelRegistry { pub fn get_hf_id(&self) -> Option<&'static str> { match self { Self::Clip => Some("laion/CLIP-ViT-bigG-14-laion2B-39B-b160k"), + Self::SdxlTurboUnet => Some("stabilityai/sdxl-turbo"), } } - + pub fn get_download_url(&self) -> &'static str { match self { Self::Clip => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/tokenizer.json", + Self::SdxlTurboUnet => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/diffusion_pytorch_model.unet.safetensors", } } - + pub fn get_filename(&self) -> &'static str { match self { Self::Clip => "clip_vit_bigg_14_lion2b_39b_b160k.tokenizer.json", + Self::SdxlTurboUnet => "diffusion_pytorch_model.unet.safetensors", } } } diff --git a/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs b/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs index ea8f624bd7..d5bb1955d5 100644 --- a/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs +++ b/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs @@ -168,23 +168,17 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result { None => { info!("No unet found in cache; loading..."); - let repo = configs.sd_version.repo(); - - info!("Downloading UNET model files from: {} ...", repo); - - let unet_file = configs.hf_api.model(repo.to_string()) - .get("unet/diffusion_pytorch_model.safetensors") - .map_err(|err| anyhow!("error fetching model: {:?}", err))?; - let mut notify_download_complete = false; //if !unet_file.exists() { if true { notify_download_complete = true; app.emit("notification", NotificationEvent::ModelDownloadStarted { - model_name: repo, + model_name: "sdxl-turbo-unet", model_type: ModelType::Unet, })?; } + + let unet_file = ModelRegistry::SdxlTurboUnet.get_filename(); let unet = UNetModel::new(&configs.sd_config, unet_file, &configs.device, configs.dtype) .map_err(|err| anyhow!("error initializing unet model: {:?}", err))?; @@ -195,7 +189,7 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result { if notify_download_complete { app.emit("notification", NotificationEvent::ModelDownloadComplete { - model_name: repo, + model_name: "sdxl-turbo-unet", model_type: ModelType::Unet, })?; } From a859bd427bf4d5eda205afd2e38b07305f8377e5 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 6 Mar 2025 08:42:44 -0500 Subject: [PATCH 6/8] download clip models (not using them yet) --- .../src/ml/downloads/download_all_models.rs | 6 +++-- .../src-tauri/src/ml/model_registry.rs | 22 ++++++++++++++----- .../infer_clip_text_embeddings.rs | 2 +- .../stable_diffusion_pipeline.rs | 15 ++++--------- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs b/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs index 6699daf7a5..953e084345 100644 --- a/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs +++ b/crates/desktop/src-tauri/src/ml/downloads/download_all_models.rs @@ -2,8 +2,10 @@ use crate::ml::downloads::download_model_registry_file::download_model_registry_ use crate::ml::model_registry::ModelRegistry; pub async fn download_all_models() -> anyhow::Result<()> { - download_model_registry_file(ModelRegistry::Clip).await?; + download_model_registry_file(ModelRegistry::ClipJson).await?; download_model_registry_file(ModelRegistry::SdxlTurboUnet).await?; - + download_model_registry_file(ModelRegistry::SdxlTurboVae).await?; + download_model_registry_file(ModelRegistry::SdxlTurboClipEncoder).await?; + download_model_registry_file(ModelRegistry::SdxlTurboClipEncoder2).await?; Ok(()) } diff --git a/crates/desktop/src-tauri/src/ml/model_registry.rs b/crates/desktop/src-tauri/src/ml/model_registry.rs index 144431f4fd..e3a4e933b7 100644 --- a/crates/desktop/src-tauri/src/ml/model_registry.rs +++ b/crates/desktop/src-tauri/src/ml/model_registry.rs @@ -1,29 +1,41 @@ #[derive(Copy, Clone, Debug)] pub enum ModelRegistry { - Clip, + ClipJson, SdxlTurboUnet, + SdxlTurboVae, + SdxlTurboClipEncoder, + SdxlTurboClipEncoder2, } impl ModelRegistry { pub fn get_hf_id(&self) -> Option<&'static str> { match self { - Self::Clip => Some("laion/CLIP-ViT-bigG-14-laion2B-39B-b160k"), - Self::SdxlTurboUnet => Some("stabilityai/sdxl-turbo"), + Self::ClipJson => Some("laion/CLIP-ViT-bigG-14-laion2B-39B-b160k"), + Self::SdxlTurboUnet + | Self::SdxlTurboVae + | Self::SdxlTurboClipEncoder + | Self::SdxlTurboClipEncoder2 => Some("stabilityai/sdxl-turbo"), } } pub fn get_download_url(&self) -> &'static str { match self { - Self::Clip => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/tokenizer.json", + Self::ClipJson => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/tokenizer.json", Self::SdxlTurboUnet => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/diffusion_pytorch_model.unet.safetensors", + Self::SdxlTurboVae => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/diffusion_pytorch_model.vae.safetensors", + Self::SdxlTurboClipEncoder => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/clip_text_encoder.safetensors", + Self::SdxlTurboClipEncoder2 => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/clip_text_encoder_2.safetensors", } } pub fn get_filename(&self) -> &'static str { match self { - Self::Clip => "clip_vit_bigg_14_lion2b_39b_b160k.tokenizer.json", + Self::ClipJson => "clip_vit_bigg_14_lion2b_39b_b160k.tokenizer.json", Self::SdxlTurboUnet => "diffusion_pytorch_model.unet.safetensors", + Self::SdxlTurboVae => "diffusion_pytorch_model.vae.safetensors", + Self::SdxlTurboClipEncoder => "clip_text_encoder.safetensors", + Self::SdxlTurboClipEncoder2 => "clip_text_encoder_2.safetensors", } } } diff --git a/crates/desktop/src-tauri/src/ml/stable_diffusion/infer_clip_text_embeddings.rs b/crates/desktop/src-tauri/src/ml/stable_diffusion/infer_clip_text_embeddings.rs index 80cade0ff3..11a56137e1 100644 --- a/crates/desktop/src-tauri/src/ml/stable_diffusion/infer_clip_text_embeddings.rs +++ b/crates/desktop/src-tauri/src/ml/stable_diffusion/infer_clip_text_embeddings.rs @@ -87,7 +87,7 @@ fn do_infer_clip_text_embeddings( info!("Loading clip from download"); //let tokenizer = tokenizer_file.get(tokenizer, sd_version, use_f16)?; - let tokenizer = PathBuf::from(ModelRegistry::Clip.get_filename()); + let tokenizer = PathBuf::from(ModelRegistry::ClipJson.get_filename()); info!("Tokenizer path: {:?}", tokenizer); diff --git a/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs b/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs index d5bb1955d5..60e3361078 100644 --- a/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs +++ b/crates/desktop/src-tauri/src/ml/stable_diffusion/stable_diffusion_pipeline.rs @@ -123,25 +123,18 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result { None => { info!("No vae found in cache; loading..."); - let repo = configs.sd_version.repo(); - - println!("Building VAE model from : {:?} ... (3)", repo); - - let vae_file = configs.hf_api.model(repo.to_string()) - .get("vae/diffusion_pytorch_model.safetensors")?; - - println!("Building VAE model from file {:?}...", &vae_file); - let mut notify_download_complete = false; //if !vae_file.exists() { if true { notify_download_complete = true; app.emit("notification", NotificationEvent::ModelDownloadStarted { - model_name: repo, + model_name: "sdxl-turbo-vae", model_type: ModelType::Vae, })?; } + let vae_file = ModelRegistry::SdxlTurboVae.get_filename(); + let vae = configs .sd_config .build_vae(vae_file, &configs.device, configs.dtype)?; @@ -152,7 +145,7 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result { if notify_download_complete { app.emit("notification", NotificationEvent::ModelDownloadComplete { - model_name: repo, + model_name: "sdxl-turbo-vae", model_type: ModelType::Vae, })?; } From c2929cfdb6748f585be4cb91cfcfbcad222e926d Mon Sep 17 00:00:00 2001 From: BFlat Date: Thu, 6 Mar 2025 20:52:25 +0700 Subject: [PATCH 7/8] color picker ux and make paint cursor visible on both dark/light bg (#441) ux updates to color picker and paint cursor --- .../RenderingPrimitives/RealTimeDrawEngine.ts | 34 ++++---- frontend/realtime/src/app/global.css | 27 +++++++ .../src/components/ui/HexEyedropPicker.tsx | 56 +++++++------ .../components/ui/TextEditor/ColorPicker.tsx | 7 +- .../ui/ToolbarMain/PaintModeMenu.tsx | 78 ++++++++++++++++--- 5 files changed, 145 insertions(+), 57 deletions(-) diff --git a/frontend/realtime/src/KonvaApp/RenderingPrimitives/RealTimeDrawEngine.ts b/frontend/realtime/src/KonvaApp/RenderingPrimitives/RealTimeDrawEngine.ts index 0efc9e5a29..134e444fdb 100644 --- a/frontend/realtime/src/KonvaApp/RenderingPrimitives/RealTimeDrawEngine.ts +++ b/frontend/realtime/src/KonvaApp/RenderingPrimitives/RealTimeDrawEngine.ts @@ -4,7 +4,7 @@ import { Shape } from "konva/lib/Shape"; import { Group } from "konva/lib/Group"; import { invoke } from "@tauri-apps/api/core"; -import { listen } from '@tauri-apps/api/event'; +import { listen } from "@tauri-apps/api/event"; import { FileUtilities } from "../FileUtilities/FileUtilities"; import { ImageNode, VideoNode, TextNode, ShapeNode, ShapeType } from "../Nodes"; @@ -31,7 +31,6 @@ interface ServerSettings { lora_path?: string; } - interface GenerateImageParams { image: string; prompt: string; @@ -210,16 +209,18 @@ export class RealTimeDrawEngine { console.log(event); // Handle model download events const payload = event.payload as any; - + // Model download started if (payload.model_download_started) { const modelInfo = payload.model_download_started; - console.log(`Model download started: ${modelInfo.model_name} (${modelInfo.model_type})`); - + console.log( + `Model download started: ${modelInfo.model_name} (${modelInfo.model_type})`, + ); + // Set up loading indicator isLoadingVisible.value = true; loadingProgress.value = 0; - + // Create fake progress updates const downloadTimer = setInterval(() => { loadingProgress.value += 2; @@ -230,23 +231,23 @@ export class RealTimeDrawEngine { } }, 500); } - + // Model download completed if (payload.model_download_complete) { const modelInfo = payload.model_download_complete; - console.log(`Model download completed: ${modelInfo.model_name} (${modelInfo.model_type})`); - + console.log( + `Model download completed: ${modelInfo.model_name} (${modelInfo.model_type})`, + ); + // Complete the loading progress loadingProgress.value = 100; - + // Hide loading indicator after a short delay setTimeout(() => { isLoadingVisible.value = false; }, 1000); } - }); - } private isEnabled: boolean = false; @@ -320,9 +321,16 @@ export class RealTimeDrawEngine { const ctx = cursorCanvas.getContext("2d"); if (!ctx) return; - // Draw the circle + // Draw the outer circle with a light stroke ctx.beginPath(); ctx.arc(size, size, size / 2, 0, Math.PI * 2); + ctx.strokeStyle = "white"; + ctx.lineWidth = 3; + ctx.stroke(); + + // Draw the inner circle with a dark stroke + ctx.beginPath(); + ctx.arc(size, size, size / 2 - 1, 0, Math.PI * 2); ctx.strokeStyle = "black"; ctx.lineWidth = 1; ctx.stroke(); diff --git a/frontend/realtime/src/app/global.css b/frontend/realtime/src/app/global.css index 3c0abd0ee7..2a70999845 100644 --- a/frontend/realtime/src/app/global.css +++ b/frontend/realtime/src/app/global.css @@ -202,3 +202,30 @@ background-position: -200% 0; } } + +.react-colorful__saturation { + margin: 16px 0; + border-radius: 10px !important; + border-bottom: none !important; +} + +.react-colorful__saturation-pointer { + width: 24px !important; + height: 24px !important; +} + +.react-colorful__hue, +.react-colorful__alpha { + height: 20px !important; + border-radius: 50px !important; +} + +.react-colorful__hue { + margin-bottom: 12px; +} + +.react-colorful__hue-pointer, +.react-colorful__alpha-pointer { + width: 20px !important; + height: 20px !important; +} diff --git a/frontend/realtime/src/components/ui/HexEyedropPicker.tsx b/frontend/realtime/src/components/ui/HexEyedropPicker.tsx index d2e1791b0b..9c0af09a8e 100644 --- a/frontend/realtime/src/components/ui/HexEyedropPicker.tsx +++ b/frontend/realtime/src/components/ui/HexEyedropPicker.tsx @@ -1,12 +1,13 @@ -import { MouseEvent, MouseEventHandler, useState } from "react"; -import { HexColorPicker } from "react-colorful"; -import { EyeDropper } from "react-eyedrop"; import { Button } from "./Button"; import { faEyedropper } from "@fortawesome/pro-solid-svg-icons"; import { twMerge } from "tailwind-merge"; -type ColorPickerHTMLAttributes = Omit, "color" | "onChange" | "onChangeCapture">; -interface ColorPickerBaseProps extends ColorPickerHTMLAttributes { +type ColorPickerHTMLAttributes = Omit< + React.HTMLAttributes, + "color" | "onChange" | "onChangeCapture" +>; +interface ColorPickerBaseProps + extends ColorPickerHTMLAttributes { color: T; onChange: (newColor: T) => void; } @@ -17,7 +18,7 @@ export const HexEyedropPicker = ({ dropperClassName, pickerClassName, onPickerChange, - Picker + Picker, }: { color: string; onDropperChange?: (newColor: string) => void; @@ -26,9 +27,8 @@ export const HexEyedropPicker = ({ onPickerChange: (newColor: string) => void; Picker: (props: Partial>) => JSX.Element; }) => { - const handleDropperClick = () => { - if (!('EyeDropper' in window)) { + if (!("EyeDropper" in window)) { // NOTE: This shouldn't happen cuz we're using Chromium console.error("EyeDropper not found in window"); return; @@ -46,42 +46,40 @@ export const HexEyedropPicker = ({ // User cancelled the picker return null; } - } + }; startEyeDrop().then((newColor) => { if (newColor) { onDropperChange?.(newColor); } }); - } + }; - const [isDragging, setIsDragging] = useState(false); - const handleOnMouseDown = () => { - setIsDragging(true); - } - const handleOnMouseUp = () => { - setIsDragging(false); - } + // const [isDragging, setIsDragging] = useState(false); + // const handleOnMouseDown = () => { + // setIsDragging(true); + // }; + // const handleOnMouseUp = () => { + // setIsDragging(false); + // }; return (
-
- ) -} + ); +}; diff --git a/frontend/realtime/src/components/ui/TextEditor/ColorPicker.tsx b/frontend/realtime/src/components/ui/TextEditor/ColorPicker.tsx index fc55f775b5..97cdaef38d 100644 --- a/frontend/realtime/src/components/ui/TextEditor/ColorPicker.tsx +++ b/frontend/realtime/src/components/ui/TextEditor/ColorPicker.tsx @@ -91,7 +91,7 @@ export const ColorPicker = ({ if (streamChanges) { onChange(color); } - } + }; return ( @@ -124,7 +124,7 @@ export const ColorPicker = ({ anchor={anchor} className={twMerge( paperWrapperStyles, - "flex flex-col items-center gap-2 overflow-hidden", + "flex flex-col items-center gap-2 overflow-hidden p-4", )} style={ { @@ -134,13 +134,14 @@ export const ColorPicker = ({ static > void) | null>(null); + const closeTimeoutRef = useRef(null); useEffect(() => { setIsOpen(defaultOpen); }, [defaultOpen]); + // Global mouse up handler + useEffect(() => { + const handleGlobalMouseUp = () => { + if (isMouseDown) { + setIsMouseDown(false); + + // If mouse is outside when released, close the panel after a short delay + if (isMouseOutside && closeOnMouseLeave && closeRef.current) { + closeTimeoutRef.current = setTimeout(() => { + onChange(currColor); + setIsOpen(false); + closeRef.current?.(); + }, 500); + } + } + }; + + window.addEventListener("mouseup", handleGlobalMouseUp); + + return () => { + window.removeEventListener("mouseup", handleGlobalMouseUp); + }; + }, [isMouseDown, isMouseOutside, closeOnMouseLeave, currColor, onChange]); + const handleMouseLeave = (close: () => void) => { - if (closeOnMouseLeave) { - onChange(currColor); - setIsOpen(false); - close(); + closeRef.current = close; + setIsMouseOutside(true); + + // Only close if mouse is not down and closeOnMouseLeave is true + if (closeOnMouseLeave && !isMouseDown) { + closeTimeoutRef.current = setTimeout(() => { + onChange(currColor); + setIsOpen(false); + close(); + }, 200); + } + }; + + const handleMouseEnter = () => { + setIsMouseOutside(false); + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = null; } }; @@ -92,7 +139,7 @@ export const PaintModeMenu = ({ if (streamChanges) { onChange(color); } - } + }; // Rerender the component when these signals change: useSignals(); @@ -123,12 +170,16 @@ export const PaintModeMenu = ({ )} {(open || isOpen) && ( -
handleMouseLeave(close)}> +
handleMouseLeave(close)} + onMouseEnter={handleMouseEnter} + onMouseDown={() => setIsMouseDown(true)} + > -
-

Brush Size:

+
+

+ Brush Size: +

Date: Thu, 6 Mar 2025 21:11:08 +0530 Subject: [PATCH 8/8] starting point (#440) --- app_config.yaml | 2 +- crates/desktop/src-tauri/Cargo.toml | 2 +- .../src-tauri/src/endpoints/image_endpoint.rs | 4 +- .../src-tauri/src/ml/models/unet_model.rs | 29 +- .../src/ml/stable_diffusion/lcm_pipeline.rs | 294 +++ .../src-tauri/src/ml/stable_diffusion/mod.rs | 3 +- .../desktop/src-tauri/src/state/app_config.rs | 2 +- .../candle-examples/examples/lcm/main.rs | 1670 +++++++++-------- .../vendor/candle/candle-flash-attn/build.rs | 2 + crates/vendor/candle/candle-nn/Cargo.toml | 1 + .../src/models/stable_diffusion/lcm.rs | 1148 +++++------ .../src/models/stable_diffusion/mod.rs | 10 + .../src/models/stable_diffusion/unet_2d.rs | 154 +- 13 files changed, 1853 insertions(+), 1468 deletions(-) create mode 100644 crates/desktop/src-tauri/src/ml/stable_diffusion/lcm_pipeline.rs diff --git a/app_config.yaml b/app_config.yaml index fca2ac4c14..2ddf4fd258 100644 --- a/app_config.yaml +++ b/app_config.yaml @@ -2,6 +2,6 @@ image_width: 512 image_height: 512 #scheduler_steps: 15 # NB(bt): It looks like we can get down to 2 sampling steps, perhaps ? -scheduler_steps: 2 +scheduler_steps: 4 scheduler_samples: 1 cfg_scale: 1.1 diff --git a/crates/desktop/src-tauri/Cargo.toml b/crates/desktop/src-tauri/Cargo.toml index c06ccc1160..aaa0304add 100644 --- a/crates/desktop/src-tauri/Cargo.toml +++ b/crates/desktop/src-tauri/Cargo.toml @@ -48,7 +48,7 @@ tokenizers = "0.15.2" [features] default = ["cuda"] -cuda = ["candle-core/cuda", "candle-nn/cuda", "candle-transformers/cuda"] +cuda = ["candle-core/cuda", "candle-nn/cuda", "candle-transformers/cuda", "candle-transformers/flash-attn", "candle-core/cudnn"] [dev-dependencies] libloading = "0.8.6" diff --git a/crates/desktop/src-tauri/src/endpoints/image_endpoint.rs b/crates/desktop/src-tauri/src/endpoints/image_endpoint.rs index 3336a54896..6343743efb 100644 --- a/crates/desktop/src-tauri/src/endpoints/image_endpoint.rs +++ b/crates/desktop/src-tauri/src/endpoints/image_endpoint.rs @@ -1,6 +1,6 @@ use crate::ml::model_cache::ModelCache; use crate::ml::prompt_cache::PromptCache; -use crate::ml::stable_diffusion::stable_diffusion_pipeline::{stable_diffusion_pipeline, Args}; +use crate::ml::stable_diffusion::lcm_pipeline::{lcm_pipeline, Args}; use crate::state::app_config::AppConfig; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; use bytes::BytesMut; @@ -79,7 +79,7 @@ async fn do_infer_image( app: &app, }; - match stable_diffusion_pipeline(args) { + match lcm_pipeline(args) { Ok(image) => { let mut bytes = Vec::with_capacity(1024*1024); diff --git a/crates/desktop/src-tauri/src/ml/models/unet_model.rs b/crates/desktop/src-tauri/src/ml/models/unet_model.rs index 2483525c96..4a8416c02f 100644 --- a/crates/desktop/src-tauri/src/ml/models/unet_model.rs +++ b/crates/desktop/src-tauri/src/ml/models/unet_model.rs @@ -8,28 +8,19 @@ pub struct UNetModel { } impl UNetModel { - pub fn new>( - sd_config: &StableDiffusionConfig, - unet_weights_file: P, - device: &Device, - dtype: DType, - ) -> anyhow::Result { + pub fn new>(sd_config: &StableDiffusionConfig, unet_weights_file: P, device: &Device, dtype: DType) -> anyhow::Result { println!("building unet model... (1)"); - - let model = sd_config.build_unet( - unet_weights_file, - &device, - 4, - false, - dtype - )?; - - Ok(Self { - model, - }) + + let model = sd_config.build_unet(unet_weights_file, &device, 4, true, dtype)?; + + Ok(Self { model }) } - + pub fn inference(&self, latent_model_input: &Tensor, timestep: f64, text_embeddings: &Tensor) -> anyhow::Result { Ok(self.model.forward(&latent_model_input, timestep, &text_embeddings)?) } + + pub fn forward_with_guidance(&self, latent_model_input: &Tensor, timestep: f64, text_embeddings: &Tensor, guidance_scale_embedding: &Tensor) -> anyhow::Result { + Ok(self.model.forward_with_guidance(&latent_model_input, timestep, &text_embeddings, Some(guidance_scale_embedding))?) + } } diff --git a/crates/desktop/src-tauri/src/ml/stable_diffusion/lcm_pipeline.rs b/crates/desktop/src-tauri/src/ml/stable_diffusion/lcm_pipeline.rs new file mode 100644 index 0000000000..ed294220d5 --- /dev/null +++ b/crates/desktop/src-tauri/src/ml/stable_diffusion/lcm_pipeline.rs @@ -0,0 +1,294 @@ +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; + +use crate::events::notification_event::{ModelType, NotificationEvent}; +use crate::ml::image::dynamic_image_to_tensor::dynamic_image_to_tensor; +use crate::ml::image::tensor_to_image_buffer::{tensor_to_image_buffer, RgbImage}; +use crate::ml::model_cache::ModelCache; +use crate::ml::model_file::StableDiffusionVersion; +use crate::ml::model_registry::ModelRegistry; +use crate::ml::models::unet_model::UNetModel; +use crate::ml::prompt_cache::PromptCache; +use crate::ml::stable_diffusion::get_vae_scale::get_vae_scale; +use crate::ml::stable_diffusion::infer_clip_text_embeddings::infer_clip_text_embeddings; +use crate::state::app_config::AppConfig; +use anyhow::{anyhow, Error as E, Result}; +use candle_core::{DType, IndexOp, Tensor, D}; +use candle_transformers::models::stable_diffusion::vae::{AutoEncoderKL, DiagonalGaussianDistribution}; +use candle_transformers::models::stable_diffusion::lcm::LCMScheduler; +use image::DynamicImage; +use log::info; +use rand::Rng; +use tauri::{AppHandle, Emitter}; + +pub struct Args<'a> { + pub image: &'a DynamicImage, + pub prompt: String, + pub uncond_prompt: String, + pub cfg_scale: Option, + pub i2i_strength: Option, + pub configs: &'a AppConfig, + pub model_cache: &'a ModelCache, + pub prompt_cache: &'a PromptCache, + pub app: &'a AppHandle, +} + +pub fn lcm_pipeline(args: Args<'_>) -> Result { + let Args { + prompt, + uncond_prompt, + cfg_scale, + i2i_strength, + configs, + model_cache, + prompt_cache, + app, + image, + } = args; + + println!("Starting image generation with the following configuration:"); + println!(" Model: {:?}", configs.sd_version); + println!(" Prompt: {}", prompt); + println!(" Steps: {}", configs.scheduler_steps); + println!(" Device: {:?}", configs.device); + + println!("Model dimensions: {}x{}", configs.sd_config.width, configs.sd_config.height); + + // Use LCM Scheduler instead of Euler Ancestral for better speed and quality + let mut scheduler = LCMScheduler::new( + configs.scheduler_steps, + candle_transformers::models::stable_diffusion::lcm::LCMSchedulerConfig::default(), + )?; + + let seed = configs.seed.unwrap_or_else(|| rand::thread_rng().gen()); + configs.device.set_seed(seed)?; + + info!("Using seed: {}", seed); + + let guidance_scale = match cfg_scale { + Some(guidance_scale) => guidance_scale, + None => match configs.sd_version { + StableDiffusionVersion::V1_5 + | StableDiffusionVersion::V2_1 + | StableDiffusionVersion::Xl => 7.5, + StableDiffusionVersion::Turbo => 0., + _ => 0., // NB: Not sure what the other model families should use, so sticking with "0" + }, + }; + + let use_guide_scale = guidance_scale > 1.0; + + info!("Using guide scale: {}", use_guide_scale); + + info!("Checking if prompt is cached"); + let maybe_cached = prompt_cache.get_copy(&prompt)?; + + let mut text_embeddings = if let Some(tensor) = maybe_cached { + tensor + } else { + + info!("Prompt is NOT cached! Calculating embedding..."); + let tensor = infer_clip_text_embeddings( + &prompt, + &uncond_prompt, + None, // tokenizer + None, // clip_weights + None, // clip2_weights + configs.sd_version, + &configs.sd_config, + false, // use_f16 + &configs.device, + configs.dtype, + use_guide_scale, + )?; + prompt_cache.store_copy(&prompt, &tensor)?; + tensor + }; + + println!("Text embeddings shape: {:?}", text_embeddings.shape()); + + println!("Loading input image into tensor..."); + + let input_image = dynamic_image_to_tensor( + image, + &configs.device, + configs.dtype)?; + + println!("Reference image shape: {:?}", input_image.shape()); + + let vae_scale = get_vae_scale(configs.sd_version); + + let maybe_vae = model_cache.get_vae()?; + + let vae = match maybe_vae { + Some(vae) => vae, + None => { + info!("No vae found in cache; loading..."); + + let repo = configs.sd_version.repo(); + + println!("Building VAE model from : {:?} ... (3)", repo); + + let vae_file = configs.hf_api.model(repo.to_string()) + .get("vae/diffusion_pytorch_model.safetensors")?; + + println!("Building VAE model from file {:?}...", &vae_file); + + let mut notify_download_complete = false; + //if !vae_file.exists() { + if true { + notify_download_complete = true; + app.emit("notification", NotificationEvent::ModelDownloadStarted { + model_name: repo, + model_type: ModelType::Vae, + })?; + } + + let vae = configs + .sd_config + .build_vae(vae_file, &configs.device, configs.dtype)?; + + let vae = Arc::new(vae); + + model_cache.set_vae(vae.clone())?; + + if notify_download_complete { + app.emit("notification", NotificationEvent::ModelDownloadComplete { + model_name: repo, + model_type: ModelType::Vae, + })?; + } + + vae + } + }; + + let maybe_unet = model_cache.get_unet()?; + + let unet = match maybe_unet { + Some(unet) => unet, + None => { + info!("No unet found in cache; loading..."); + + let mut notify_download_complete = false; + //if !unet_file.exists() { + if true { + notify_download_complete = true; + app.emit("notification", NotificationEvent::ModelDownloadStarted { + model_name: "sdxl-turbo-unet", + model_type: ModelType::Unet, + })?; + } + + let unet_file = ModelRegistry::SdxlTurboUnet.get_filename(); + + let unet = UNetModel::new(&configs.sd_config, unet_file, &configs.device, configs.dtype) + .map_err(|err| anyhow!("error initializing unet model: {:?}", err))?; + + let unet = Arc::new(unet); + + model_cache.set_unet(unet.clone())?; + + if notify_download_complete { + app.emit("notification", NotificationEvent::ModelDownloadComplete { + model_name: "sdxl-turbo-unet", + model_type: ModelType::Unet, + })?; + } + + unet + } + }; + + let init_latent_dist : DiagonalGaussianDistribution = vae.encode(&input_image)?; + + // Parse and calculate the img2img strength + let img2img_strength = match i2i_strength { + None => 0.75f64, + Some(strength) => (strength as f64) / 100.0f64, + }; + + // Create LCM guidance scale embeddings + let embedding_dim = 256; + let guidance_scale_embedding = scheduler.get_guidance_scale_embedding( + guidance_scale, + embedding_dim, + &configs.device, + configs.dtype + )?; + + // Generate latents from input image using the LCM approach + println!("Generating latents from input image..."); + let init_latents = (init_latent_dist.sample()? * vae_scale)?; + println!("Initial latents shape: {:?}", init_latents.shape()); + + // Calculate timesteps for LCM with proper img2img handling + let timesteps = LCMScheduler::get_timesteps_for_steps(configs.scheduler_steps, img2img_strength); + let t_start = timesteps[0]; // First denoising step + + // Add noise at the appropriate timestep for img2img + println!("Adding noise to latents for t_start={}", t_start); + let noise = init_latents.randn_like(0f64, 1f64)?; + let latents = scheduler.add_noise(&init_latents, noise, t_start)?; + + println!("Latents initialized successfully"); + println!("Initial noise shape: {:?}", latents.shape()); + + println!("Starting diffusion process..."); + + let mut latents = latents; + let timesteps: Vec<_> = scheduler.timesteps().iter().copied().collect(); + + for (timestep_index, ×tep) in timesteps.iter().enumerate() { + println!("Processing step {}/{} @ timestamp = {}", timestep_index + 1, timesteps.len(), timestep); + + let latent_model_input = if use_guide_scale { + Tensor::cat(&[&latents, &latents], 0)? + } else { + latents.clone() + }; + + let latent_model_input = scheduler.scale_model_input(latent_model_input, timestep); + + // Use the guidance scale embedding in the UNet inference with the proper method name + let mut noise_pred = match unet.forward_with_guidance( + &latent_model_input, + timestep as f64, + &text_embeddings, + &guidance_scale_embedding) { + Ok(pred) => pred, + Err(e) => { + println!("UNet inference failed with error: {}", e); + return Err(anyhow::anyhow!("UNet inference failed: {}", e)); + }, + }; + + if use_guide_scale { + let chunks = noise_pred.chunk(2, 0)?; + let (noise_pred_uncond, noise_pred_text) = (&chunks[0], &chunks[1]); + noise_pred = (noise_pred_uncond + (guidance_scale * (noise_pred_text - noise_pred_uncond)?)?)?; + } + + // Apply the LCM scheduler step + latents = scheduler.step(&noise_pred, timestep, &latents, timestep_index, configs.scheduler_steps)?; + } + + println!("Diffusion process completed, decoding image..."); + let image = vae.decode(&(latents / vae_scale)?)?; + + println!("VAE decode completed"); + + println!("Scaling image back"); + let image = ((image / 2.)? + 0.5)?; + + println!("Normalized image values"); + let image = (image.clamp(0f32, 1.)? * 255.)?; + + println!("Convert to int8"); + let image = image.to_dtype(DType::U8)?; + + println!("Converted to 8-bit format"); + + let image = tensor_to_image_buffer(&image.i(0)?)?; + + Ok(image) +} diff --git a/crates/desktop/src-tauri/src/ml/stable_diffusion/mod.rs b/crates/desktop/src-tauri/src/ml/stable_diffusion/mod.rs index d07f90d6e6..67e6759bd0 100644 --- a/crates/desktop/src-tauri/src/ml/stable_diffusion/mod.rs +++ b/crates/desktop/src-tauri/src/ml/stable_diffusion/mod.rs @@ -1,3 +1,4 @@ pub mod get_vae_scale; pub mod infer_clip_text_embeddings; -pub mod stable_diffusion_pipeline; \ No newline at end of file +pub mod stable_diffusion_pipeline; +pub mod lcm_pipeline; \ No newline at end of file diff --git a/crates/desktop/src-tauri/src/state/app_config.rs b/crates/desktop/src-tauri/src/state/app_config.rs index 0d22f1c5fd..27a9d65f95 100644 --- a/crates/desktop/src-tauri/src/state/app_config.rs +++ b/crates/desktop/src-tauri/src/state/app_config.rs @@ -71,7 +71,7 @@ impl AppConfig { sd_config, image_height: yaml_configs.image_height.unwrap_or(DEFAULT_SD_IMAGE_HEIGHT), image_width: yaml_configs.image_width.unwrap_or(DEFAULT_SD_IMAGE_WIDTH), - scheduler_steps: yaml_configs.scheduler_steps.unwrap_or(1), + scheduler_steps: yaml_configs.scheduler_steps.unwrap_or(4), scheduler_samples: yaml_configs.scheduler_samples.unwrap_or(15), seed: yaml_configs.seed, cfg_scale: yaml_configs.cfg_scale, diff --git a/crates/vendor/candle/candle-examples/examples/lcm/main.rs b/crates/vendor/candle/candle-examples/examples/lcm/main.rs index 7319f7d584..746198475f 100644 --- a/crates/vendor/candle/candle-examples/examples/lcm/main.rs +++ b/crates/vendor/candle/candle-examples/examples/lcm/main.rs @@ -5,6 +5,7 @@ extern crate accelerate_src; extern crate intel_mkl_src; use candle_transformers::models::stable_diffusion; +use candle_transformers::models::stable_diffusion::lcm::LCMScheduler; use std::ops::Div; use anyhow::{Error as E, Result}; @@ -18,855 +19,1048 @@ use candle_transformers::models::stable_diffusion::unet_2d::UNet2DConditionModel use candle_transformers::models::stable_diffusion::unet_2d::UNet2DConditionModelConfig; use candle_transformers::models::stable_diffusion::unet_2d::BlockConfig; -const PRINT_DEBUG_TENSORS: bool = false; - #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { - /// The prompt to be used for image generation. - #[arg(long, default_value = "A very realistic photo of a rusty robot walking on a sandy beach")] - prompt: String, + /// The prompt to be used for image generation. + #[arg( + long, + default_value = "A very realistic photo of a rusty robot walking on a sandy beach" + )] + prompt: String, - #[arg(long, default_value = "")] - uncond_prompt: String, + #[arg(long, default_value = "")] + uncond_prompt: String, - /// Run on CPU rather than on GPU. - #[arg(long)] - cpu: bool, + /// Run on CPU rather than on GPU. + #[arg(long)] + cpu: bool, - /// Enable tracing (generates a trace-timestamp.json file). - #[arg(long)] - tracing: bool, + /// Enable tracing (generates a trace-timestamp.json file). + #[arg(long)] + tracing: bool, - /// The height in pixels of the generated image. - #[arg(long)] - height: Option, + /// The height in pixels of the generated image. + #[arg(long)] + height: Option, - /// The width in pixels of the generated image. - #[arg(long)] - width: Option, + /// The width in pixels of the generated image. + #[arg(long)] + width: Option, - /// The UNet weight file, in .safetensors format. - #[arg(long, value_name = "FILE")] - unet_weights: Option, + /// The UNet weight file, in .safetensors format. + #[arg(long, value_name = "FILE")] + unet_weights: Option, - /// The CLIP weight file, in .safetensors format. - #[arg(long, value_name = "FILE")] - clip_weights: Option, + /// The CLIP weight file, in .safetensors format. + #[arg(long, value_name = "FILE")] + clip_weights: Option, - /// The CLIP2 weight file, in .safetensors format. - #[arg(long, value_name = "FILE")] - clip2_weights: Option, + /// The CLIP2 weight file, in .safetensors format. + #[arg(long, value_name = "FILE")] + clip2_weights: Option, - /// The VAE weight file, in .safetensors format. - #[arg(long, value_name = "FILE")] - vae_weights: Option, + /// The VAE weight file, in .safetensors format. + #[arg(long, value_name = "FILE")] + vae_weights: Option, - #[arg(long, value_name = "FILE")] - /// The file specifying the tokenizer to used for tokenization. - tokenizer: Option, + #[arg(long, value_name = "FILE")] + /// The file specifying the tokenizer to used for tokenization. + tokenizer: Option, - /// The size of the sliced attention or 0 for automatic slicing (disabled by default) - #[arg(long)] - sliced_attention_size: Option, + /// The size of the sliced attention or 0 for automatic slicing (disabled by default) + #[arg(long)] + sliced_attention_size: Option, - /// The number of inference steps to run. - #[arg(long, default_value_t = 4)] - n_steps: usize, + /// The number of inference steps to run. + #[arg(long, default_value_t = 4)] + n_steps: usize, - /// The number of samples to generate iteratively. - #[arg(long, default_value_t = 1)] - num_samples: usize, + /// The number of samples to generate iteratively. + #[arg(long, default_value_t = 1)] + num_samples: usize, - /// The numbers of samples to generate simultaneously. - #[arg[long, default_value_t = 1]] - bsize: usize, + /// The numbers of samples to generate simultaneously. + #[arg[long, default_value_t = 1]] + bsize: usize, - /// The name of the final image to generate. - #[arg(long, value_name = "FILE", default_value = "sd_final.png")] - final_image: String, + /// The name of the final image to generate. + #[arg(long, value_name = "FILE", default_value = "sd_final.png")] + final_image: String, - #[arg(long, value_enum, default_value = "v2-1")] - sd_version: StableDiffusionVersion, + #[arg(long, value_enum, default_value = "v2-1")] + sd_version: StableDiffusionVersion, - /// Generate intermediary images at each step. - #[arg(long, action)] - intermediary_images: bool, + /// Generate intermediary images at each step. + #[arg(long, action)] + intermediary_images: bool, - #[arg(long)] - use_flash_attn: bool, + #[arg(long)] + use_flash_attn: bool, - #[arg(long)] - use_f16: bool, + #[arg(long)] + use_f16: bool, - /// The guidance scale for classifier-free guidance. - #[arg(long, default_value_t = 2.0)] - guidance_scale: f64, + /// The guidance scale for classifier-free guidance. + #[arg(long, default_value_t = 7.5)] + guidance_scale: f64, - /// Path to the mask image for inpainting. - #[arg(long, value_name = "FILE")] - mask_path: Option, + /// Path to the mask image for inpainting. + #[arg(long, value_name = "FILE")] + mask_path: Option, - /// Path to the image used to initialize the latents. For inpainting, this is the image to be masked. - #[arg(long, value_name = "FILE")] - img2img: Option, + /// Path to the image used to initialize the latents. For inpainting, this is the image to be masked. + #[arg(long, value_name = "FILE")] + img2img: Option, - /// The strength parameter for img2img, between 0 and 1. - #[arg(long, default_value_t = 0.7)] - strength: f64, + /// The strength parameter for img2img, between 0 and 1. + #[arg(long, default_value_t = 0.7)] + strength: f64, - /// The seed to use when generating random samples. - #[arg(long)] - seed: Option, + /// The seed to use when generating random samples. + #[arg(long)] + seed: Option, - /// Force the saved image to update only the masked region - #[arg(long)] - only_update_masked: bool, - - #[arg(long)] - save_tensors: bool, - - /// Use half precision (F16) rather than F32 - #[arg(long)] - half_precision: bool, + /// Force the saved image to update only the masked region + #[arg(long)] + only_update_masked: bool, } #[derive(Debug, Clone, Copy, clap::ValueEnum, PartialEq, Eq)] enum StableDiffusionVersion { - V1_5, - V1_5Inpaint, - V2_1, - V2Inpaint, - Xl, - XlInpaint, - Turbo, + V1_5, + V1_5Inpaint, + V2_1, + V2Inpaint, + Xl, + XlInpaint, + Turbo, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ModelFile { - Tokenizer, - Tokenizer2, - Clip, - Clip2, - Unet, - UnetLcm, - Vae, + Tokenizer, + Tokenizer2, + Clip, + Clip2, + Unet, + UnetLcm, + Vae, } impl StableDiffusionVersion { - fn repo(&self) -> &'static str { - match self { - Self::XlInpaint => "diffusers/stable-diffusion-xl-1.0-inpainting-0.1", - Self::Xl => "stabilityai/stable-diffusion-xl-base-1.0", - Self::V2Inpaint => "stabilityai/stable-diffusion-2-inpainting", - Self::V2_1 => "stabilityai/stable-diffusion-2-1", - Self::V1_5 => "runwayml/stable-diffusion-v1-5", - Self::V1_5Inpaint => "stable-diffusion-v1-5/stable-diffusion-inpainting", - Self::Turbo => "stabilityai/sdxl-turbo", - } - } - - fn unet_file(&self, use_f16: bool) -> &'static str { - match self { - Self::V1_5 | Self::V1_5Inpaint | Self::V2_1 | Self::V2Inpaint | Self::Xl | Self::XlInpaint | Self::Turbo => { - if use_f16 { - "unet/diffusion_pytorch_model.fp16.safetensors" - } else { - "unet/diffusion_pytorch_model.safetensors" + fn repo(&self) -> &'static str { + match self { + Self::XlInpaint => "diffusers/stable-diffusion-xl-1.0-inpainting-0.1", + Self::Xl => "stabilityai/stable-diffusion-xl-base-1.0", + Self::V2Inpaint => "stabilityai/stable-diffusion-2-inpainting", + Self::V2_1 => "stabilityai/stable-diffusion-2-1", + Self::V1_5 => "runwayml/stable-diffusion-v1-5", + Self::V1_5Inpaint => "stable-diffusion-v1-5/stable-diffusion-inpainting", + Self::Turbo => "stabilityai/sdxl-turbo", } - }, } - } - fn vae_file(&self, use_f16: bool) -> &'static str { - match self { - Self::V1_5 | Self::V1_5Inpaint | Self::V2_1 | Self::V2Inpaint | Self::Xl | Self::XlInpaint | Self::Turbo => { - if use_f16 { - "vae/diffusion_pytorch_model.fp16.safetensors" - } else { - "vae/diffusion_pytorch_model.safetensors" + fn unet_file(&self, use_f16: bool) -> &'static str { + match self { + Self::V1_5 + | Self::V1_5Inpaint + | Self::V2_1 + | Self::V2Inpaint + | Self::Xl + | Self::XlInpaint + | Self::Turbo => { + if use_f16 { + "unet/diffusion_pytorch_model.fp16.safetensors" + } else { + "unet/diffusion_pytorch_model.safetensors" + } + } } - }, } - } - fn clip_file(&self, use_f16: bool) -> &'static str { - match self { - Self::V1_5 | Self::V1_5Inpaint | Self::V2_1 | Self::V2Inpaint | Self::Xl | Self::XlInpaint | Self::Turbo => { - if use_f16 { - "text_encoder/model.fp16.safetensors" - } else { - "text_encoder/model.safetensors" + fn vae_file(&self, use_f16: bool) -> &'static str { + match self { + Self::V1_5 + | Self::V1_5Inpaint + | Self::V2_1 + | Self::V2Inpaint + | Self::Xl + | Self::XlInpaint + | Self::Turbo => { + if use_f16 { + "vae/diffusion_pytorch_model.fp16.safetensors" + } else { + "vae/diffusion_pytorch_model.safetensors" + } + } } - }, } - } - fn clip2_file(&self, use_f16: bool) -> &'static str { - match self { - Self::V1_5 | Self::V1_5Inpaint | Self::V2_1 | Self::V2Inpaint | Self::Xl | Self::XlInpaint | Self::Turbo => { - if use_f16 { - "text_encoder_2/model.fp16.safetensors" - } else { - "text_encoder_2/model.safetensors" + fn clip_file(&self, use_f16: bool) -> &'static str { + match self { + Self::V1_5 + | Self::V1_5Inpaint + | Self::V2_1 + | Self::V2Inpaint + | Self::Xl + | Self::XlInpaint + | Self::Turbo => { + if use_f16 { + "text_encoder/model.fp16.safetensors" + } else { + "text_encoder/model.safetensors" + } + } + } + } + + fn clip2_file(&self, use_f16: bool) -> &'static str { + match self { + Self::V1_5 + | Self::V1_5Inpaint + | Self::V2_1 + | Self::V2Inpaint + | Self::Xl + | Self::XlInpaint + | Self::Turbo => { + if use_f16 { + "text_encoder_2/model.fp16.safetensors" + } else { + "text_encoder_2/model.safetensors" + } + } } - }, } - } } impl ModelFile { - fn get(&self, filename: Option, version: StableDiffusionVersion, use_f16: bool) -> Result { - use hf_hub::api::sync::Api; - match filename { - Some(filename) => Ok(std::path::PathBuf::from(filename)), - None => { - let (repo, path) = match self { - Self::Tokenizer => { - let tokenizer_repo = match version { - StableDiffusionVersion::V1_5 | StableDiffusionVersion::V2_1 | StableDiffusionVersion::V1_5Inpaint | StableDiffusionVersion::V2Inpaint => "openai/clip-vit-base-patch32", - StableDiffusionVersion::Xl | StableDiffusionVersion::XlInpaint | StableDiffusionVersion::Turbo => { - // This seems similar to the patch32 version except some very small - // difference in the split regex. - "openai/clip-vit-large-patch14" - }, - }; - (tokenizer_repo, "tokenizer.json") - }, - Self::Tokenizer2 => ("laion/CLIP-ViT-bigG-14-laion2B-39B-b160k", "tokenizer.json"), - Self::Clip => (version.repo(), version.clip_file(use_f16)), - Self::Clip2 => (version.repo(), version.clip2_file(use_f16)), - Self::UnetLcm => ("SimianLuo/LCM_Dreamshaper_v7", if use_f16 { "unet/diffusion_pytorch_model.fp16.safetensors" } else { "unet/diffusion_pytorch_model.safetensors" }), - Self::Unet => (version.repo(), version.unet_file(use_f16)), - Self::Vae => { - // Override for SDXL when using f16 weights. - // See https://github.com/huggingface/candle/issues/1060 - if matches!(version, StableDiffusionVersion::Xl | StableDiffusionVersion::Turbo,) && use_f16 { - ("madebyollin/sdxl-vae-fp16-fix", "diffusion_pytorch_model.safetensors") - } else { - (version.repo(), version.vae_file(use_f16)) + fn get( + &self, + filename: Option, + version: StableDiffusionVersion, + use_f16: bool, + ) -> Result { + use hf_hub::api::sync::Api; + match filename { + Some(filename) => Ok(std::path::PathBuf::from(filename)), + None => { + let (repo, path) = match self { + Self::Tokenizer => { + let tokenizer_repo = match version { + StableDiffusionVersion::V1_5 + | StableDiffusionVersion::V2_1 + | StableDiffusionVersion::V1_5Inpaint + | StableDiffusionVersion::V2Inpaint => "openai/clip-vit-base-patch32", + StableDiffusionVersion::Xl + | StableDiffusionVersion::XlInpaint + | StableDiffusionVersion::Turbo => { + // This seems similar to the patch32 version except some very small + // difference in the split regex. + "openai/clip-vit-large-patch14" + } + }; + (tokenizer_repo, "tokenizer.json") + } + Self::Tokenizer2 => { + ("laion/CLIP-ViT-bigG-14-laion2B-39B-b160k", "tokenizer.json") + } + Self::Clip => (version.repo(), version.clip_file(use_f16)), + Self::Clip2 => (version.repo(), version.clip2_file(use_f16)), + Self::UnetLcm => ( + "SimianLuo/LCM_Dreamshaper_v7", + if use_f16 { + "unet/diffusion_pytorch_model.fp16.safetensors" + } else { + "unet/diffusion_pytorch_model.safetensors" + }, + ), + Self::Unet => (version.repo(), version.unet_file(use_f16)), + Self::Vae => { + // Override for SDXL when using f16 weights. + // See https://github.com/huggingface/candle/issues/1060 + if matches!( + version, + StableDiffusionVersion::Xl | StableDiffusionVersion::Turbo, + ) && use_f16 + { + ( + "madebyollin/sdxl-vae-fp16-fix", + "diffusion_pytorch_model.safetensors", + ) + } else { + (version.repo(), version.vae_file(use_f16)) + } + } + }; + let filename = Api::new()?.model(repo.to_string()).get(path)?; + Ok(filename) } - }, - }; - let filename = Api::new()?.model(repo.to_string()).get(path)?; - Ok(filename) - }, + } } - } } -fn output_filename(basename: &str, sample_idx: usize, num_samples: usize, timestep_idx: Option) -> String { - let filename = if num_samples > 1 { - match basename.rsplit_once('.') { - None => format!("{basename}.{sample_idx}.png"), - Some((filename_no_extension, extension)) => { - format!("{filename_no_extension}.{sample_idx}.{extension}") - }, +fn output_filename( + basename: &str, + sample_idx: usize, + num_samples: usize, + timestep_idx: Option, +) -> String { + let filename = if num_samples > 1 { + match basename.rsplit_once('.') { + None => format!("{basename}.{sample_idx}.png"), + Some((filename_no_extension, extension)) => { + format!("{filename_no_extension}.{sample_idx}.{extension}") + } + } + } else { + basename.to_string() + }; + match timestep_idx { + None => filename, + Some(timestep_idx) => match filename.rsplit_once('.') { + None => format!("{filename}-{timestep_idx}.png"), + Some((filename_no_extension, extension)) => { + format!("{filename_no_extension}-{timestep_idx}.{extension}") + } + }, } - } else { - basename.to_string() - }; - match timestep_idx { - None => filename, - Some(timestep_idx) => match filename.rsplit_once('.') { - None => format!("{filename}-{timestep_idx}.png"), - Some((filename_no_extension, extension)) => { - format!("{filename_no_extension}-{timestep_idx}.{extension}") - }, - }, - } } #[allow(clippy::too_many_arguments)] -fn save_image(vae: &AutoEncoderKL, latents: &Tensor, vae_scale: f64, bsize: usize, idx: usize, final_image: &str, num_samples: usize, timestep_ids: Option) -> Result<()> { - let images = vae.decode(&(latents / vae_scale)?)?; - let images = ((images / 2.)? + 0.5)?.to_device(&Device::Cpu)?; - let images = (images.clamp(0f32, 1.)? * 255.)?.to_dtype(DType::U8)?; - for batch in 0..bsize { - let image = images.i(batch)?; - let image_filename = output_filename(final_image, (bsize * idx) + batch + 1, batch + num_samples, timestep_ids); - candle_examples::save_image(&image, image_filename)?; - } - Ok(()) +fn save_image( + vae: &AutoEncoderKL, + latents: &Tensor, + vae_scale: f64, + bsize: usize, + idx: usize, + final_image: &str, + num_samples: usize, + timestep_ids: Option, +) -> Result<()> { + let images = vae.decode(&(latents / vae_scale)?)?; + let images = ((images / 2.)? + 0.5)?.to_device(&Device::Cpu)?; + let images = (images.clamp(0f32, 1.)? * 255.)?.to_dtype(DType::U8)?; + for batch in 0..bsize { + let image = images.i(batch)?; + let image_filename = output_filename( + final_image, + (bsize * idx) + batch + 1, + batch + num_samples, + timestep_ids, + ); + candle_examples::save_image(&image, image_filename)?; + } + Ok(()) } #[allow(clippy::too_many_arguments)] -fn text_embeddings(prompt: &str, uncond_prompt: &str, tokenizer: Option, clip_weights: Option, clip2_weights: Option, sd_version: StableDiffusionVersion, sd_config: &stable_diffusion::StableDiffusionConfig, use_f16: bool, device: &Device, dtype: DType, use_guide_scale: bool, first: bool) -> Result { - let tokenizer_file = if first { ModelFile::Tokenizer } else { ModelFile::Tokenizer2 }; - let tokenizer = tokenizer_file.get(tokenizer, sd_version, use_f16)?; - let tokenizer = Tokenizer::from_file(tokenizer).map_err(E::msg)?; - let pad_id = match &sd_config.clip.pad_with { - Some(padding) => *tokenizer.get_vocab(true).get(padding.as_str()).unwrap(), - None => *tokenizer.get_vocab(true).get("<|endoftext|>").unwrap(), - }; - println!("Running with prompt \"{prompt}\"."); - let mut tokens = tokenizer.encode(prompt, true).map_err(E::msg)?.get_ids().to_vec(); - if tokens.len() > sd_config.clip.max_position_embeddings { - anyhow::bail!("the prompt is too long, {} > max-tokens ({})", tokens.len(), sd_config.clip.max_position_embeddings) - } - while tokens.len() < sd_config.clip.max_position_embeddings { - tokens.push(pad_id) - } - let tokens = Tensor::new(tokens.as_slice(), device)?.unsqueeze(0)?; - - println!("Building the Clip transformer."); - let clip_weights_file = if first { ModelFile::Clip } else { ModelFile::Clip2 }; - let clip_weights = if first { clip_weights_file.get(clip_weights, sd_version, use_f16)? } else { clip_weights_file.get(clip2_weights, sd_version, use_f16)? }; - let clip_config = if first { &sd_config.clip } else { sd_config.clip2.as_ref().unwrap() }; - let text_model = stable_diffusion::build_clip_transformer(clip_config, clip_weights, device, DType::F32)?; - let text_embeddings = text_model.forward(&tokens)?; - - let text_embeddings = if use_guide_scale { - let mut uncond_tokens = tokenizer.encode(uncond_prompt, true).map_err(E::msg)?.get_ids().to_vec(); - if uncond_tokens.len() > sd_config.clip.max_position_embeddings { - anyhow::bail!("the negative prompt is too long, {} > max-tokens ({})", uncond_tokens.len(), sd_config.clip.max_position_embeddings) +fn text_embeddings( + prompt: &str, + uncond_prompt: &str, + tokenizer: Option, + clip_weights: Option, + clip2_weights: Option, + sd_version: StableDiffusionVersion, + sd_config: &stable_diffusion::StableDiffusionConfig, + use_f16: bool, + device: &Device, + dtype: DType, + use_guide_scale: bool, + first: bool, +) -> Result { + let tokenizer_file = if first { + ModelFile::Tokenizer + } else { + ModelFile::Tokenizer2 + }; + let tokenizer = tokenizer_file.get(tokenizer, sd_version, use_f16)?; + let tokenizer = Tokenizer::from_file(tokenizer).map_err(E::msg)?; + let pad_id = match &sd_config.clip.pad_with { + Some(padding) => *tokenizer.get_vocab(true).get(padding.as_str()).unwrap(), + None => *tokenizer.get_vocab(true).get("<|endoftext|>").unwrap(), + }; + println!("Running with prompt \"{prompt}\"."); + let mut tokens = tokenizer + .encode(prompt, true) + .map_err(E::msg)? + .get_ids() + .to_vec(); + if tokens.len() > sd_config.clip.max_position_embeddings { + anyhow::bail!( + "the prompt is too long, {} > max-tokens ({})", + tokens.len(), + sd_config.clip.max_position_embeddings + ) } - while uncond_tokens.len() < sd_config.clip.max_position_embeddings { - uncond_tokens.push(pad_id) + while tokens.len() < sd_config.clip.max_position_embeddings { + tokens.push(pad_id) } + let tokens = Tensor::new(tokens.as_slice(), device)?.unsqueeze(0)?; - let uncond_tokens = Tensor::new(uncond_tokens.as_slice(), device)?.unsqueeze(0)?; - let uncond_embeddings = text_model.forward(&uncond_tokens)?; + println!("Building the Clip transformer."); + let clip_weights_file = if first { + ModelFile::Clip + } else { + ModelFile::Clip2 + }; + let clip_weights = if first { + clip_weights_file.get(clip_weights, sd_version, use_f16)? + } else { + clip_weights_file.get(clip2_weights, sd_version, use_f16)? + }; + let clip_config = if first { + &sd_config.clip + } else { + sd_config.clip2.as_ref().unwrap() + }; + let text_model = + stable_diffusion::build_clip_transformer(clip_config, clip_weights, device, DType::F32)?; + let text_embeddings = text_model.forward(&tokens)?; - Tensor::cat(&[uncond_embeddings, text_embeddings], 0)?.to_dtype(dtype)? - } else { - text_embeddings.to_dtype(dtype)? - }; - Ok(text_embeddings) + let text_embeddings = if use_guide_scale { + let mut uncond_tokens = tokenizer + .encode(uncond_prompt, true) + .map_err(E::msg)? + .get_ids() + .to_vec(); + if uncond_tokens.len() > sd_config.clip.max_position_embeddings { + anyhow::bail!( + "the negative prompt is too long, {} > max-tokens ({})", + uncond_tokens.len(), + sd_config.clip.max_position_embeddings + ) + } + while uncond_tokens.len() < sd_config.clip.max_position_embeddings { + uncond_tokens.push(pad_id) + } + + let uncond_tokens = Tensor::new(uncond_tokens.as_slice(), device)?.unsqueeze(0)?; + let uncond_embeddings = text_model.forward(&uncond_tokens)?; + + Tensor::cat(&[uncond_embeddings, text_embeddings], 0)?.to_dtype(dtype)? + } else { + text_embeddings.to_dtype(dtype)? + }; + Ok(text_embeddings) } fn image_preprocess>(path: T) -> anyhow::Result { - let img = image::ImageReader::open(path)?.decode()?; - let (height, width) = (img.height() as usize, img.width() as usize); - let height = height - height % 32; - let width = width - width % 32; - let img = img.resize_to_fill(width as u32, height as u32, image::imageops::FilterType::CatmullRom); - let img = img.to_rgb8(); - let img = img.into_raw(); - let img = Tensor::from_vec(img, (height, width, 3), &Device::Cpu)?.permute((2, 0, 1))?.to_dtype(DType::F32)?.affine(2. / 255., -1.)?.unsqueeze(0)?; - Ok(img) + let img = image::ImageReader::open(path)?.decode()?; + let (height, width) = (img.height() as usize, img.width() as usize); + let height = height - height % 32; + let width = width - width % 32; + let img = img.resize_to_fill( + width as u32, + height as u32, + image::imageops::FilterType::CatmullRom, + ); + let img = img.to_rgb8(); + let img = img.into_raw(); + let img = Tensor::from_vec(img, (height, width, 3), &Device::Cpu)? + .permute((2, 0, 1))? + .to_dtype(DType::F32)? + .affine(2. / 255., -1.)? + .unsqueeze(0)?; + Ok(img) } /// Convert the mask image to a single channel tensor. Also ensure the image is a multiple of 32 in both dimensions. fn mask_preprocess>(path: T) -> anyhow::Result { - let img = image::open(path)?.to_luma8(); - let (new_width, new_height) = { - let (width, height) = img.dimensions(); - (width - width % 32, height - height % 32) - }; - let img = image::imageops::resize(&img, new_width, new_height, image::imageops::FilterType::CatmullRom).into_raw(); - let mask = Tensor::from_vec(img, (new_height as usize, new_width as usize), &Device::Cpu)?.unsqueeze(0)?.to_dtype(DType::F32)?.div(255.0)?.unsqueeze(0)?; - Ok(mask) + let img = image::open(path)?.to_luma8(); + let (new_width, new_height) = { + let (width, height) = img.dimensions(); + (width - width % 32, height - height % 32) + }; + let img = image::imageops::resize( + &img, + new_width, + new_height, + image::imageops::FilterType::CatmullRom, + ) + .into_raw(); + let mask = Tensor::from_vec(img, (new_height as usize, new_width as usize), &Device::Cpu)? + .unsqueeze(0)? + .to_dtype(DType::F32)? + .div(255.0)? + .unsqueeze(0)?; + Ok(mask) } /// Generates the mask latents, scaled mask and mask_4 for inpainting. Returns a tuple of None if inpainting is not /// being used. #[allow(clippy::too_many_arguments)] -fn inpainting_tensors(sd_version: StableDiffusionVersion, mask_path: Option, dtype: DType, device: &Device, use_guide_scale: bool, vae: &AutoEncoderKL, image: Option, vae_scale: f64) -> Result<(Option, Option, Option)> { - match sd_version { - StableDiffusionVersion::XlInpaint | StableDiffusionVersion::V2Inpaint | StableDiffusionVersion::V1_5Inpaint => { - let inpaint_mask = mask_path.ok_or_else(|| anyhow::anyhow!("An inpainting model was requested but mask-path is not provided."))?; - // Get the mask image with shape [1, 1, 128, 128] - let mask = mask_preprocess(inpaint_mask)?.to_device(device)?.to_dtype(dtype)?; - // Generate the masked image from the image and the mask with shape [1, 3, 1024, 1024] - let xmask = mask.le(0.5)?.repeat(&[1, 3, 1, 1])?.to_dtype(dtype)?; - let image = &image.ok_or_else(|| anyhow::anyhow!("An inpainting model was requested but img2img which is used as the input image is not provided."))?; - let masked_img = (image * xmask)?; - // Scale down the mask - let shape = masked_img.shape(); - let (w, h) = (shape.dims()[3] / 8, shape.dims()[2] / 8); - let mask = mask.interpolate2d(w, h)?; - // shape: [1, 4, 128, 128] - let mask_latents = vae.encode(&masked_img)?; - let mask_latents = (mask_latents.sample()? * vae_scale)?.to_device(device)?; +fn inpainting_tensors( + sd_version: StableDiffusionVersion, + mask_path: Option, + dtype: DType, + device: &Device, + use_guide_scale: bool, + vae: &AutoEncoderKL, + image: Option, + vae_scale: f64, +) -> Result<(Option, Option, Option)> { + match sd_version { + StableDiffusionVersion::XlInpaint + | StableDiffusionVersion::V2Inpaint + | StableDiffusionVersion::V1_5Inpaint => { + let inpaint_mask = mask_path.ok_or_else(|| { + anyhow::anyhow!("An inpainting model was requested but mask-path is not provided.") + })?; + // Get the mask image with shape [1, 1, 128, 128] + let mask = mask_preprocess(inpaint_mask)? + .to_device(device)? + .to_dtype(dtype)?; + // Generate the masked image from the image and the mask with shape [1, 3, 1024, 1024] + let xmask = mask.le(0.5)?.repeat(&[1, 3, 1, 1])?.to_dtype(dtype)?; + let image = &image + .ok_or_else(|| anyhow::anyhow!( + "An inpainting model was requested but img2img which is used as the input image is not provided." + ))?; + let masked_img = (image * xmask)?; + // Scale down the mask + let shape = masked_img.shape(); + let (w, h) = (shape.dims()[3] / 8, shape.dims()[2] / 8); + let mask = mask.interpolate2d(w, h)?; + // shape: [1, 4, 128, 128] + let mask_latents = vae.encode(&masked_img)?; + let mask_latents = (mask_latents.sample()? * vae_scale)?.to_device(device)?; - let mask_4 = mask.as_ref().repeat(&[1, 4, 1, 1])?; - let (mask_latents, mask) = if use_guide_scale { (Tensor::cat(&[&mask_latents, &mask_latents], 0)?, Tensor::cat(&[&mask, &mask], 0)?) } else { (mask_latents, mask) }; - Ok((Some(mask_latents), Some(mask), Some(mask_4))) - }, - _ => Ok((None, None, None)), - } + let mask_4 = mask.as_ref().repeat(&[1, 4, 1, 1])?; + let (mask_latents, mask) = if use_guide_scale { + ( + Tensor::cat(&[&mask_latents, &mask_latents], 0)?, + Tensor::cat(&[&mask, &mask], 0)?, + ) + } else { + (mask_latents, mask) + }; + Ok((Some(mask_latents), Some(mask), Some(mask_4))) + } + _ => Ok((None, None, None)), + } } fn get_tensor_stats(tensor: &Tensor) -> Result<(f32, f32, f32, f32)> { - // Convert tensor to F32 for stats calculation - makes statistics more accurate anyway - let tensor_f32 = tensor.to_dtype(DType::F32)?; - let flat = tensor_f32.flatten_all()?; - - let min = flat.min(0)?.to_scalar::()?; - let max = flat.max(0)?.to_scalar::()?; - let mean = flat.mean_all()?.to_scalar::()?; - - // Create a tensor with the same shape as flat but filled with the mean value - let mean_tensor = Tensor::new(&[mean], flat.device())?; - let mean_tensor = mean_tensor.broadcast_as(flat.shape().dims())?; - let diff = (flat - mean_tensor)?; - let squared = diff.sqr()?; - let variance = squared.mean_all()?; - let std = variance.sqrt()?.to_scalar::()?; - - Ok((min, max, mean, std)) -} - -// Add this function to track statistics at key points -fn log_tensor_stats(name: &str, tensor: &Tensor) -> Result<()> { - let (min, max, mean, std) = get_tensor_stats(tensor)?; - println!("{} stats: min={:.5}, max={:.5}, mean={:.5}, std={:.5}", name, min, max, mean, std); - Ok(()) -} - -// Add this simpler alternative function -fn save_tensor_as_numpy(tensor: &Tensor, filename: &str) -> Result<()> { - use std::fs::File; - use std::io::Write; - - // Move tensor to CPU and convert to f32 for consistent serialization - let tensor_cpu = tensor.to_device(&Device::Cpu)?.to_dtype(DType::F32)?; - let data = tensor_cpu.to_vec1::()?; - - // Create a simple binary file with the tensor data - let mut file = File::create(filename)?; - - // Write shape information first (as u32 values) - let shape = tensor.shape().dims(); - file.write_all(&(shape.len() as u32).to_le_bytes())?; - for &dim in shape { - file.write_all(&(dim as u32).to_le_bytes())?; - } - - // Write all float values - for val in data { - file.write_all(&val.to_le_bytes())?; - } - - Ok(()) + let flat = tensor.flatten_all()?; + let min = flat.min(0)?.to_scalar::()?; + let max = flat.max(0)?.to_scalar::()?; + let mean = flat.mean_all()?.to_scalar::()?; + + // Calculate standard deviation manually + // Create a tensor with the same shape as flat but filled with the mean value + let mean_tensor = Tensor::new(&[mean], flat.device())?; + let mean_tensor = mean_tensor.broadcast_as(flat.shape().dims())?; + let diff = (flat - mean_tensor)?; + let squared = diff.sqr()?; + let variance = squared.mean_all()?; + let std = variance.sqrt()?.to_scalar::()?; + + Ok((min, max, mean, std)) } fn run(args: Args) -> Result<()> { - use tracing_chrome::ChromeLayerBuilder; - use tracing_subscriber::prelude::*; + use tracing_chrome::ChromeLayerBuilder; + use tracing_subscriber::prelude::*; - let Args { prompt, uncond_prompt, cpu, height, width, n_steps, tokenizer, final_image, sliced_attention_size, num_samples, bsize, sd_version, clip_weights, clip2_weights, vae_weights, unet_weights, tracing, use_f16, guidance_scale, use_flash_attn, mask_path, img2img, strength, seed, only_update_masked, save_tensors, intermediary_images, half_precision } = args; + let Args { + prompt, + uncond_prompt, + cpu, + height, + width, + n_steps, + tokenizer, + final_image, + sliced_attention_size, + num_samples, + bsize, + sd_version, + clip_weights, + clip2_weights, + vae_weights, + unet_weights, + tracing, + use_f16, + guidance_scale, + use_flash_attn, + mask_path, + img2img, + strength, + seed, + .. + } = args; - if !(0. ..=1.).contains(&strength) { - anyhow::bail!("strength should be between 0 and 1, got {strength}") - } - - let _guard = if tracing { - let (chrome_layer, guard) = ChromeLayerBuilder::new().build(); - tracing_subscriber::registry().with(chrome_layer).init(); - Some(guard) - } else { - None - }; - - println!("=== LCM-Dreamshaper F16 DEBUG MODE ==="); - println!("Using dtype: {:?}", if half_precision { DType::F16 } else { DType::F32 }); - - let dtype = if half_precision { DType::F16 } else { DType::F32 }; - let sd_config = match sd_version { - StableDiffusionVersion::V1_5 | StableDiffusionVersion::V1_5Inpaint => stable_diffusion::StableDiffusionConfig::v1_5(sliced_attention_size, height, width), - StableDiffusionVersion::V2_1 | StableDiffusionVersion::V2Inpaint => stable_diffusion::StableDiffusionConfig::v2_1(sliced_attention_size, height, width), - StableDiffusionVersion::Xl | StableDiffusionVersion::XlInpaint => stable_diffusion::StableDiffusionConfig::sdxl(sliced_attention_size, height, width), - StableDiffusionVersion::Turbo => stable_diffusion::StableDiffusionConfig::sdxl_turbo(sliced_attention_size, height, width), - }; - - let mut scheduler = stable_diffusion::lcm::LCMScheduler::new(n_steps, stable_diffusion::lcm::LCMSchedulerConfig::default())?; - println!("Scheduler created with dtype={:?}", scheduler.dtype()); - - let device = candle_examples::device(cpu)?; - // If a seed is not given, generate a random seed and print it - let seed = seed.unwrap_or(rand::thread_rng().gen_range(0u64..u64::MAX)); - println!("Using seed {seed}"); - device.set_seed(seed)?; - let use_guide_scale = guidance_scale > 1.0; - - let which = match sd_version { - StableDiffusionVersion::Xl | StableDiffusionVersion::XlInpaint | StableDiffusionVersion::Turbo => vec![true, false], - _ => vec![true], - }; - - // Define the base model and LCM model repositories - let base_model_repo = match sd_version { - StableDiffusionVersion::V1_5 | StableDiffusionVersion::V1_5Inpaint => { - // Dreamshaper is based on SD 1.5, so use it for those versions - "Lykon/dreamshaper-7" - }, - _ => { - // For other versions, use the standard repo - sd_version.repo() - }, - }; - let lcm_model_repo = "SimianLuo/LCM_Dreamshaper_v7"; // LCM UNet - - // Create a custom ModelFile enum variant for the base model - let clip_weights = if clip_weights.is_none() { - use hf_hub::api::sync::Api; - let api = Api::new()?; - let clip_path = api.model(base_model_repo.to_string()).get(if use_f16 { "text_encoder/model.fp16.safetensors" } else { "text_encoder/model.safetensors" })?; - Some(clip_path.to_string_lossy().to_string()) - } else { - clip_weights - }; - - let vae_weights = if vae_weights.is_none() { - use hf_hub::api::sync::Api; - let api = Api::new()?; - let vae_path = api.model(base_model_repo.to_string()).get(if use_f16 { "vae/diffusion_pytorch_model.fp16.safetensors" } else { "vae/diffusion_pytorch_model.safetensors" })?; - Some(vae_path.to_string_lossy().to_string()) - } else { - vae_weights - }; - - // For the UNet, always use the LCM model - let unet_weights = { - use hf_hub::api::sync::Api; - let api = Api::new()?; - let unet_path = api.model(lcm_model_repo.to_string()).get(if use_f16 { - if lcm_model_repo == "SimianLuo/LCM_Dreamshaper_v7" { - "unet/diffusion_pytorch_model.safetensors" - } else { - "unet/diffusion_pytorch_model.fp16.safetensors" - } - } else { - "unet/diffusion_pytorch_model.safetensors" - })?; - Some(unet_path.to_string_lossy().to_string()) - }; - - // Load text embeddings from the base model - let text_embeddings = which.iter().map(|first| text_embeddings(&prompt, &uncond_prompt, tokenizer.clone(), clip_weights.clone(), clip2_weights.clone(), sd_version, &sd_config, use_f16, &device, dtype, use_guide_scale, *first)).collect::>>()?; - - let text_embeddings = Tensor::cat(&text_embeddings, D::Minus1)?; - let text_embeddings = text_embeddings.repeat((bsize, 1, 1))?; - println!("Text embeddings shape: {:?}", text_embeddings.shape()); - if PRINT_DEBUG_TENSORS { - println!("Text embeddings first few values: {}", text_embeddings.i(0)?.i(0)?.narrow(0, 0, 10)?); - } - - // Replace the random projection with a fixed one to match diffusers - let text_embeddings = if text_embeddings.dim(2)? == 2048 { - println!("Projecting text embeddings from 2048 to 768 dimensions"); - - // Load the projection matrix from a file (you'll need to extract this from the diffusers model) - // For now, we'll still use a random one but with a fixed seed - device.set_seed(42)?; // Use a fixed seed for the projection - let projection = Tensor::randn(0.0, 0.02, (2048, 768), &device)?.to_dtype(dtype)?; - - let proj_embeddings = text_embeddings.matmul(&projection)?; - proj_embeddings - } else { - text_embeddings - }; - if PRINT_DEBUG_TENSORS { - println!("Projected text embeddings: {:?}", text_embeddings); - } - - println!("Building the autoencoder."); - let vae_weights = ModelFile::Vae.get(vae_weights, sd_version, use_f16)?; - let vae = sd_config.build_vae(vae_weights, &device, dtype)?; - - let (image, init_latent_dist) = match &img2img { - None => (None, None), - Some(image) => { - let image = image_preprocess(image)?.to_device(&device)?.to_dtype(dtype)?; - println!("Input image shape: {:?}", image.shape()); - (Some(image.clone()), Some(vae.encode(&image)?)) - }, - }; - - println!( - "Latent shape after VAE encoding: {:?}", - init_latent_dist.as_ref().map(|dist| { - // Try to get shape from a temporary sample - match dist.sample() { - Ok(sample) => format!("{:?}", sample.shape()), - Err(_) => "Unable to determine shape".to_string(), - } - }) - ); - - println!("Building the unet."); - let unet_weights = ModelFile::UnetLcm.get(unet_weights, sd_version, use_f16)?; - let in_channels = match sd_version { - StableDiffusionVersion::XlInpaint | StableDiffusionVersion::V2Inpaint | StableDiffusionVersion::V1_5Inpaint => 9, - _ => 4, - }; - println!("Loading UNet weights from: SimianLuo/LCM_Dreamshaper_v7/unet"); - - // Ensure we're loading the correct UNet configuration - println!("Building UNet for LCM (SimianLuo/LCM_Dreamshaper_v7)"); - - // For LCM models, ensure that: - // 1. We're using the correct prediction type (epsilon) - // 2. The cross-attention dimension is correct (768 for Dreamshaper) - // 3. The in_channels matches the expected input (4 for standard, 9 for inpainting) - let unet_config = UNet2DConditionModelConfig { - // LCM specific config values - cross_attention_dim: 768, // Dreamshaper v7 uses 768 - // Rest of configuration... - center_input_sample: false, - flip_sin_to_cos: true, - freq_shift: 0.0, - blocks: vec![BlockConfig { out_channels: 320, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 640, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 1280, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 1280, use_cross_attn: None, attention_head_dim: 8 }], - layers_per_block: 2, - downsample_padding: 1, - mid_block_scale_factor: 1.0, - norm_num_groups: 32, - norm_eps: 1e-5, - sliced_attention_size: None, - use_linear_projection: false, - }; - - // Load the model directly from the safetensors file - let vs_unet = unsafe { VarBuilder::from_mmaped_safetensors(&[unet_weights], dtype, &device)? }; - - let unet = UNet2DConditionModel::new(vs_unet, in_channels, 4, use_flash_attn, unet_config)?; - - // Ensure strength is at least 0.4 for img2img to work well - let effective_strength = if img2img.is_some() { - strength.max(0.4).min(0.99) // Keep between 0.4 and 0.99 - } else { - strength - }; - - // Make sure we're doing a proper image-to-image transform - // with the right amount of noise - println!("Using img2img with strength={:.4}", effective_strength); - let mut raw_t_start = n_steps - (n_steps as f64 * effective_strength).ceil() as usize; - - let vae_scale = match sd_version { - StableDiffusionVersion::V1_5 | StableDiffusionVersion::V1_5Inpaint | StableDiffusionVersion::V2_1 | StableDiffusionVersion::V2Inpaint | StableDiffusionVersion::XlInpaint | StableDiffusionVersion::Xl => 0.18215, - StableDiffusionVersion::Turbo => 0.13025, - }; - - let (mask_latents, mask, mask_4) = inpainting_tensors(sd_version, mask_path, dtype, &device, use_guide_scale, &vae, image, vae_scale)?; - - println!("Timesteps: {:?}", scheduler.timesteps()); - // println!("Alpha cumprod: {:?}", scheduler.alphas_cumprod()); - - // Enable debug dumps to compare with diffusers - let enable_debug_dumps = true; - let debug_dir = "lcm_rust_debug"; - if enable_debug_dumps { - std::fs::create_dir_all(debug_dir).expect("Failed to create debug directory"); - - // Log the scheduler configuration - let mut config_file = std::fs::File::create(format!("{}/scheduler_config.txt", debug_dir)).expect("Failed to create config file"); - use std::io::Write; - writeln!(config_file, "Rust LCM Configuration:").unwrap(); - writeln!(config_file, " beta_start: {}", scheduler.config.beta_start).unwrap(); - writeln!(config_file, " beta_end: {}", scheduler.config.beta_end).unwrap(); - // More config params... - writeln!(config_file, "\nTimesteps: {:?}", scheduler.timesteps()).unwrap(); - writeln!(config_file, "First 10 alphas_cumprod: {:?}", scheduler.alphas_cumprod().iter().take(10).collect::>()).unwrap(); - } - - // Add this code before the loop where we iterate through samples - // This will generate and log the guidance scale embedding - - // Create and log guidance scale embedding for comparison with Python - println!("\n==== Generating LCM Guidance Scale Embedding ===="); - let guidance_scale = args.guidance_scale; - println!("Guidance scale: {}", guidance_scale); - - // The embedding dimension should match the UNet time embedding dimension - // For LCM Dreamshaper, this is typically 1280 (not 320) - // Use a hardcoded value since we don't have direct access to the config - let embedding_dim = 1280; // Standard for SD models based on UNet config - println!("Embedding dimension: {}", embedding_dim); - - // Generate the embedding - let guidance_embedding = scheduler.get_guidance_scale_embedding(guidance_scale, embedding_dim, &device, dtype)?; - - println!("Guidance embedding shape: {:?}", guidance_embedding.shape()); - - // Save the embedding to a numpy file for comparison with Python - if save_tensors { - let tensor_dir = "lcm_rust_tensors"; - std::fs::create_dir_all(tensor_dir)?; - - save_tensor_as_numpy(&guidance_embedding, &format!("{}/guidance_embedding.npy", tensor_dir))?; - println!("Saved guidance embedding to {}/guidance_embedding.npy", tensor_dir); - } - println!("===============================================\n"); - - for idx in 0..num_samples { - let timesteps = scheduler.timesteps().to_vec(); - - // Initialize latents - let latents = if let Some(init_latent_dist) = &init_latent_dist { - println!("Using img2img with strength={:.4}", effective_strength); - - // LCM can be more sensitive to the starting timestep - // Get the latents from the VAE - let latents = (init_latent_dist.sample()? * vae_scale)?.to_device(&device)?; - - // Calculate the actual start timestep based on strength - // In LCM, starting too late can cause artifacts, so ensure we start early enough - let t_start_index = (timesteps.len() as f64 * (1.0 - effective_strength.max(0.5))).floor() as usize; - let t_start = timesteps[t_start_index.min(timesteps.len() - 1)]; - - println!("Starting from timestep {}/{} (index: {})", t_start, timesteps[0], t_start_index); - - // Use a fixed seed for noise to improve reproducibility - device.set_seed(seed)?; - let noise = (Tensor::randn(0f32, 1f32, latents.shape(), &device)?).to_dtype(dtype)?; - - // Clone the noise when passing it to add_noise - let noised_latents = scheduler.add_noise(&latents, noise.clone(), t_start)?; - - // Now we can use noise for logging since we cloned it above - log_tensor_stats("initial_latents", &latents)?; - log_tensor_stats("noise", &noise)?; - log_tensor_stats("noised_latents", &noised_latents)?; - - // Check the dtype of noised_latents - debug_tensor("final_img2img_latents", &noised_latents)?; - - // Explicitly convert to expected dtype to avoid errors - noised_latents.to_dtype(dtype)? - } else { - // Regular text-to-image generation with random noise - let latents = Tensor::randn(0f32, 1f32, (bsize, 4, sd_config.height / 8, sd_config.width / 8), &device)?; - - // Use affine for multiplication - let scaled_latents = latents.affine(scheduler.init_noise_sigma(), 0.0)?; - debug_tensor("scaled_initial_latents", &scaled_latents)?; - - // Explicitly convert to expected dtype - scaled_latents.to_dtype(dtype)? - }; - let mut latents = latents.to_dtype(dtype)?; - - println!("Setting seed to {}", seed); - device.set_seed(seed)?; - - if PRINT_DEBUG_TENSORS { - println!("Initial latents shape: {:?}", latents.shape()); - println!("Initial latents first few values: {}", latents.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + if !(0. ..=1.).contains(&strength) { + anyhow::bail!("strength should be between 0 and 1, got {strength}") } - println!("Starting LCM sampling"); - let start_time = std::time::Instant::now(); - - for (timestep_index, ×tep) in timesteps.iter().enumerate() { - if timestep_index < raw_t_start { - continue; - } - - println!("\n--- Timestep {}/{} (value: {}) ---", timestep_index + 1, timesteps.len(), timestep); - - let latent_model_input = if use_guide_scale { - println!("Using classifier-free guidance with scale: {}", guidance_scale); - Tensor::cat(&[&latents, &latents], 0)? - } else { - latents.clone() - }; - - if PRINT_DEBUG_TENSORS { - println!("Latent input shape before scaling: {:?}", latent_model_input.shape()); - println!("Latent input first few values: {}", latent_model_input.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); - } - - let latent_model_input = scheduler.scale_model_input(latent_model_input, timestep); - - if PRINT_DEBUG_TENSORS { - println!("Latent input shape after scaling: {:?}", latent_model_input.shape()); - println!("Scaled latent input first few values: {}", latent_model_input.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); - } - - // Generate guidance scale embedding - let guidance_emb = if use_guide_scale { - // For LCM/Dreamshaper, the time embedding dimension is 1280 - let time_embed_dim = 1280; // Standard for SD models (320 base channels * 4) - - println!("Generating guidance scale embedding (scale={})", guidance_scale); - Some(scheduler.get_guidance_scale_embedding(guidance_scale, time_embed_dim, &device, dtype)?) - } else { + let _guard = if tracing { + let (chrome_layer, guard) = ChromeLayerBuilder::new().build(); + tracing_subscriber::registry().with(chrome_layer).init(); + Some(guard) + } else { None - }; + }; - // Before UNet forward call - debug_tensor("latent_input_to_unet", &latent_model_input)?; - debug_tensor("text_embeddings_to_unet", &text_embeddings)?; + let dtype = if use_f16 { DType::F16 } else { DType::F32 }; + let sd_config = match sd_version { + StableDiffusionVersion::V1_5 | StableDiffusionVersion::V1_5Inpaint => { + stable_diffusion::StableDiffusionConfig::v1_5(sliced_attention_size, height, width) + } + StableDiffusionVersion::V2_1 | StableDiffusionVersion::V2Inpaint => { + stable_diffusion::StableDiffusionConfig::v2_1(sliced_attention_size, height, width) + } + StableDiffusionVersion::Xl | StableDiffusionVersion::XlInpaint => { + stable_diffusion::StableDiffusionConfig::sdxl(sliced_attention_size, height, width) + } + StableDiffusionVersion::Turbo => stable_diffusion::StableDiffusionConfig::sdxl_turbo( + sliced_attention_size, + height, + width, + ), + }; - // Pass the guidance embedding to the UNet - // This would require modifying the UNet forward method to accept this parameter - println!("Running UNet forward pass with guidance embedding..."); - let noise_pred = unet.forward_with_guidance(&latent_model_input, timestep as f64, &text_embeddings, guidance_emb.as_ref())?; + let mut scheduler = stable_diffusion::lcm::LCMScheduler::new( + n_steps, + stable_diffusion::lcm::LCMSchedulerConfig::default(), + )?; - if PRINT_DEBUG_TENSORS { - println!("Noise prediction shape: {:?}", noise_pred.shape()); - println!("Noise prediction first few values: {}", noise_pred.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); - } + let device = candle_examples::device(cpu)?; + // If a seed is not given, generate a random seed and print it + let seed = seed.unwrap_or(rand::thread_rng().gen_range(0u64..u64::MAX)); + println!("Using seed {seed}"); + device.set_seed(seed)?; + let use_guide_scale = guidance_scale > 1.0; - let noise_pred = if use_guide_scale { - let noise_pred = noise_pred.chunk(2, 0)?; - let (noise_pred_uncond, noise_pred_text) = (&noise_pred[0], &noise_pred[1]); + let which = match sd_version { + StableDiffusionVersion::Xl + | StableDiffusionVersion::XlInpaint + | StableDiffusionVersion::Turbo => vec![true, false], + _ => vec![true], + }; - // LCM requires a different guidance approach than standard diffusion - // Linear interpolation with scale factor - let diff = (noise_pred_text - noise_pred_uncond)?; - let scaled_diff = diff.affine(guidance_scale, 0.0)?; - (noise_pred_uncond + &scaled_diff)? - } else { - noise_pred - }; + // Define the base model and LCM model repositories + let base_model_repo = match sd_version { + StableDiffusionVersion::V1_5 | StableDiffusionVersion::V1_5Inpaint => { + // Dreamshaper is based on SD 1.5, so use it for those versions + "Lykon/dreamshaper-7" + }, + _ => { + // For other versions, use the standard repo + sd_version.repo() + } + }; + let lcm_model_repo = "SimianLuo/LCM_Dreamshaper_v7"; // LCM UNet - if PRINT_DEBUG_TENSORS { - println!("Latents before step first few values: {}", latents.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); - } + // Create a custom ModelFile enum variant for the base model + let clip_weights = if clip_weights.is_none() { + use hf_hub::api::sync::Api; + let api = Api::new()?; + let clip_path = api.model(base_model_repo.to_string()) + .get(if use_f16 { "text_encoder/model.fp16.safetensors" } + else { "text_encoder/model.safetensors" })?; + Some(clip_path.to_string_lossy().to_string()) + } else { + clip_weights + }; - println!("Applying scheduler step..."); - latents = scheduler.step(&noise_pred, timestep, &latents)?; + let vae_weights = if vae_weights.is_none() { + use hf_hub::api::sync::Api; + let api = Api::new()?; + let vae_path = api.model(base_model_repo.to_string()) + .get(if use_f16 { "vae/diffusion_pytorch_model.fp16.safetensors" } + else { "vae/diffusion_pytorch_model.safetensors" })?; + Some(vae_path.to_string_lossy().to_string()) + } else { + vae_weights + }; - if PRINT_DEBUG_TENSORS { - println!("Latents after step first few values: {}", latents.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); - } + // For the UNet, always use the LCM model + let unet_weights = { + use hf_hub::api::sync::Api; + let api = Api::new()?; + let unet_path = api.model(lcm_model_repo.to_string()) + .get(if use_f16 { "unet/diffusion_pytorch_model.fp16.safetensors" } + else { "unet/diffusion_pytorch_model.safetensors" })?; + Some(unet_path.to_string_lossy().to_string()) + }; - println!("Step {}/{} completed", timestep_index + 1, n_steps); + // Load text embeddings from the base model + let text_embeddings = which + .iter() + .map(|first| { + text_embeddings( + &prompt, + &uncond_prompt, + tokenizer.clone(), + clip_weights.clone(), + clip2_weights.clone(), + sd_version, + &sd_config, + use_f16, + &device, + dtype, + use_guide_scale, + *first, + ) + }) + .collect::>>()?; - // Then use it at key points - log_tensor_stats("latents_before_step", &latents)?; - log_tensor_stats("noise_pred", &noise_pred)?; - log_tensor_stats("latents_after_step", &latents)?; + let text_embeddings = Tensor::cat(&text_embeddings, D::Minus1)?; + let text_embeddings = text_embeddings.repeat((bsize, 1, 1))?; + println!("Text embeddings shape: {:?}", text_embeddings.shape()); + println!("Text embeddings first few values: {}", + text_embeddings.i(0)?.i(0)?.narrow(0, 0, 10)?); - if save_tensors { - let tensor_dir = "lcm_rust_tensors"; - std::fs::create_dir_all(tensor_dir)?; + // Replace the random projection with a fixed one to match diffusers + let text_embeddings = if text_embeddings.dim(2)? == 2048 { + println!("Projecting text embeddings from 2048 to 768 dimensions"); + + // Load the projection matrix from a file (you'll need to extract this from the diffusers model) + // For now, we'll still use a random one but with a fixed seed + device.set_seed(42)?; // Use a fixed seed for the projection + let projection = Tensor::randn( + 0.0, + 0.02, + (2048, 768), + &device, + )?.to_dtype(dtype)?; + + let proj_embeddings = text_embeddings.matmul(&projection)?; + proj_embeddings + } else { + text_embeddings + }; + println!("Projected text embeddings: {:?}", text_embeddings); - // Save key tensors at each step - let step_dir = format!("{}/step_{}", tensor_dir, timestep_index); - std::fs::create_dir_all(&step_dir)?; + println!("Building the autoencoder."); + let vae_weights = ModelFile::Vae.get(vae_weights, sd_version, use_f16)?; + let vae = sd_config.build_vae(vae_weights, &device, dtype)?; - // Save various tensors as numpy arrays for later comparison - save_tensor_as_numpy(&latent_model_input, &format!("{}/input.npy", step_dir))?; - save_tensor_as_numpy(&noise_pred, &format!("{}/noise_pred.npy", step_dir))?; - save_tensor_as_numpy(&latents, &format!("{}/output.npy", step_dir))?; - } + let (image, init_latent_dist) = match &img2img { + None => (None, None), + Some(image) => { + let image = image_preprocess(image)? + .to_device(&device)? + .to_dtype(dtype)?; + println!("Input image shape: {:?}", image.shape()); + (Some(image.clone()), Some(vae.encode(&image)?)) + } + }; - // After UNet forward call - debug_tensor("noise_pred_from_unet", &noise_pred)?; + println!("Latent shape after VAE encoding: {:?}", + init_latent_dist.as_ref().map(|dist| { + // Try to get shape from a temporary sample + match dist.sample() { + Ok(sample) => format!("{:?}", sample.shape()), + Err(_) => "Unable to determine shape".to_string() + } + })); + + println!("Building the unet."); + let unet_weights = ModelFile::UnetLcm.get(unet_weights, sd_version, use_f16)?; + let in_channels = match sd_version { + StableDiffusionVersion::XlInpaint + | StableDiffusionVersion::V2Inpaint + | StableDiffusionVersion::V1_5Inpaint => 9, + _ => 4, + }; + println!("Loading UNet weights from: {:?}", unet_weights); + + // Load tensors using candle's safetensors loader + let tensors = candle::safetensors::load(&unet_weights, &device)?; + + // Create a new HashMap to store the processed tensors + let mut processed_tensors = std::collections::HashMap::new(); + + // Process and print shapes of tensors + for (name, tensor) in tensors.iter() { + let processed_tensor = if name.contains("proj_in.weight") || name.contains("proj_out.weight") { + let shape = tensor.shape(); + if shape.dims().len() == 2 { + // If it's a 2D tensor, reshape it to 4D with trailing 1s + println!("Expanding tensor {} from {:?}", name, shape); + tensor.reshape((shape.dims()[0], shape.dims()[1], 1, 1))? + } else { + tensor.clone() + } + } else { + tensor.clone() + }; + + if name.contains("down_blocks.1.attentions.0.proj_in") { + println!("Found tensor: {} with shape: {:?} -> {:?}", + name, tensor.shape(), processed_tensor.shape()); + } + + processed_tensors.insert(name.clone(), processed_tensor); } + + println!("Building UNet with in_channels: {}", in_channels); - let dt = start_time.elapsed().as_secs_f32(); - println!("Sampling completed in {:.2}s", dt); + // Make sure the UNet config exactly matches the diffusers model + let unet_config = UNet2DConditionModelConfig { + // LCM specific config values + cross_attention_dim: 768, // Dreamshaper v7 uses 768 + time_cond_proj_dim: Some(256), // LCM models use 256-dimensional guidance embeddings + // Rest of configuration... + center_input_sample: false, + flip_sin_to_cos: true, + freq_shift: 0.0, + blocks: vec![ + BlockConfig { + out_channels: 320, + use_cross_attn: Some(1), + attention_head_dim: 8, + }, + BlockConfig { + out_channels: 640, + use_cross_attn: Some(1), + attention_head_dim: 8, + }, + BlockConfig { + out_channels: 1280, + use_cross_attn: Some(1), + attention_head_dim: 8, + }, + BlockConfig { + out_channels: 1280, + use_cross_attn: None, + attention_head_dim: 8, + }, + ], + layers_per_block: 2, + downsample_padding: 1, + mid_block_scale_factor: 1.0, + norm_num_groups: 32, + norm_eps: 1e-5, + sliced_attention_size: None, + use_linear_projection: false, + time_embed_dim: Some(1280), + // time_embed_dim: Some(256), + }; + + + // Let's try a different approach - load the model directly from the safetensors file + // and modify the weights after loading + let vs_unet = unsafe { + VarBuilder::from_mmaped_safetensors(&[unet_weights], dtype, &device)? + }; + + let unet = UNet2DConditionModel::new( + vs_unet, + in_channels, + 4, + use_flash_attn, + unet_config, + )?; - // Generate final image - println!("Generating final image for sample {}/{}", idx + 1, num_samples); - save_image(&vae, &latents, vae_scale, bsize, idx, &final_image, num_samples, None)?; - } - Ok(()) -} + // Run with a much higher strength to ensure sufficient transformation + let effective_strength = if img2img.is_some() { + // Force at least 0.8 strength for good results + strength.max(0.8) + } else { + strength + }; -// Add this debug function to check dtype at critical points -fn debug_tensor(name: &str, tensor: &Tensor) -> Result<()> { - println!("DEBUG {}: shape={:?}, dtype={:?}", name, tensor.shape(), tensor.dtype()); - Ok(()) + // Use this effective_strength in the t_start calculation + // let raw_t_start = n_steps - (n_steps as f64 * effective_strength) as usize; + // let raw_t_start = (n_steps as f64 * (1.0 - effective_strength)).round() as usize; + let raw_t_start = ((n_steps as f64 * (1.0 - effective_strength)).round() as usize).min(n_steps - 1); + + println!("Using img2img with strength={}, raw t_start={}", effective_strength, raw_t_start); + + // Ensure we have at least 2 steps for img2img (more steps = more transformation) + if raw_t_start >= n_steps - 1 { + println!("WARNING: strength too low, forcing more steps"); + n_steps - 2 // Ensure we do at least 2 steps + } else { + raw_t_start + }; + + let vae_scale = match sd_version { + StableDiffusionVersion::V1_5 + | StableDiffusionVersion::V1_5Inpaint + | StableDiffusionVersion::V2_1 + | StableDiffusionVersion::V2Inpaint + | StableDiffusionVersion::XlInpaint + | StableDiffusionVersion::Xl => 0.18215, + StableDiffusionVersion::Turbo => 0.13025, + }; + + let (mask_latents, mask, mask_4) = inpainting_tensors( + sd_version, + mask_path, + dtype, + &device, + use_guide_scale, + &vae, + image, + vae_scale, + )?; + + println!("Timesteps: {:?}", scheduler.timesteps()); + println!("Alpha cumprod: {:?}", scheduler.alphas_cumprod()); + + for idx in 0..num_samples { + let timesteps = scheduler.timesteps().to_vec(); + + // Initialize latents + // let latents = match &init_latent_dist { + // Some(init_latent_dist) => { + // let latents = (init_latent_dist.sample()? * vae_scale)?.to_device(&device)?; + // if raw_t_start < timesteps.len() { + // println!("Adding noise to image latents for img2img"); + // let noise = latents.randn_like(0f64, 1f64)?; + + // // Print the latents before and after adding noise + // // println!("Latents before noise: {}", + // // latents.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + + // let noised_latents = scheduler.add_noise(&latents, noise, timesteps[raw_t_start])?; + + // let scaled_noised_latents = ((latents.clone() * (1.0 - strength))? + (noised_latents.clone() * strength)?)?; + // // println!("Latents after noise: {}", + // // noised_latents.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + + // // Print the amount of noise added - clone tensors to avoid moves + // let latents_clone = latents.clone(); + // let noised_clone = noised_latents.clone(); + // println!("Amount of noise added: {}", + // ((noised_clone - latents_clone)?.div(&latents)?).i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + + // scaled_noised_latents + // } else { + // println!("No noise added (t_start >= timesteps.len())"); + // latents + // } + // } + // None => { + // let latents = Tensor::randn( + // 0f32, + // 1f32, + // (bsize, 4, sd_config.height / 8, sd_config.width / 8), + // &device, + // )?; + // (latents * scheduler.init_noise_sigma())? + // } + // }; + let latents = match &init_latent_dist { + Some(init_latent_dist) => { + let latents = (init_latent_dist.sample()? * vae_scale)?.to_device(&device)?; + + // Compute `t_start` + let timesteps = LCMScheduler::get_timesteps_for_steps(n_steps, strength); + let t_start = timesteps[0]; // First denoising step + + // Add noise at `t_start` + let noise = latents.randn_like(0f64, 1f64)?; + let noisy_latents = scheduler.add_noise(&latents, noise, t_start)?; + + println!("Latents after adding noise at {}: {:?}", t_start, noisy_latents.shape()); + noisy_latents + } + None => { + let latents = Tensor::randn( + 0f32, + 1f32, + (bsize, 4, sd_config.height / 8, sd_config.width / 8), + &device, + )?; + (latents * scheduler.init_noise_sigma())? + } + }; + + let mut latents = latents.to_dtype(dtype)?; + + println!("Setting seed to {}", seed); + device.set_seed(seed)?; + + println!("Initial latents shape: {:?}", latents.shape()); + println!("Initial latents first few values: {}", + latents.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + + println!("Starting LCM sampling"); + let start_time = std::time::Instant::now(); + + let embedding_dim = 256; + let guidance_scale_embedding = scheduler.get_guidance_scale_embedding(guidance_scale, embedding_dim, &device, dtype)?; + + for (timestep_index, ×tep) in timesteps.iter().enumerate() { + if timestep_index < raw_t_start { + println!("Skipping timestep {} because it's before t_start", timestep); + // continue; + } + + println!("\n--- Timestep {}/{} (value: {}) ---", + timestep_index + 1, timesteps.len(), timestep); + + let latent_model_input = if use_guide_scale { + println!("Using classifier-free guidance with scale: {}", guidance_scale); + Tensor::cat(&[&latents, &latents], 0)? + } else { + latents.clone() + }; + + println!("Latent input shape before scaling: {:?}", latent_model_input.shape()); + println!("Latent input first few values: {}", + latent_model_input.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + + let latent_model_input = scheduler.scale_model_input(latent_model_input, timestep); + + println!("Latent input shape after scaling: {:?}", latent_model_input.shape()); + println!("Scaled latent input first few values: {}", + latent_model_input.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + + println!("Running UNet forward pass..."); + let noise_pred = unet.forward_with_guidance(&latent_model_input, timestep as f64, &text_embeddings, Some(&guidance_scale_embedding))?; + + println!("Noise prediction shape: {:?}", noise_pred.shape()); + println!("Noise prediction first few values: {}", + noise_pred.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + + let noise_pred = if use_guide_scale { + let noise_pred = noise_pred.chunk(2, 0)?; + let (noise_pred_uncond, noise_pred_text) = (&noise_pred[0], &noise_pred[1]); + + println!("Uncond noise pred first few values: {}", + noise_pred_uncond.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + println!("Text noise pred first few values: {}", + noise_pred_text.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + + let guided = (noise_pred_uncond + ((noise_pred_text - noise_pred_uncond)? * guidance_scale)?)?; + + println!("Guided noise pred first few values: {}", + guided.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + guided + } else { + noise_pred + }; + + println!("Latents before step first few values: {}", + latents.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + + println!("Applying scheduler step..."); + latents = scheduler.step(&noise_pred, timestep, &latents, timestep_index, n_steps)?; + + println!("Latents after step first few values: {}", + latents.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); + + println!("Step {}/{} completed", timestep_index + 1, n_steps); + } + + let dt = start_time.elapsed().as_secs_f32(); + println!("Sampling completed in {:.2}s", dt); + + // Generate final image + println!("Generating final image for sample {}/{}", idx + 1, num_samples); + save_image(&vae, &latents, vae_scale, bsize, idx, &final_image, num_samples, None)?; + } + Ok(()) } fn main() -> Result<()> { - let args = Args::parse(); - run(args) + let args = Args::parse(); + run(args) } diff --git a/crates/vendor/candle/candle-flash-attn/build.rs b/crates/vendor/candle/candle-flash-attn/build.rs index e6cefb92c4..b8db9fcd17 100644 --- a/crates/vendor/candle/candle-flash-attn/build.rs +++ b/crates/vendor/candle/candle-flash-attn/build.rs @@ -86,6 +86,8 @@ fn main() -> Result<()> { .arg("--expt-relaxed-constexpr") .arg("--expt-extended-lambda") .arg("--use_fast_math") + .arg("--compiler-options") + .arg("-fPIC") .arg("--verbose"); if let Ok(target) = std::env::var("TARGET") { diff --git a/crates/vendor/candle/candle-nn/Cargo.toml b/crates/vendor/candle/candle-nn/Cargo.toml index 38711f1df7..4acb624b6d 100644 --- a/crates/vendor/candle/candle-nn/Cargo.toml +++ b/crates/vendor/candle/candle-nn/Cargo.toml @@ -32,3 +32,4 @@ metal = ["candle/metal", "dep:candle-metal-kernels", "dep:metal"] [[bench]] name = "bench_main" harness = false + diff --git a/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/lcm.rs b/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/lcm.rs index 8c0b7d27ef..96fbc2cdfd 100644 --- a/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/lcm.rs +++ b/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/lcm.rs @@ -1,762 +1,520 @@ -use super::schedulers::{betas_for_alpha_bar, BetaSchedule, PredictionType, TimestepSpacing}; -use candle::{Result, Tensor, IndexOp, Device, DType, Shape}; +use super::schedulers::{betas_for_alpha_bar, BetaSchedule, PredictionType}; +use candle::{DType, Device, IndexOp, Result, Tensor}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum LCMVarianceType { - FixedSmall, - FixedSmallLog, - FixedLarge, - FixedLargeLog, - Learned, + FixedSmall, + FixedSmallLog, + FixedLarge, + FixedLargeLog, + Learned, } impl Default for LCMVarianceType { - fn default() -> Self { - Self::FixedSmall - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LCMSolverType { - Default, - LogRho, -} - -impl Default for LCMSolverType { - fn default() -> Self { - Self::Default - } + fn default() -> Self { + Self::FixedSmall + } } #[derive(Debug, Clone)] pub struct LCMSchedulerConfig { - /// The value of beta at the beginning of training. - pub beta_start: f64, - /// The value of beta at the end of training. - pub beta_end: f64, - /// How beta evolved during training. - pub beta_schedule: BetaSchedule, - /// Option to predicted sample between -1 and 1 for numerical stability. - pub clip_sample: bool, - /// Option to clip the variance used when adding noise to the denoised sample. - pub variance_type: LCMVarianceType, - /// prediction type of the scheduler function - pub prediction_type: PredictionType, - /// number of diffusion steps used to train the model. - pub train_timesteps: usize, - /// time step spacing for the diffusion process - pub timestep_spacing: TimestepSpacing, - /// Original training steps - pub original_inference_steps: usize, - /// Timestep scaling factor (default: 10.0 in diffusers) - pub timestep_scaling: f64, - /// Adjust the indexes of the inference schedule by this value. - pub steps_offset: usize, - /// The solver type to use for the diffusion process - pub solver_type: LCMSolverType, + /// The value of beta at the beginning of training. + pub beta_start: f64, + /// The value of beta at the end of training. + pub beta_end: f64, + /// How beta evolved during training. + pub beta_schedule: BetaSchedule, + /// Option to predicted sample between -1 and 1 for numerical stability. + pub clip_sample: bool, + /// Option to clip the variance used when adding noise to the denoised sample. + pub variance_type: LCMVarianceType, + /// prediction type of the scheduler function + pub prediction_type: PredictionType, + /// number of diffusion steps used to train the model. + pub train_timesteps: usize, + /// timestep scaling factor + pub timestep_scaling: f64, } impl Default for LCMSchedulerConfig { - fn default() -> Self { - Self { beta_start: 0.00085, beta_end: 0.012, beta_schedule: BetaSchedule::ScaledLinear, clip_sample: false, variance_type: LCMVarianceType::FixedSmall, prediction_type: PredictionType::Epsilon, train_timesteps: 1000, timestep_spacing: TimestepSpacing::Leading, original_inference_steps: 50, timestep_scaling: 10.0, steps_offset: 1, solver_type: LCMSolverType::Default } - } + fn default() -> Self { + Self { + beta_start: 0.00085, + beta_end: 0.012, + beta_schedule: BetaSchedule::ScaledLinear, + clip_sample: false, + variance_type: LCMVarianceType::FixedSmall, + prediction_type: PredictionType::Epsilon, + train_timesteps: 1000, + timestep_scaling: 10.0, + } + } } pub struct LCMScheduler { - alphas_cumprod: Vec, - init_noise_sigma: f64, - timesteps: Vec, - step_ratio: usize, - pub config: LCMSchedulerConfig, - step_index: Option, - begin_index: Option, - dtype: DType, + alphas_cumprod: Vec, + init_noise_sigma: f64, + timesteps: Vec, + step_ratio: usize, + pub config: LCMSchedulerConfig, } impl LCMScheduler { - pub fn new(inference_steps: usize, config: LCMSchedulerConfig) -> Result { - // Set up the correct betas - let betas = match config.beta_schedule { - BetaSchedule::ScaledLinear => { - // LCM uses scaled_linear specifically this way - let start = config.beta_start.sqrt(); - let end = config.beta_end.sqrt(); - let betas = super::utils::linspace(start, end, config.train_timesteps)?; - betas.sqr()? - }, - // Other schedules... - _ => unreachable!("LCM needs ScaledLinear"), - }; + pub fn new(inference_steps: usize, config: LCMSchedulerConfig) -> Result { + let betas = match config.beta_schedule { + BetaSchedule::ScaledLinear => super::utils::linspace( + config.beta_start.sqrt(), + config.beta_end.sqrt(), + config.train_timesteps, + )? + .sqr()?, + BetaSchedule::Linear => { + super::utils::linspace(config.beta_start, config.beta_end, config.train_timesteps)? + } + BetaSchedule::SquaredcosCapV2 => betas_for_alpha_bar(config.train_timesteps, 0.999)?, + }; - // Set up alphas exactly as in diffusers - let betas = betas.to_vec1::()?; - let alphas = betas.iter().map(|beta| 1.0 - beta).collect::>(); + let betas = betas.to_vec1::()?; + let mut alphas_cumprod = Vec::with_capacity(betas.len()); + let mut alpha_prod = 1f64; + for &beta in betas.iter() { + let alpha = 1.0 - beta; + alpha_prod *= alpha; + alphas_cumprod.push(alpha_prod); + } - let mut alphas_cumprod = Vec::with_capacity(alphas.len()); - let mut alpha_prod = 1.0; - for alpha in alphas.iter() { - alpha_prod *= alpha; - alphas_cumprod.push(alpha_prod); + // Create proper LCM timesteps to match diffusers + let strength = 0.8; + let timesteps = LCMScheduler::get_timesteps_for_steps(inference_steps, strength); + let step_ratio = config.train_timesteps / inference_steps; + + println!("Using LCM timesteps: {:?}", timesteps); + + Ok(Self { + alphas_cumprod, + init_noise_sigma: 1.0, + timesteps, + step_ratio, + config, + }) } - // Create timesteps based on TimestepSpacing, similar to DDIM - let step_ratio = config.train_timesteps / inference_steps; - let timesteps: Vec = match config.timestep_spacing { - TimestepSpacing::Leading => (0..(inference_steps)).map(|s| s * step_ratio + config.steps_offset).rev().collect(), - TimestepSpacing::Trailing => std::iter::successors(Some(config.train_timesteps), |n| if *n > step_ratio { Some(n - step_ratio) } else { None }).map(|n| n - 1).collect(), - TimestepSpacing::Linspace => super::utils::linspace(0.0, (config.train_timesteps - 1) as f64, inference_steps)?.to_vec1::()?.iter().map(|&f| f as usize).rev().collect(), - }; + fn get_variance(&self, timestep: usize) -> f64 { + let prev_t = timestep as isize - self.step_ratio as isize; + let alpha_prod_t = self.alphas_cumprod[timestep]; + let alpha_prod_t_prev = if prev_t >= 0 { + self.alphas_cumprod[prev_t as usize] + } else { + 1.0 + }; + let current_beta_t = 1. - alpha_prod_t / alpha_prod_t_prev; - println!("Using LCM timesteps with {:?} spacing: {:?}", config.timestep_spacing, timesteps); + // For t > 0, compute predicted variance βt (see formula (6) and (7) from [the pdf](https://arxiv.org/pdf/2006.11239.pdf)) + // and sample from it to get previous sample + // x_{t-1} ~ N(pred_prev_sample, variance) == add variance to pred_sample + let variance = (1. - alpha_prod_t_prev) / (1. - alpha_prod_t) * current_beta_t; - let init_noise_sigma = 1.0; - - Ok(Self { alphas_cumprod, init_noise_sigma, timesteps, step_ratio, config, step_index: None, begin_index: None, dtype: DType::F32 }) - } - - pub fn with_dtype(mut self, dtype: DType) -> Self { - self.dtype = dtype; - self - } - - fn get_variance(&self, timestep: usize) -> f64 { - let prev_t = timestep as isize - self.step_ratio as isize; - let alpha_prod_t = self.alphas_cumprod[timestep]; - let alpha_prod_t_prev = if prev_t >= 0 { self.alphas_cumprod[prev_t as usize] } else { 1.0 }; - let current_beta_t = 1. - alpha_prod_t / alpha_prod_t_prev; - - // For t > 0, compute predicted variance βt (see formula (6) and (7) from [the pdf](https://arxiv.org/pdf/2006.11239.pdf)) - // and sample from it to get previous sample - // x_{t-1} ~ N(pred_prev_sample, variance) == add variance to pred_sample - let variance = (1. - alpha_prod_t_prev) / (1. - alpha_prod_t) * current_beta_t; - - // retrieve variance - match self.config.variance_type { - LCMVarianceType::FixedSmall => variance.max(1e-20), - // for rl-diffuser https://arxiv.org/abs/2205.09991 - LCMVarianceType::FixedSmallLog => { - let variance = variance.max(1e-20).ln(); - (variance * 0.5).exp() - }, - LCMVarianceType::FixedLarge => current_beta_t, - LCMVarianceType::FixedLargeLog => current_beta_t.ln(), - LCMVarianceType::Learned => variance, - } - } - - pub fn timesteps(&self) -> &[usize] { - self.timesteps.as_slice() - } - - /// Ensures interchangeability with schedulers that need to scale the denoising model input - /// depending on the current timestep. - pub fn scale_model_input(&self, sample: Tensor, timestep: usize) -> Tensor { - // LCM doesn't apply any special scaling to model inputs - println!("LCM: No scaling applied to model input for timestep {}", timestep); - sample - } - - fn get_scalings_for_boundary_condition_discrete(&self, timestep: usize) -> (f64, f64) { - // LCM uses a fixed sigma_data value of 0.5 - let sigma_data = 0.5f64; - - // Timestep scaling factor from diffusers configuration - let timestep_scaling = self.config.timestep_scaling; - - // In diffusers, they don't use the timestep value directly - let alpha_cumprod = self.alphas_cumprod[timestep]; - let sqrt_alpha_prod = alpha_cumprod.sqrt(); - let sqrt_one_minus_alpha_prod = (1.0 - alpha_cumprod).sqrt(); - - // This is the key calculation - convert alpha to sigma space with scaling - let scaled_timestep = sqrt_one_minus_alpha_prod / sqrt_alpha_prod * timestep_scaling; - - println!("Boundary condition for timestep={}: scaled_t={} (with scaling={})", timestep, scaled_timestep, timestep_scaling); - - // Calculate boundary condition coefficients with scaled timestep - let c_skip = sigma_data.powf(2.0) / (scaled_timestep.powf(2.0) + sigma_data.powf(2.0)); - let c_out = scaled_timestep / (scaled_timestep.powf(2.0) + sigma_data.powf(2.0)).sqrt(); - - (c_skip, c_out) - } - - pub fn step(&mut self, model_output: &Tensor, timestep: usize, sample: &Tensor) -> Result { - // Initialize step_index if it's not already set - if self.step_index.is_none() { - self.init_step_index(timestep)?; - println!("Initialized step_index to {} for timestep {}", self.step_index.unwrap(), timestep); + // retrieve variance + match self.config.variance_type { + LCMVarianceType::FixedSmall => variance.max(1e-20), + // for rl-diffuser https://arxiv.org/abs/2205.09991 + LCMVarianceType::FixedSmallLog => { + let variance = variance.max(1e-20).ln(); + (variance * 0.5).exp() + } + LCMVarianceType::FixedLarge => current_beta_t, + LCMVarianceType::FixedLargeLog => current_beta_t.ln(), + LCMVarianceType::Learned => variance, + } } - // Get current timestep index - let timestep_index = self.step_index.unwrap(); - - // Calculate prev_timestep_index - let prev_timestep_index = timestep_index + 1; - let prev_timestep = if prev_timestep_index >= self.timesteps.len() { - 0 // Final step - } else { - self.timesteps[prev_timestep_index] - }; - - // Update step_index for next time - self.step_index = Some(prev_timestep_index); - - // Choose solver based on configuration - match self.config.solver_type { - LCMSolverType::Default => self.step_default(model_output, timestep, prev_timestep, sample), - LCMSolverType::LogRho => self.step_logrho(model_output, timestep, prev_timestep, sample), + pub fn timesteps(&self) -> &[usize] { + self.timesteps.as_slice() } - } - // Default solver (existing logic moved to a separate method) - fn step_default(&self, model_output: &Tensor, timestep: usize, prev_timestep: usize, sample: &Tensor) -> Result { - // Get dtype from sample and use it for consistency - let target_dtype = sample.dtype(); - let device = sample.device(); + /// Ensures interchangeability with schedulers that need to scale the denoising model input + /// depending on the current timestep. + pub fn scale_model_input(&self, sample: Tensor, _timestep: usize) -> Tensor { + sample + } + fn get_scalings_for_boundary_condition_discrete(&self, timestep: usize) -> (f64, f64) { + let sigma_data: f64 = 0.5; + let scaled_timestep = (timestep as f64) * self.config.timestep_scaling; + let sigma_squared = sigma_data.powi(2); + let scaled_timestep_squared = scaled_timestep.powi(2); + let denominator = scaled_timestep_squared + sigma_squared; - println!("Step (Default): current_t={}, prev_t={}", timestep, prev_timestep); + println!( + "Boundary condition for timestep {}, scaled timestep {}", + timestep, scaled_timestep + ); + println!( + " Intermediate values: sigma_data^2: {:.20}, scaled_timestep^2: {:.20}, denominator: {:.20}", + sigma_squared, scaled_timestep_squared, denominator + ); - // Convert model output to match sample dtype - let model_output = model_output.to_dtype(target_dtype)?; + let c_skip = sigma_squared / denominator; + let c_out = scaled_timestep / (scaled_timestep_squared + sigma_squared).sqrt(); - // Get the exact alpha values for this timestep - let alpha_prod_t = if timestep < self.alphas_cumprod.len() { - self.alphas_cumprod[timestep] - } else { - println!("Warning: Timestep {} out of range, using last alpha", timestep); - *self.alphas_cumprod.last().unwrap() - }; + println!( + " Final computed values: c_skip: {:.20}, c_out: {:.20}", + c_skip, c_out + ); - // Get the previous alpha value - let alpha_prod_t_prev = if prev_timestep == 0 { - 1.0 // Final step goes to alpha=1 - } else if prev_timestep < self.alphas_cumprod.len() { - self.alphas_cumprod[prev_timestep] - } else { - println!("Warning: Prev timestep {} out of range", prev_timestep); - *self.alphas_cumprod.last().unwrap() - }; + (c_skip, c_out) + } - // Calculate coefficients for boundary conditions - let (c_skip, c_out) = self.get_scalings_for_boundary_condition_discrete(timestep); + pub fn get_guidance_scale_embedding( + &self, + guidance_scale: f64, + embedding_dim: usize, + device: &Device, + dtype: DType, + ) -> Result { + // Scale by exactly 1000.0 to match diffusers + let w = guidance_scale * 1000.0; + println!( + "Creating guidance embedding with dim={}, scaled value={}", + embedding_dim, w + ); + + // Calculate the half dimension + let half_dim = embedding_dim / 2; + println!("Half dimension: {}", half_dim); + + let log_10000 = (10000.0f32).ln(); + let freq_factor = log_10000 / ((half_dim - 1) as f32); // pre-compute division + + let mut freqs = Vec::with_capacity(half_dim); + for i in 0..half_dim { + // This matches torch.exp(torch.arange(half_dim) * -emb) + let freq = (-freq_factor * i as f32).exp(); + freqs.push(freq); + } + + println!( + "First few frequencies: {:?}", + &freqs.iter().take(5).collect::>() + ); + + // To match PyTorch's w[:, None] * emb[None, :], we need to broadcast manually + // First, create a vector with w multiplied by each frequency + let mut emb_data = Vec::with_capacity(half_dim); + let w_val = w as f32; + for &freq in freqs.iter() { + emb_data.push(w_val * freq); + } + + println!( + "First few scaled frequencies: {:?}", + &emb_data.iter().take(5).collect::>() + ); + + // Create the embedding tensor + let emb = Tensor::new(emb_data.as_slice(), device)?.to_dtype(dtype)?; + println!("Embedding tensor shape: {:?}", emb.shape()); + + // Reshape for correct sin/cos application + let emb = emb.reshape((1, half_dim))?; + println!("Reshaped embedding tensor: {:?}", emb.shape()); + + // Compute sin and cos components + let sin_emb = emb.sin()?; + println!("Sin embedding shape: {:?}", sin_emb.shape()); + let cos_emb = emb.cos()?; + println!("Cos embedding shape: {:?}", cos_emb.shape()); + + // Concatenate sin and cos (along dimension 1) + let result = Tensor::cat(&[sin_emb, cos_emb], 1)?; + println!("After concatenation shape: {:?}", result.shape()); + + // If odd dimension, pad with zero + let result = if embedding_dim % 2 == 1 { + println!("Adding zero padding for odd dimension"); + let zero = Tensor::zeros((1, 1), dtype, device)?; + Tensor::cat(&[result, zero], 1)? + } else { + result + }; + + println!("Final guidance embedding shape: {:?}", result.shape()); + + Ok(result) + } + + pub fn get_timesteps_for_steps(inference_steps: usize, strength: f64) -> Vec { + // These are the timesteps for LCM + let all_timesteps = match inference_steps { + 2 => vec![999, 499], + 3 => vec![999, 679, 339], + 4 => vec![999, 759, 499, 259], + 5 => vec![999, 799, 599, 399, 199], + 6 => vec![999, 839, 679, 499, 339, 179], + 7 => vec![999, 859, 719, 579, 439, 299, 159], + 8 => vec![999, 879, 759, 639, 499, 379, 259, 139], + 9 => vec![999, 899, 779, 679, 559, 459, 339, 239, 119], + 10 => vec![999, 899, 799, 699, 599, 499, 399, 299, 199, 99], + 11 => vec![999, 919, 819, 739, 639, 559, 459, 379, 279, 199, 99], + 12 => vec![999, 919, 839, 759, 679, 599, 499, 419, 339, 259, 179, 99], + 13 => vec![ + 999, 939, 859, 779, 699, 619, 539, 479, 399, 319, 239, 159, 79, + ], + 14 => vec![ + 999, 939, 859, 799, 719, 659, 579, 499, 439, 359, 299, 219, 159, 79, + ], + 15 => vec![ + 999, 939, 879, 799, 739, 679, 599, 539, 479, 399, 339, 279, 199, 139, 79, + ], + 16 => vec![ + 999, 939, 879, 819, 759, 699, 639, 579, 499, 439, 379, 319, 259, 199, 139, 79, + ], + _ => { + // if there's an error return the timesteps for 16 steps + let linspace = match super::utils::linspace(0.0, (999) as f64, inference_steps) { + Ok(linspace) => match linspace.to_vec1::() { + Ok(linspace) => linspace, + Err(_) => { + return vec![ + 999, 939, 879, 819, 759, 699, 639, 579, 499, 439, 379, 319, 259, + 199, 139, 79, + ]; + } + }, + Err(_) => { + return vec![ + 999, 939, 879, 819, 759, 699, 639, 579, 499, 439, 379, 319, 259, 199, + 139, 79, + ]; + } + }; + + let timesteps = linspace.iter().map(|&f| f as usize).rev().collect(); + println!("timesteps: {:?}", timesteps); + timesteps + } + }; + + println!("all_timesteps: {:?}", all_timesteps); + + // Compute `t_start` based on `strength` + let num_train_timesteps = 1000; + let init_timestep = (strength * num_train_timesteps as f64).round() as usize; + let t_start = all_timesteps + .iter() + .position(|&t| t <= init_timestep) + .unwrap_or(0); + + println!( + "get_timesteps: t_start: {} init_timestep: {} strength: {}", + t_start, init_timestep, strength + ); + + // Return only the steps from `t_start` onward + all_timesteps[t_start..].to_vec() + } + + pub fn step( + &self, + model_output: &Tensor, + timestep: usize, + sample: &Tensor, + step_index: usize, + num_inference_steps: usize, + ) -> Result { + let step_index = self.timesteps.iter().position(|&t| t == timestep).unwrap(); + let prev_timestep = if step_index + 1 < self.timesteps.len() { + self.timesteps[step_index + 1] + } else { + 0 + }; + + println!( + "LCM step: timestep={}, prev_timestep={}, step_index={}", + timestep, prev_timestep, step_index + ); + + let alpha_prod_t = self.alphas_cumprod[timestep]; + let alpha_prod_t_prev = if prev_timestep == 0 { + 1.0 + } else { + self.alphas_cumprod[prev_timestep] + }; - // Compute predicted original sample from noise prediction - let pred_original_sample = match self.config.prediction_type { - PredictionType::Epsilon => { let beta_prod_t = 1.0 - alpha_prod_t; - let beta_prod_t_sqrt = beta_prod_t.sqrt(); + let beta_prod_t_prev = 1.0 - alpha_prod_t_prev; - let scaled_noise = model_output.affine(beta_prod_t_sqrt, 0.0)?; - let diff = (sample - &scaled_noise)?; - diff.affine(1.0 / alpha_prod_t.sqrt(), 0.0)? - }, - PredictionType::Sample => model_output.clone(), - PredictionType::VPrediction => { - let sqrt_alpha_prod = alpha_prod_t.sqrt(); - let sqrt_one_minus_alpha_prod = (1.0 - alpha_prod_t).sqrt(); + let (c_skip, c_out) = self.get_scalings_for_boundary_condition_discrete(timestep); - let term1 = sample.affine(sqrt_alpha_prod, 0.0)?; - let term2 = model_output.affine(sqrt_one_minus_alpha_prod, 0.0)?; - (term1 - term2)? - }, - _ => unreachable!(), - }; + let pred_original_sample = match self.config.prediction_type { + PredictionType::Epsilon => { + ((sample - (beta_prod_t.sqrt() * model_output)?)? / alpha_prod_t.sqrt())? + } + PredictionType::Sample => model_output.clone(), + PredictionType::VPrediction => { + ((sample * alpha_prod_t.sqrt())? - (model_output * beta_prod_t.sqrt())?)? + } + }; - // Apply possible clipping - let pred_orig = if self.config.clip_sample { pred_original_sample.clamp(-1f32, 1f32)? } else { pred_original_sample }; + let denoised = ((pred_original_sample * c_out)? + (sample * c_skip)?)?; - // Apply boundary condition - let term1 = pred_orig.affine(c_out, 0.0)?; - let term2 = sample.affine(c_skip, 0.0)?; - let denoised = (&term1 + &term2)?; + let pred_prev_sample = if step_index != self.timesteps.len() - 1 { + println!("Adding noise at intermediate step {}", timestep); + let noise = Tensor::randn_like(&denoised, 0.0, 1.0)?; + ((alpha_prod_t_prev.sqrt() * denoised)? + (beta_prod_t_prev.sqrt() * noise)?)? + } else { + println!("No noise for final timestep {}", timestep); + denoised + }; - // Set up the previous sample - let pred_prev_sample = if prev_timestep == 0 { - denoised.clone() - } else { - let prev_sqrt_alpha = alpha_prod_t_prev.sqrt(); - denoised.affine(prev_sqrt_alpha, 0.0)? - }; + println!( + "Denoised sample shape: {:?}, first few values: {}", + pred_prev_sample.shape(), + pred_prev_sample + .i(0)? + .i(0)? + .narrow(0, 0, 5)? + .narrow(1, 0, 5)? + ); - Ok(pred_prev_sample) - } - - // LogRho solver implementation - fn step_logrho(&self, model_output: &Tensor, timestep: usize, prev_timestep: usize, sample: &Tensor) -> Result { - // Ensure consistent dtype by getting it from the sample - let target_dtype = sample.dtype(); - let device = sample.device(); - - println!("Step (LogRho): current_t={}, prev_t={}", timestep, prev_timestep); - println!(" Using target dtype: {:?}", target_dtype); - - // Convert model output to match sample dtype - let model_output = model_output.to_dtype(target_dtype)?; - println!(" model_output dtype after conversion: {:?}", model_output.dtype()); - - // Get alpha values - let alpha_prod_t = if timestep < self.alphas_cumprod.len() { self.alphas_cumprod[timestep] } else { *self.alphas_cumprod.last().unwrap() }; - - let alpha_prod_t_prev = if prev_timestep == 0 { - 1.0 - } else if prev_timestep < self.alphas_cumprod.len() { - self.alphas_cumprod[prev_timestep] - } else { - *self.alphas_cumprod.last().unwrap() - }; - - // Special case: if we're at the last timestep, return the predicted sample directly - if prev_timestep == 0 { - // Handle final step differently - exact same implementation as in default solver - return self.step_default(&model_output, timestep, prev_timestep, sample); + Ok(pred_prev_sample) } - // Calculate sigma values for LogRho solver with numerical stability - let sigma_t = ((1.0 - alpha_prod_t).max(1e-12)).sqrt(); - let sigma_t_prev = ((1.0 - alpha_prod_t_prev).max(1e-12)).sqrt(); + // pub fn step(&self, model_output: &Tensor, timestep: usize, sample: &Tensor) -> Result { + // let step_index = self.timesteps.iter().position(|&t| t == timestep).unwrap(); - // Calculate key logrho parameters with numerical stability - let logrho_t = sigma_t.ln() - alpha_prod_t.sqrt().max(1e-12).ln(); - let logrho_t_prev = sigma_t_prev.ln() - alpha_prod_t_prev.sqrt().max(1e-12).ln(); + // // Get previous timestep value correctly + // let prev_timestep = if step_index == self.timesteps.len() - 1 { + // 0 // If this is the last step, use timestep 0 + // } else { + // self.timesteps[step_index + 1] + // }; - // Calculate h parameter (step size in log rho space) - let h = logrho_t_prev - logrho_t; - let exp_h = h.exp().clamp(0.01, 5.0); // Constrain to a reasonable range - println!(" LogRho params: h={:.6}, exp(h)={:.6}", h, exp_h); + // println!( + // "LCM step: timestep={}, prev_timestep={}", + // timestep, prev_timestep + // ); - // For epsilon prediction we need to transform the model output - let score = match self.config.prediction_type { - PredictionType::Epsilon => { - // For ε prediction type, score is -σ/√α_t * ε - let scale = -sigma_t / alpha_prod_t.sqrt().max(1e-12); - model_output.affine(scale, 0.0)?.to_dtype(target_dtype)? - }, - PredictionType::VPrediction => { - // model_output is already in the right form - model_output.clone() - }, - PredictionType::Sample => { - // Score = (x_t - √α_t * x_0) / σ_t - let alpha_sqrt = alpha_prod_t.sqrt(); - let scaled_pred = model_output.affine(alpha_sqrt, 0.0)?.to_dtype(target_dtype)?; - let diff = (sample.clone() - scaled_pred)?.to_dtype(target_dtype)?; - diff.affine(1.0 / sigma_t, 0.0)?.to_dtype(target_dtype)? - }, - _ => unreachable!(), - }; + // // Get alpha values for current and previous timestep + // let alpha_prod_t = self.alphas_cumprod[timestep]; + // let alpha_prod_t_prev = if prev_timestep == 0 { + // 1.0 // Use 1.0 for the final step + // } else { + // self.alphas_cumprod[prev_timestep] + // }; - println!(" score dtype: {:?}", score.dtype()); + // println!( + // "LCM scheduler step: timestep={}, step_index={}, sigma={}", + // timestep, + // step_index, + // alpha_prod_t.sqrt() + // ); - // Calculate d term: d = x_t + σ_t^2/√α_t * score - let scale_d = sigma_t.powi(2) / alpha_prod_t.sqrt().max(1e-12); - let score_scaled = score.affine(scale_d, 0.0)?.to_dtype(target_dtype)?; - let d = (sample + score_scaled)?.to_dtype(target_dtype)?; - println!(" d dtype: {:?}", d.dtype()); + // let beta_prod_t = 1.0 - alpha_prod_t; + // let beta_prod_t_prev = 1.0 - alpha_prod_t_prev; - // Apply the main update formula directly: - // x_prev = exp_h * sample + sigma_prev * (1 - exp_h) * d + // // Get scalings for boundary conditions + // let (c_skip, c_out) = self.get_scalings_for_boundary_condition_discrete(timestep); - // First part: exp_h * sample - let term1 = sample.affine(exp_h, 0.0)?.to_dtype(target_dtype)?; - println!(" term1 dtype: {:?}", term1.dtype()); + // // Compute predicted original sample x_0 + // let mut pred_original_sample = match self.config.prediction_type { + // PredictionType::Epsilon => { + // let predicted_original_sample = + // ((sample - (beta_prod_t.sqrt() * model_output)?)? / alpha_prod_t.sqrt())?; + // predicted_original_sample + // } + // PredictionType::Sample => model_output.clone(), + // PredictionType::VPrediction => { + // let predicted_original_sample = + // ((sample * alpha_prod_t.sqrt())? - (model_output * beta_prod_t.sqrt())?)?; + // predicted_original_sample + // } + // }; - // Second part: sigma_prev * (1 - exp_h) * d - let term2_factor = sigma_t_prev * (1.0 - exp_h); - println!(" term2_factor: {:.6}", term2_factor); + // if self.config.clip_sample { + // pred_original_sample = pred_original_sample.clamp(-1f32, 1f32)?; + // } - let term2 = d.affine(term2_factor, 0.0)?.to_dtype(target_dtype)?; - println!(" term2 dtype: {:?}", term2.dtype()); + // println!("boundary: Pred original sample first few values: {}", + // pred_original_sample.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); - // Add the terms together - let pred_prev_sample = (&term1 + &term2)?.to_dtype(target_dtype)?; - println!(" pred_prev_sample dtype: {:?}", pred_prev_sample.dtype()); + // // Denoise model output using boundary conditions + // let denoised = ((pred_original_sample * c_out)? + (sample * c_skip)?)?; - Ok(pred_prev_sample) - } + // println!("boundary: Denoised first few values: {}", + // denoised.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?); - pub fn add_noise(&self, original_samples: &Tensor, noise: Tensor, timestep: usize) -> Result { - // Get the correct alpha value - let alpha_cumprod = self.alphas_cumprod[timestep]; + // // LCM doesn't add noise during inference + // let pred_prev_sample = if prev_timestep >= self.alphas_cumprod.len() { + // denoised + // } else { + // let scale = (Tensor::zeros_like(&denoised)? + alpha_prod_t_prev.sqrt())?; + // (&denoised).mul(&scale)? + // }; - println!("Adding noise for timestep={}, alpha={:.6}, sqrt(alpha)={:.6}", timestep, alpha_cumprod, alpha_cumprod.sqrt()); - println!(" original_samples: shape={:?}, dtype={:?}", original_samples.shape(), original_samples.dtype()); - println!(" noise: shape={:?}, dtype={:?}", noise.shape(), noise.dtype()); + // // let pred_prev_sample = pred_prev_sample.clamp(-1f32, 1f32)?; - // Keep operations in original_samples' dtype for now - let target_dtype = original_samples.dtype(); - let noise = noise.to_dtype(target_dtype)?; + // println!( + // "LCM scheduler pred_original shape: {:?}", + // pred_prev_sample.shape() + // ); + // println!( + // "LCM scheduler pred_original first few values: {}", + // pred_prev_sample + // .i(0)? + // .i(0)? + // .narrow(0, 0, 5)? + // .narrow(1, 0, 5)? + // ); - // Calculate coefficients - let sqrt_alpha = alpha_cumprod.sqrt(); - let sqrt_one_minus_alpha = (1.0 - alpha_cumprod).sqrt(); + // Ok(pred_prev_sample) + // } - // Apply noise formula - let scaled_samples = original_samples.affine(sqrt_alpha, 0.0)?; - let scaled_noise = noise.affine(sqrt_one_minus_alpha, 0.0)?; + pub fn add_noise(&self, original: &Tensor, noise: Tensor, timestep: usize) -> Result { + let timestep = if timestep >= self.alphas_cumprod.len() { + self.alphas_cumprod.len() - 1 // Ensure valid range + } else { + timestep + }; + let sqrt_alpha_prod = self.alphas_cumprod[timestep].sqrt(); + let sqrt_one_minus_alpha_prod = (1.0 - self.alphas_cumprod[timestep] + 1e-6).sqrt(); // Prevent zero issue - println!(" scaled_samples: shape={:?}, dtype={:?}", scaled_samples.shape(), scaled_samples.dtype()); - println!(" scaled_noise: shape={:?}, dtype={:?}", scaled_noise.shape(), scaled_noise.dtype()); - - // Add scaled noise - should maintain dtype - let result = scaled_samples.add(&scaled_noise)?; - - println!(" result: shape={:?}, dtype={:?}", result.shape(), result.dtype()); - - Ok(result) - } - - pub fn init_noise_sigma(&self) -> f64 { - self.init_noise_sigma - } - - pub fn alphas_cumprod(&self) -> &[f64] { - &self.alphas_cumprod - } - - pub fn log_config(&self) { - println!("LCM Scheduler Configuration:"); - println!(" beta_start: {}", self.config.beta_start); - println!(" beta_end: {}", self.config.beta_end); - println!(" beta_schedule: {:?}", self.config.beta_schedule); - println!(" train_timesteps: {}", self.config.train_timesteps); - println!(" prediction_type: {:?}", self.config.prediction_type); - println!(" clip_sample: {}", self.config.clip_sample); - println!(" variance_type: {:?}", self.config.variance_type); - println!(" timesteps: {:?}", self.timesteps); - // Print the first few alphas_cumprod values - let num_to_show = std::cmp::min(10, self.alphas_cumprod.len()); - println!(" First {} alphas_cumprod: {:?}", num_to_show, self.alphas_cumprod.iter().take(num_to_show).collect::>()); - } - - /// Creates embeddings from guidance scale values, similar to timestep embeddings - /// Based on diffusers implementation - pub fn get_guidance_scale_embedding(&self, guidance_scale: f64, embedding_dim: usize, device: &Device, dtype: DType) -> Result { - // Scale by exactly 1000.0 to match diffusers - let w = guidance_scale * 1000.0; - println!("Creating guidance embedding with dim={}, scaled value={}", embedding_dim, w); - - // Create embedding vector directly to avoid tensor operations - let mut embedding = Vec::with_capacity(embedding_dim); - let half_dim = embedding_dim / 2; - - // Compute the position frequencies - for i in 0..half_dim { - let freq = (i as f32) * (-((10000.0f32).ln()) / ((half_dim - 1) as f32)); - let freq = freq.exp(); - - // Apply the guidance scale (w value) - let value = w as f32 * freq; - - // Add sin and cos values - embedding.push(value.sin()); - embedding.push(value.cos()); + (original * sqrt_alpha_prod)? + (noise * sqrt_one_minus_alpha_prod)? } - // If odd dimension, pad with zero - if embedding.len() < embedding_dim { - embedding.push(0.0); + // pub fn add_noise( + // &self, + // original_samples: &Tensor, + // noise: Tensor, + // timestep: usize, + // ) -> Result { + // // Get the correct alpha value + // let alpha_cumprod = self.alphas_cumprod[timestep]; + + // // Print the alpha value to debug + // println!( + // "Adding noise with timestep={}, alpha={}, sqrt(alpha)={}", + // timestep, + // alpha_cumprod, + // alpha_cumprod.sqrt() + // ); + + // // Correctly scale the original samples and noise using scalar multiplication + // // let scaled_samples = (original_samples * alpha_cumprod.sqrt())?; + // // let scaled_noise = (noise * (1.0 - alpha_cumprod).sqrt())?; + + // // scaled_samples + scaled_noise + // let clamped_strength = 0.6; + // let scaled_samples = (original_samples * alpha_cumprod.sqrt())?; + // let scaled_noise = ((noise * (1.0 - alpha_cumprod).sqrt())? * clamped_strength)?; + + // scaled_samples + scaled_noise + // } + + pub fn init_noise_sigma(&self) -> f64 { + self.init_noise_sigma } - // Ensure we have the correct length - assert_eq!(embedding.len(), embedding_dim, "Embedding length mismatch"); - - // Create tensor from the embedding vector - let tensor = Tensor::new(embedding.as_slice(), device)?.to_dtype(dtype)?; - - // Reshape to match expected [1, embedding_dim] shape - let result = tensor.reshape((1, embedding_dim))?; - - println!("Final guidance embedding shape: {:?}", result.shape()); - - Ok(result) - } - - /// Calculate timesteps based on LCM's specific formula - /// This is now only used for backward compatibility or custom timesteps - pub fn get_timesteps_for_steps(inference_steps: usize) -> Vec { - match inference_steps { - // 4 => vec![999, 499, 259, 0], - 4 => vec![499, 259], - 8 => vec![999, 879, 759, 639, 499, 379, 259, 139, 0], - _ => { - let intervals = (999 / (inference_steps - 1)) as usize; - let mut timesteps = Vec::with_capacity(inference_steps); - - // First step always starts at 999 - timesteps.push(999); - - // Generate intermediate steps - for i in 1..inference_steps { - // The pattern looks approximately linear with a step size determined by the number of steps - let ts = 999 - i * intervals; - timesteps.push(ts); - } - - // Last step is always 0 - if let Some(last) = timesteps.last_mut() { - if *last != 0 { - *last = 0; - } - } - - timesteps - }, + pub fn alphas_cumprod(&self) -> &[f64] { + &self.alphas_cumprod } - } - - /// Get the appropriate starting timestep based on strength - pub fn get_timestep_for_strength(&self, strength: f64) -> (usize, f64) { - // Convert strength to a timestep index - let num_steps = self.timesteps.len(); - let strength_adjusted = (strength * num_steps as f64).floor() as usize; - let start_idx = num_steps.saturating_sub(strength_adjusted + 1); - - // Get the timestep at this index - let timestep = if start_idx < self.timesteps.len() { self.timesteps[start_idx] } else { *self.timesteps.first().unwrap_or(&999) }; - - // Get the corresponding alpha - let alpha = if timestep < self.alphas_cumprod.len() { self.alphas_cumprod[timestep] } else { *self.alphas_cumprod.last().unwrap_or(&0.0) }; - - println!("For strength {:.2}, using timestep {} (index {}), alpha={:.6}", strength, timestep, start_idx, alpha); - - (timestep, alpha) - } - - /// Add noise for img2img based on strength - pub fn add_noise_for_img2img(&mut self, original_samples: &Tensor, noise: &Tensor, strength: f64) -> Result<(Tensor, usize)> { - // Get starting timestep and alpha - let (timestep, alpha) = self.get_timestep_for_strength(strength); - - // Set begin_index based on this timestep - let start_idx = self.index_for_timestep(timestep)?; - self.set_begin_index(start_idx); - - println!("Adding img2img noise with strength={:.4}, timestep={}, alpha={:.6}", strength, timestep, alpha); - println!(" original_samples: shape={:?}, dtype={:?}", original_samples.shape(), original_samples.dtype()); - println!(" noise: shape={:?}, dtype={:?}", noise.shape(), noise.dtype()); - - // Keep operations in original_samples' dtype for now - let target_dtype = original_samples.dtype(); - let noise_converted = noise.to_dtype(target_dtype)?; - - // Calculate coefficients - let sqrt_alpha = alpha.sqrt(); - let sqrt_one_minus_alpha = (1.0 - alpha).sqrt(); - - println!(" Using scalar multiplication with target dtype={:?}", target_dtype); - - // Apply noise formula - let scaled_samples = original_samples.affine(sqrt_alpha, 0.0)?; - let scaled_noise = noise_converted.affine(sqrt_one_minus_alpha, 0.0)?; - - println!(" scaled_samples: shape={:?}, dtype={:?}", scaled_samples.shape(), scaled_samples.dtype()); - println!(" scaled_noise: shape={:?}, dtype={:?}", scaled_noise.shape(), scaled_noise.dtype()); - - // Add scaled noise - should maintain dtype - let noised = scaled_samples.add(&scaled_noise)?; - - println!(" noised: shape={:?}, dtype={:?}", noised.shape(), noised.dtype()); - - // Return noised image and the timestep to start from - Ok((noised, timestep)) - } - - pub fn index_for_timestep(&self, timestep: usize) -> Result { - // Collect all indices where timestep matches - let indices: Vec = self.timesteps.iter().enumerate().filter_map(|(idx, &ts)| if ts == timestep { Some(idx) } else { None }).collect(); - - if indices.is_empty() { - return Err(candle::Error::Msg(format!("Timestep {} not found in scheduler timesteps", timestep))); - } - - // Take the second index if there are multiple matches, otherwise take the first - let pos = if indices.len() > 1 { 1 } else { 0 }; - Ok(indices[pos]) - } - - pub fn set_begin_index(&mut self, begin_index: usize) { - self.begin_index = Some(begin_index); - println!("Set begin_index to {}", begin_index); - } - - pub fn init_step_index(&mut self, timestep: usize) -> Result { - self.step_index = if let Some(begin_index) = self.begin_index { - // If begin_index is set, use it directly - Some(begin_index) - } else { - // Otherwise find the index based on the timestep - // Find the position of timestep in self.timesteps - let pos = self.timesteps.iter().position(|&t| t == timestep).ok_or_else(|| candle::Error::Msg(format!("Timestep {} not found in timesteps", timestep)))?; - Some(pos) - }; - - Ok(self.step_index.unwrap()) - } - - // Updated helper function to match diffusers' get_timesteps logic exactly - pub fn retrieve_timesteps(scheduler: &mut LCMScheduler, num_inference_steps: usize, strength: Option, custom_timesteps: Option>) -> Result> { - // 1. When custom timesteps are provided, use them directly - if let Some(custom_ts) = custom_timesteps { - println!("Using custom timesteps: {:?}", custom_ts); - return Ok(custom_ts); - } - - // 2. Get timesteps from scheduler - let mut timesteps = scheduler.timesteps().to_vec(); - - // 3. For img2img, adjust timesteps based on strength - if let Some(strength) = strength { - if strength < 1.0 { - // Match diffusers logic exactly: - // init_timestep = min(int(num_inference_steps * strength), num_inference_steps) - let init_timestep = ((num_inference_steps as f64 * strength).floor() as usize).min(num_inference_steps); - - // t_start = max(num_inference_steps - init_timestep, 0) - let t_start = num_inference_steps.saturating_sub(init_timestep); - - // In diffusers, order is typically 1 for LCM, so we'll simplify here - let scheduler_order = 1; - let start_idx = t_start * scheduler_order; - - // Slice timesteps from the calculated start position - if start_idx < timesteps.len() { - println!("Adjusting timesteps for strength {}: starting at index {} (init_timestep={})", strength, start_idx, init_timestep); - timesteps = timesteps[start_idx..].to_vec(); - - // Set begin_index in the scheduler - scheduler.set_begin_index(start_idx); - } - - // Return the actual number of inference steps we'll perform - let actual_steps = num_inference_steps.saturating_sub(t_start); - println!("Will perform {} inference steps due to strength {}", actual_steps, strength); - } - } - - println!("Final timesteps for inference: {:?}", timesteps); - Ok(timesteps) - } - - // Add this method to help initialize latents from an image (for img2img) - pub fn prepare_img2img_latents( - &mut self, - image_tensor: &Tensor, // Encoded image from VAE - timestep: usize, - batch_size: usize, - num_images_per_prompt: usize, - dtype: DType, - device: &Device, - ) -> Result { - // Store the dtype for consistent operations - self.dtype = dtype; - - let latent_dim = image_tensor.dim(2)? / 8; // Assuming standard VAE downsampling - - println!("Preparing img2img latents for timestep={}", timestep); - println!(" image_tensor: shape={:?}, dtype={:?}", image_tensor.shape(), image_tensor.dtype()); - - // Create noise tensor using the same approach as in main.rs - let noise_shape = Shape::from((batch_size * num_images_per_prompt, 4, latent_dim, latent_dim)); - let noise = Tensor::randn(0f32, 1f32, noise_shape, device)?.to_dtype(dtype)?; - println!(" noise: shape={:?}, dtype={:?}", noise.shape(), noise.dtype()); - - // Encode image to latents - let encoded_latents = image_tensor.clone(); // Assuming already encoded - - // Scale to match VAE expected scale - use affine for scalar multiplication - println!(" Using affine for VAE scaling by 0.18215"); - let scaled_latents = encoded_latents.affine(0.18215, 0.0)?; - println!(" scaled_latents: shape={:?}, dtype={:?}", scaled_latents.shape(), scaled_latents.dtype()); - - // Add noise based on timestep (this handles the "strength" parameter) - let noised_latents = self.add_noise(&scaled_latents, noise, timestep)?; - println!(" noised_latents: shape={:?}, dtype={:?}", noised_latents.shape(), noised_latents.dtype()); - - Ok(noised_latents) - } - - /// Apply classifier-free guidance to model output - pub fn apply_guidance(&self, model_output_uncond: &Tensor, model_output_text: &Tensor, guidance_scale: f64) -> Result { - // Skip guidance if scale is 1.0 (or very close to it) - if (guidance_scale - 1.0).abs() < 1e-5 { - return Ok(model_output_text.clone()); - } - - println!("Applying guidance with scale={:.4}", guidance_scale); - println!(" model_output_uncond: shape={:?}, dtype={:?}", model_output_uncond.shape(), model_output_uncond.dtype()); - println!(" model_output_text: shape={:?}, dtype={:?}", model_output_text.shape(), model_output_text.dtype()); - - // Apply the classifier-free guidance formula: - // output = uncond + guidance_scale * (text - uncond) - - // First calculate (text - uncond) - let guidance_diff = (model_output_text - model_output_uncond)?; - println!(" guidance_diff: shape={:?}, dtype={:?}", guidance_diff.shape(), guidance_diff.dtype()); - - // Then scale by guidance_scale using affine for scalar multiplication - let guidance_factor = guidance_scale - 1.0; - println!(" Using affine for scaling by {:.4}", guidance_factor); - let scaled_diff = guidance_diff.affine(guidance_factor, 0.0)?; - println!(" scaled_diff: shape={:?}, dtype={:?}", scaled_diff.shape(), scaled_diff.dtype()); - - // Finally add back to uncond: uncond + guidance_scale * (text - uncond) - let guided_output = (model_output_uncond + &scaled_diff)?; - println!(" guided_output: shape={:?}, dtype={:?}", guided_output.shape(), guided_output.dtype()); - - Ok(guided_output) - } - - // Add this helper method to manage dtype consistency across the pipeline - pub fn ensure_dtype_consistency(&mut self, input_tensor: &Tensor) -> Result<()> { - // Store the current tensor's dtype for reference - self.dtype = input_tensor.dtype(); - println!("Setting LCM scheduler dtype to match input: {:?}", self.dtype); - Ok(()) - } - - // Add a new method to prepare latents with explicit dtype control - pub fn prepare_latents(&mut self, batch_size: usize, height: usize, width: usize, num_channels_latents: usize, dtype: DType, device: &Device, generator: Option) -> Result { - // Store the requested dtype - self.dtype = dtype; - - // Calculate latent dimensions - let latent_height = height / 8; - let latent_width = width / 8; - - // Create random latents - let shape = Shape::from((batch_size, num_channels_latents, latent_height, latent_width)); - - // Note: Candle doesn't have a direct way to set random seeds for tensors - // So we'll just use regular random generation for now - let latents = if let Some(seed) = generator { - println!("Note: Using random latents (seed not supported in this implementation)"); - // In a real implementation, you would want to set the random seed here - Tensor::randn(0f32, 1f32, shape, device)? - } else { - println!("Generating random latents"); - Tensor::randn(0f32, 1f32, shape, device)? - }; - - // Convert to the requested dtype - let latents = latents.to_dtype(dtype)?; - println!("Generated latents: shape={:?}, dtype={:?}", latents.shape(), latents.dtype()); - - // Scale by the init_noise_sigma - let scaled_latents = latents.affine(self.init_noise_sigma, 0.0)?; - - Ok(scaled_latents) - } - - // Add this method to maintain dtype consistency across operations - pub fn match_dtype(&self, tensor: &Tensor, dtype: Option) -> Result { - match dtype { - Some(d) => tensor.to_dtype(d), - None => Ok(tensor.clone()), - } - } - - // Add this public getter for the dtype field - pub fn dtype(&self) -> DType { - self.dtype - } } diff --git a/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/mod.rs b/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/mod.rs index d174da34d2..fe74fcf9d9 100644 --- a/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/mod.rs +++ b/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/mod.rs @@ -84,6 +84,8 @@ impl StableDiffusionConfig { }; // https://huggingface.co/runwayml/stable-diffusion-v1-5/blob/main/unet/config.json let unet = unet_2d::UNet2DConditionModelConfig { + time_embed_dim: None, + time_cond_proj_dim: None, blocks: vec![ bc(320, Some(1), 8), bc(640, Some(1), 8), @@ -153,6 +155,8 @@ impl StableDiffusionConfig { }; // https://huggingface.co/stabilityai/stable-diffusion-2-1/blob/main/unet/config.json let unet = unet_2d::UNet2DConditionModelConfig { + time_cond_proj_dim: None, + time_embed_dim: None, blocks: vec![ bc(320, Some(1), 5), bc(640, Some(1), 10), @@ -237,6 +241,8 @@ impl StableDiffusionConfig { }; // https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/unet/config.json let unet = unet_2d::UNet2DConditionModelConfig { + time_cond_proj_dim: None, + time_embed_dim: None, blocks: vec![ bc(320, None, 5), bc(640, Some(2), 10), @@ -306,6 +312,8 @@ impl StableDiffusionConfig { }; // https://huggingface.co/stabilityai/sdxl-turbo/blob/main/unet/config.json let unet = unet_2d::UNet2DConditionModelConfig { + time_cond_proj_dim: None, + time_embed_dim: None, blocks: vec![ bc(320, None, 5), bc(640, Some(2), 10), @@ -405,6 +413,8 @@ impl StableDiffusionConfig { }; // https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/unet/config.json let unet = unet_2d::UNet2DConditionModelConfig { + time_cond_proj_dim: None, + time_embed_dim: None, blocks: vec![ bc(320, None, 5), bc(640, Some(2), 10), diff --git a/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/unet_2d.rs b/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/unet_2d.rs index 6215b5efda..421fbf269c 100644 --- a/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/unet_2d.rs +++ b/crates/vendor/candle/candle-transformers/src/models/stable_diffusion/unet_2d.rs @@ -10,6 +10,10 @@ use candle_nn as nn; use candle_nn::Module; use std::collections::HashMap; +// fn save_tensor_as_numpy(tensor: &Tensor, filename: &str) -> Result<()> { +// // Use Candle's built-in NPY serialization +// tensor.write_npy(filename).map_err(|e| anyhow::anyhow!("{}", e)) +// } #[derive(Debug, Clone, Copy)] pub struct BlockConfig { pub out_channels: usize, @@ -33,11 +37,13 @@ pub struct UNet2DConditionModelConfig { pub cross_attention_dim: usize, pub sliced_attention_size: Option, pub use_linear_projection: bool, + pub time_cond_proj_dim: Option, + pub time_embed_dim: Option, } impl Default for UNet2DConditionModelConfig { fn default() -> Self { - Self { center_input_sample: false, flip_sin_to_cos: true, freq_shift: 0., blocks: vec![BlockConfig { out_channels: 320, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 640, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 1280, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 1280, use_cross_attn: None, attention_head_dim: 8 }], layers_per_block: 2, downsample_padding: 1, mid_block_scale_factor: 1., norm_num_groups: 32, norm_eps: 1e-5, cross_attention_dim: 1280, sliced_attention_size: None, use_linear_projection: false } + Self { center_input_sample: false, flip_sin_to_cos: true, freq_shift: 0., blocks: vec![BlockConfig { out_channels: 320, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 640, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 1280, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 1280, use_cross_attn: None, attention_head_dim: 8 }], layers_per_block: 2, downsample_padding: 1, mid_block_scale_factor: 1., norm_num_groups: 32, norm_eps: 1e-5, cross_attention_dim: 1280, sliced_attention_size: None, use_linear_projection: false, time_cond_proj_dim: Some(256), time_embed_dim: None } } } @@ -73,8 +79,16 @@ impl UNet2DConditionModel { let b_channels = config.blocks[0].out_channels; let bl_channels = config.blocks.last().unwrap().out_channels; let bl_attention_head_dim = config.blocks.last().unwrap().attention_head_dim; - let time_embed_dim = b_channels * 4; + let time_embed_dim = config.time_embed_dim.unwrap_or(b_channels * 4); let conv_cfg = nn::Conv2dConfig { padding: 1, ..Default::default() }; + + // Log important configuration details + println!("Creating UNet2DConditionModel with {} blocks", n_blocks); + if let Some(proj_dim) = config.time_cond_proj_dim { + println!("UNet configured with time_cond_proj_dim: {}", proj_dim); + println!("This means the model expects guidance embeddings of dimension {}", proj_dim); + } + let conv_in = conv2d(in_channels, b_channels, 3, conv_cfg, vs.pp("conv_in"))?; let time_proj = Timesteps::new(b_channels, config.flip_sin_to_cos, config.freq_shift); @@ -241,18 +255,138 @@ impl UNet2DConditionModel { // Add guidance embedding if provided if let Some(guidance_emb) = guidance_embedding { - // Print shapes for debugging - println!("Guidance embedding shape: {:?}", guidance_emb.shape()); - println!("Time embedding shape: {:?}", time_emb.shape()); + // Get dimensions of both embeddings + let (batch_size, time_emb_dim) = time_emb.dims2()?; + let (guidance_batch, guidance_emb_dim) = guidance_emb.dims2()?; - // Get the batch size and embedding dimension - let (batch_size, emb_dim) = time_emb.dims2()?; + println!("==== Guidance Embedding Integration ===="); + println!("Time embedding dims: [{}, {}]", batch_size, time_emb_dim); + println!("Guidance embedding dims: [{}, {}]", guidance_batch, guidance_emb_dim); - // Reshape guidance embedding to match time embedding dimensions - let guidance_emb = guidance_emb.reshape(&[1, emb_dim])?.expand(&[batch_size, emb_dim])?; + // Use time_cond_proj_dim from config to determine expected guidance embedding dimension + let expected_guidance_dim = self.config.time_cond_proj_dim.unwrap_or(time_emb_dim); + println!("Expected guidance dimension from config: {}", expected_guidance_dim); + + // Check if dimensions match the expected projection dimension + let processed_guidance_emb = if guidance_emb_dim != time_emb_dim { + // Convert guidance_emb to f32 for calculations + let guidance_emb_f32 = guidance_emb.to_dtype(DType::F32)?; + println!("Converting guidance embedding to F32 for processing"); + + // If the guidance embedding dimension matches the expected projection dimension, + // we need to project it to match the time embedding dimension + if guidance_emb_dim == expected_guidance_dim { + println!("Projecting guidance embedding from {} to {}", guidance_emb_dim, time_emb_dim); + + // Use linear interpolation + let src_positions: Vec = (0..guidance_emb_dim).map(|i| i as f32 / (guidance_emb_dim - 1) as f32).collect(); + let dst_positions: Vec = (0..time_emb_dim).map(|i| i as f32 / (time_emb_dim - 1) as f32).collect(); + + // Get guidance embedding data (now f32) + let guidance_data = guidance_emb_f32.reshape((guidance_emb_dim,))?.to_vec1::()?; + println!("Reshaping guidance data to vector of length {}", guidance_data.len()); + + // Perform linear interpolation + let mut interpolated_data = Vec::with_capacity(time_emb_dim); + for &t in &dst_positions { + // Find the two source positions that surround the target position + let mut i = 0; + while i < src_positions.len() - 1 && src_positions[i + 1] < t { + i += 1; + } + + if i == src_positions.len() - 1 { + // At or beyond the last position + interpolated_data.push(guidance_data[i]); + } else if t <= src_positions[0] { + // At or before the first position + interpolated_data.push(guidance_data[0]); + } else { + // Linear interpolation between two points + let t_norm = (t - src_positions[i]) / (src_positions[i + 1] - src_positions[i]); + let value = guidance_data[i] * (1.0 - t_norm) + guidance_data[i + 1] * t_norm; + interpolated_data.push(value); + } + } + + println!("Created interpolated data of length {}", interpolated_data.len()); + + // Create tensor and convert to time_emb's dtype + let tensor = Tensor::from_vec(interpolated_data, (1, time_emb_dim), device)?; + let result = tensor.to_dtype(time_emb.dtype())?; + println!("Final projected guidance embedding shape: {:?}", result.shape()); + result + } else { + // Unusual case - dimensions don't match expected pattern + println!("Warning: Guidance embedding dimension ({}) doesn't match expected projection dimension ({})", guidance_emb_dim, expected_guidance_dim); + + // Fallback to generic interpolation + let guidance_data = guidance_emb_f32.reshape((guidance_emb_dim,))?.to_vec1::()?; + + let src_positions: Vec = (0..guidance_emb_dim).map(|i| i as f32 / (guidance_emb_dim - 1) as f32).collect(); + let dst_positions: Vec = (0..time_emb_dim).map(|i| i as f32 / (time_emb_dim - 1) as f32).collect(); + + let mut interpolated_data = Vec::with_capacity(time_emb_dim); + for &t in &dst_positions { + let mut i = 0; + while i < src_positions.len() - 1 && src_positions[i + 1] < t { + i += 1; + } + + if i == src_positions.len() - 1 { + interpolated_data.push(guidance_data[i]); + } else if t <= src_positions[0] { + interpolated_data.push(guidance_data[0]); + } else { + let t_norm = (t - src_positions[i]) / (src_positions[i + 1] - src_positions[i]); + let value = guidance_data[i] * (1.0 - t_norm) + guidance_data[i + 1] * t_norm; + interpolated_data.push(value); + } + } + + println!("Created interpolated data of length {}", interpolated_data.len()); + let tensor = Tensor::from_vec(interpolated_data, (1, time_emb_dim), device)?; + let result = tensor.to_dtype(time_emb.dtype())?; + println!("Final fallback guidance embedding shape: {:?}", result.shape()); + result + } + } else { + // Dimensions match, no processing needed + println!("Guidance dimension already matches time embedding dimension, no interpolation needed"); + let result = guidance_emb.reshape((1, time_emb_dim))?.to_dtype(time_emb.dtype())?; + println!("Reshaped guidance embedding shape: {:?}", result.shape()); + result + }; + + // Expand to match batch size if needed + let expanded_guidance = processed_guidance_emb.broadcast_as(time_emb.shape())?; + println!("Expanded guidance embedding shape: {:?}", expanded_guidance.shape()); + + // Save the processed and expanded guidance embeddings to disk if requested + if let Ok(save_dir) = std::env::var("CANDLE_SAVE_TENSORS_DIR") { + // Save pre-expansion (original projection) + let proc_path = format!("{}/unet_processed_guidance_embedding.npy", save_dir); + println!("Saving processed guidance embedding to {}", proc_path); + if let Err(e) = processed_guidance_emb.write_npy(&proc_path) { + println!("Error saving processed guidance embedding: {}", e); + } else { + println!("Successfully saved processed guidance embedding to {}", proc_path); + } + + // Save post-expansion (final tensor used for addition to time_emb) + let exp_path = format!("{}/unet_expanded_guidance_embedding.npy", save_dir); + println!("Saving expanded guidance embedding to {}", exp_path); + if let Err(e) = expanded_guidance.write_npy(&exp_path) { + println!("Error saving expanded guidance embedding: {}", e); + } else { + println!("Successfully saved expanded guidance embedding to {}", exp_path); + } + } // Add to time embeddings - time_emb = (time_emb + guidance_emb)?; + time_emb = (time_emb + expanded_guidance)?; + println!("Final time embedding with guidance shape: {:?}", time_emb.shape()); + println!("==== End Guidance Integration ===="); } // 2. pre-process