add CoreML NPU support for Apple Silicon, include Flash Attention toggle, and improve process management for startup scripts.

This commit is contained in:
orailnoor committed 2026-06-14 09:56:16 +05:30
1 parent b55e62be7d
commit 74b0c3ba46
10 files changed
+515 -168

No files matched your search

+50 -70
View File
@@ -340,75 +340,7 @@ function App() {
}
}, [activeModel]);
const activeTabContent = useMemo(() => {
switch (activeTab) {
case "generator":
return (
<Generator
prompt={prompt}
setPrompt={setPrompt}
negativePrompt={negativePrompt}
setNegativePrompt={setNegativePrompt}
constraints={constraints}
setConstraints={setConstraints}
activeModel={activeModel}
generatedImages={generatedImages}
setGeneratedImages={setGeneratedImages}
isGenerating={isGenerating}
setIsGenerating={setIsGenerating}
generationProgress={generationProgress}
setGenerationProgress={setGenerationProgress}
setActiveTab={setActiveTab}
showAlert={showAlert}
showConfirm={showConfirm}
/>
);
case "models":
return (
<ModelManager
activeModel={activeModel}
setActiveModel={setActiveModel}
serverRunning={serverRunning}
setServerRunning={setServerRunning}
constraints={constraints}
backendOptions={backendOptions}
showAlert={showAlert}
showConfirm={showConfirm}
/>
);
case "constraints":
return (
<ImageConstraints
constraints={constraints}
setConstraints={setConstraints}
activeModel={activeModel}
specs={specs}
backendOptions={backendOptions}
serverRunning={serverRunning}
setServerRunning={setServerRunning}
setActiveModel={setActiveModel}
showAlert={showAlert}
showConfirm={showConfirm}
/>
);
default:
return null;
}
}, [
activeTab,
prompt,
negativePrompt,
constraints,
activeModel,
generatedImages,
isGenerating,
generationProgress,
serverRunning,
specs,
backendOptions,
showAlert,
showConfirm,
]);
// Tab contents are kept mounted to preserve loading/generation state when switching tabs
const sidebarContent = useMemo(() => (
<Sidebar
@@ -507,7 +439,55 @@ function App() {
)}
{/* Dynamic Workspace Container */}
{activeTabContent}
<div style={{ display: activeTab === "generator" ? "flex" : "none", flex: 1, flexDirection: "column", overflow: "hidden" }}>
<Generator
prompt={prompt}
setPrompt={setPrompt}
negativePrompt={negativePrompt}
setNegativePrompt={setNegativePrompt}
constraints={constraints}
setConstraints={setConstraints}
activeModel={activeModel}
generatedImages={generatedImages}
setGeneratedImages={setGeneratedImages}
isGenerating={isGenerating}
setIsGenerating={setIsGenerating}
generationProgress={generationProgress}
setGenerationProgress={setGenerationProgress}
setActiveTab={setActiveTab}
showAlert={showAlert}
showConfirm={showConfirm}
/>
</div>
<div style={{ display: activeTab === "models" ? "flex" : "none", flex: 1, flexDirection: "column", overflow: "hidden" }}>
<ModelManager
activeModel={activeModel}
setActiveModel={setActiveModel}
serverRunning={serverRunning}
setServerRunning={setServerRunning}
constraints={constraints}
backendOptions={backendOptions}
showAlert={showAlert}
showConfirm={showConfirm}
activeTab={activeTab}
/>
</div>
<div style={{ display: activeTab === "constraints" ? "flex" : "none", flex: 1, flexDirection: "column", overflow: "hidden" }}>
<ImageConstraints
constraints={constraints}
setConstraints={setConstraints}
activeModel={activeModel}
specs={specs}
backendOptions={backendOptions}
serverRunning={serverRunning}
setServerRunning={setServerRunning}
setActiveModel={setActiveModel}
showAlert={showAlert}
showConfirm={showConfirm}
/>
</div>
</div>
{dialog && (
+63 -56
View File
@@ -60,6 +60,12 @@ function Generator({
const abortControllerRef = useRef(null);
const hasRealGenerationStepRef = useRef(false);
React.useEffect(() => {
if (constraints.backendType === "apple-npu" || constraints.backendType === "openvino-npu") {
setBaseImage(null);
}
}, [constraints.backendType]);
const handleBaseImageSelect = (e) => {
const file = e.target.files?.[0];
e.target.value = "";
@@ -114,10 +120,9 @@ function Generator({
try {
console.log("Settings out of sync, restarting backend...");
await startServer(activeModel, constraints);
let isReady = false;
let isReady = false;
let crashError = null;
const maxStartupPolls = constraints.backendType === "openvino-npu" ? 1200 : 240;
const maxStartupPolls = (constraints.backendType === "openvino-npu" || constraints.backendType === "apple-npu") ? 1200 : 240;
for (let i = 0; i < maxStartupPolls; i++) {
const status = await getBackendStatus();
if (status.loading) {
@@ -610,61 +615,63 @@ function Generator({
/>
</div>
<div className="m3-text-field">
<label className="m3-text-field-label">Base Image (Optional)</label>
{baseImage ? (
<div style={{ display: "flex", gap: "12px", alignItems: "center", padding: "12px", background: "var(--md-sys-color-surface-variant)", borderRadius: "var(--md-shape-corner-medium)", border: "1px solid var(--md-sys-color-outline-variant)" }}>
<img src={baseImage} alt="Base" style={{ width: "64px", height: "64px", objectFit: "cover", borderRadius: "8px", flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: "0.85rem" }}>Base image ready</div>
<div style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)" }}>
The AI will redraw this image guided by your prompt.
{constraints.backendType !== "apple-npu" && constraints.backendType !== "openvino-npu" && (
<div className="m3-text-field">
<label className="m3-text-field-label">Base Image (Optional)</label>
{baseImage ? (
<div style={{ display: "flex", gap: "12px", alignItems: "center", padding: "12px", background: "var(--md-sys-color-surface-variant)", borderRadius: "var(--md-shape-corner-medium)", border: "1px solid var(--md-sys-color-outline-variant)" }}>
<img src={baseImage} alt="Base" style={{ width: "64px", height: "64px", objectFit: "cover", borderRadius: "8px", flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: "0.85rem" }}>Base image ready</div>
<div style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)" }}>
The AI will redraw this image guided by your prompt.
</div>
</div>
<button type="button" className="m3-btn m3-btn-error" style={{ height: "34px", flexShrink: 0 }} onClick={handleClearBaseImage} disabled={isGenerating}>
<Trash2 size={14} />
<span>Remove</span>
</button>
</div>
) : (
<label className="import-box" style={{ margin: 0, padding: "16px", cursor: isGenerating ? "not-allowed" : "pointer" }}>
<input
type="file"
accept="image/png,image/jpeg,image/webp"
style={{ display: "none" }}
onChange={handleBaseImageSelect}
disabled={isGenerating}
/>
<ImagePlus className="import-icon" />
<span style={{ fontWeight: 600 }}>Upload a base image</span>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", textAlign: "center" }}>
Optional. Generate a new image based on one of your own.
</span>
</label>
)}
{baseImage && (
<div style={{ marginTop: "12px" }}>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.8rem", marginBottom: "4px" }}>
<span>Transformation strength</span>
<span>{constraints.denoisingStrength.toFixed(2)}</span>
</div>
<input
type="range"
min="0.1"
max="1"
step="0.05"
value={constraints.denoisingStrength}
onChange={(e) => setConstraints((prev) => ({ ...prev, denoisingStrength: parseFloat(e.target.value) }))}
disabled={isGenerating}
style={{ width: "100%" }}
/>
<div style={{ fontSize: "0.72rem", color: "var(--md-sys-color-outline)", marginTop: "2px" }}>
Lower = closer to your image; higher = more creative freedom. 0.7 is a good start.
</div>
</div>
<button type="button" className="m3-btn m3-btn-error" style={{ height: "34px", flexShrink: 0 }} onClick={handleClearBaseImage} disabled={isGenerating}>
<Trash2 size={14} />
<span>Remove</span>
</button>
</div>
) : (
<label className="import-box" style={{ margin: 0, padding: "16px", cursor: isGenerating ? "not-allowed" : "pointer" }}>
<input
type="file"
accept="image/png,image/jpeg,image/webp"
style={{ display: "none" }}
onChange={handleBaseImageSelect}
disabled={isGenerating}
/>
<ImagePlus className="import-icon" />
<span style={{ fontWeight: 600 }}>Upload a base image</span>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", textAlign: "center" }}>
Optional. Generate a new image based on one of your own.
</span>
</label>
)}
{baseImage && (
<div style={{ marginTop: "12px" }}>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.8rem", marginBottom: "4px" }}>
<span>Transformation strength</span>
<span>{constraints.denoisingStrength.toFixed(2)}</span>
</div>
<input
type="range"
min="0.1"
max="1"
step="0.05"
value={constraints.denoisingStrength}
onChange={(e) => setConstraints((prev) => ({ ...prev, denoisingStrength: parseFloat(e.target.value) }))}
disabled={isGenerating}
style={{ width: "100%" }}
/>
<div style={{ fontSize: "0.72rem", color: "var(--md-sys-color-outline)", marginTop: "2px" }}>
Lower = closer to your image; higher = more creative freedom. 0.7 is a good start.
</div>
</div>
)}
</div>
)}
</div>
)}
{/* Configuration Status Chips (Material 3 style) */}
<div className="m3-text-field">
@@ -36,6 +36,8 @@ function ImageConstraints({
const availableBackends = backendOptions?.options?.length
? backendOptions.options
: [{ id: "cpu", label: "CPU", available: true }];
const isMac = availableBackends.some(b => b.id === "metal" || b.id === "apple-npu") ||
(specs?.os_name && (specs.os_name.toLowerCase().includes("darwin") || specs.os_name.toLowerCase().includes("mac")));
useEffect(() => {
if (isOpenVinoNpu && constraints.steps > 8) {
@@ -185,7 +187,7 @@ function ImageConstraints({
}}>
<Info size={16} style={{ color: "rgb(251, 191, 36)", flexShrink: 0, marginTop: "2px" }} />
<div>
<strong>Low-VRAM Protection Active:</strong> Capped at 512x512 for the loaded SD 1.x model (<code>{activeModel}</code>). Generating at larger sizes (like 1024x1024) is disabled to prevent out-of-memory crashes on your {specs && specs.gpu_name && !specs.gpu_name.includes("Loading") ? specs.gpu_name : "graphics processor"}.
<strong>Graphics Memory Protection Active:</strong> Locked at 512x512 size for this model (<code>{activeModel}</code>). Creating larger images (like 1024x1024) is disabled to prevent your {specs && specs.gpu_name && !specs.gpu_name.includes("Loading") ? specs.gpu_name : "graphics processor"} from running out of memory and crashing.
</div>
</div>
)}
@@ -357,9 +359,13 @@ function ImageConstraints({
</div>
))}
</div>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "4px", lineHeight: 1.35 }}>
CPU is slow but safest. Vulkan works on supported GPUs. CUDA is shown only when NVIDIA CUDA support is available.
</span>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "4px", lineHeight: 1.35 }}>
{isMac ? (
"CPU is slow but safest. Metal uses your Mac's graphics card. Apple Neural Engine (NPU) is highly power-efficient and fast."
) : (
"CPU is slow but safest. Vulkan works on supported GPUs. CUDA is shown only when NVIDIA CUDA support is available."
)}
</span>
{constraints.backendType === "cuda" && specs?.gpu_name && String(specs.gpu_name).toLowerCase().includes("gtx") && (
<div style={{
marginTop: "12px",
@@ -386,7 +392,7 @@ function ImageConstraints({
</div>
<div className="m3-text-field" style={{ marginTop: "20px" }}>
<label className="m3-text-field-label">Memory Optimization (GPU VRAM)</label>
<label className="m3-text-field-label">Memory Optimization (Graphics Memory)</label>
<div style={{ display: "flex", flexDirection: "column", gap: "14px", marginTop: "10px" }}>
<label style={{ display: "flex", alignItems: "flex-start", gap: "10px", cursor: "pointer", fontSize: "0.85rem" }}>
@@ -405,7 +411,7 @@ function ImageConstraints({
<div>
<strong style={{ color: "var(--md-sys-color-on-surface)" }}>Enable VAE Tiling</strong>
<div style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "2px", lineHeight: 1.35 }}>
Processes VAE decoding in smaller tiles. Drastically reduces VRAM usage (from 2GB+ down to ~100MB) with no speed loss. Highly recommended for GPUs with 4GB-6GB VRAM.
Builds the image in smaller, bite-sized sections. This heavily reduces graphics memory usage with no loss in speed. Highly recommended for computers with standard graphics cards.
</div>
</div>
</label>
@@ -426,7 +432,28 @@ function ImageConstraints({
<div>
<strong style={{ color: "var(--md-sys-color-on-surface)" }}>Run VAE on CPU</strong>
<div style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "2px", lineHeight: 1.35 }}>
Offloads the heavy VAE decoder computation from GPU VRAM to system memory (RAM). Saves ~2GB of VRAM, but makes the final decoding stage slightly slower.
Performs the final image rendering step using your computer's main memory (RAM) instead of graphics memory. Saves graphics memory, but makes the final stage of creating the image slightly slower.
</div>
</div>
</label>
<label style={{ display: "flex", alignItems: "flex-start", gap: "10px", cursor: "pointer", fontSize: "0.85rem" }}>
<input
type="checkbox"
checked={constraints.useFlashAttn !== false}
onChange={(e) => updateConstraint("useFlashAttn", e.target.checked)}
style={{
width: "16px",
height: "16px",
marginTop: "3px",
accentColor: "var(--md-sys-color-primary)",
cursor: "pointer"
}}
/>
<div>
<strong style={{ color: "var(--md-sys-color-on-surface)" }}>Enable Flash Attention</strong>
<div style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "2px", lineHeight: 1.35 }}>
Accelerates generation using memory-efficient attention. On some specific Mac models or GPUs, this may cause a slight slowdown, so you can disable it if needed.
</div>
</div>
</label>
+91 -11
View File
@@ -27,7 +27,7 @@ const MODEL_LIBRARY = [
],
},
{
group: "SD 1.5 - Ultra Fast / Low VRAM",
group: "SD 1.5 - Ultra Fast / Low Memory",
items: [
{
name: "DreamShaper 8",
@@ -79,7 +79,24 @@ const OPENVINO_MODEL_LIBRARY = [
},
];
function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRunning, constraints, backendOptions, showAlert = async ({ message }) => window.alert(message), showConfirm = async ({ message }) => window.confirm(message) }) {
const COREML_MODEL_LIBRARY = [
{
group: "Apple Silicon NPU - CoreML Test",
items: [
{
name: "Stable Diffusion v1.5 CoreML (split_einsum, palettized 6-bit)",
filename: "coreml-stable-diffusion-v1-5-palettized_split_einsum_v2_compiled",
format: "CoreML",
approxSize: "2.0 GB",
resolution: "512x512",
notes: "Apple Silicon ANE (Neural Engine) optimized 6-bit palettized Stable Diffusion 1.5 model. Unzips automatically on download completion and runs extremely fast on Mac NPUs.",
url: "https://huggingface.co/apple/coreml-stable-diffusion-v1-5-palettized/resolve/main/coreml-stable-diffusion-v1-5-palettized_split_einsum_v2_compiled.zip",
},
],
},
];
function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRunning, constraints, backendOptions, showAlert = async ({ message }) => window.alert(message), showConfirm = async ({ message }) => window.confirm(message), activeTab }) {
const [localModels, setLocalModels] = useState([]);
const [isLoadingModels, setIsLoadingModels] = useState(true);
const [downloadingModelId, setDownloadingModelId] = useState(null);
@@ -96,12 +113,42 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
const [pendingLoadModel, setPendingLoadModel] = useState(null);
const [backendInfo, setBackendInfo] = useState({ backendMode: "", backendBinary: "", backendDevice: "" });
const cancelLoadRef = React.useRef(false);
const handleCancelLoad = async () => {
cancelLoadRef.current = true;
try {
await stopServer();
} catch (err) {
console.warn("Failed to stop server on cancel:", err);
}
};
const modelNames = localModels.map((model) => normalizeModel(model).filename);
const isBusy = loadingModelId !== null || isUnloading;
const openvinoSupported = Boolean(backendOptions?.openvinoNpu?.supported);
const visibleModelLibrary = openvinoSupported ? [...MODEL_LIBRARY, ...OPENVINO_MODEL_LIBRARY] : MODEL_LIBRARY;
const appleNpuSupported = Boolean(
backendOptions?.options?.some((opt) => opt.id === "apple-npu") ||
backendOptions?.unavailable?.some((opt) => opt.id === "apple-npu")
);
let visibleModelLibrary = [...MODEL_LIBRARY];
if (openvinoSupported) {
visibleModelLibrary = [...visibleModelLibrary, ...OPENVINO_MODEL_LIBRARY];
}
if (appleNpuSupported) {
visibleModelLibrary = [...visibleModelLibrary, ...COREML_MODEL_LIBRARY];
}
const getLocalModelInfo = (modelId) => localModels.map(normalizeModel).find((model) => model.filename === modelId);
useEffect(() => {
if (activeTab === "models") {
fetchModels();
checkActiveDownload();
}
}, [activeTab]);
useEffect(() => {
fetchModels();
checkActiveDownload();
@@ -258,6 +305,10 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
}
return;
}
if (model.format === "CoreML") {
downloadByUrl(model.url, `${model.filename}.zip`);
return;
}
downloadByUrl(model.url, model.filename);
};
@@ -306,6 +357,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
return;
}
cancelLoadRef.current = false;
setLoadingModelId(modelId);
setModelLoadProgress({
progress: 0,
@@ -320,7 +372,18 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
});
try {
const modelInfo = getLocalModelInfo(modelId);
const loadConstraints = modelInfo?.backendType === "openvino-npu"
const isCoreMLModel = modelInfo?.format === "CoreML" || modelInfo?.backendType === "apple-npu";
const loadConstraints = isCoreMLModel
? {
...constraints,
backendType: "apple-npu",
useGpu: true,
width: 512,
height: 512,
steps: constraints.steps || 20,
cfgScale: constraints.cfgScale || 7,
}
: modelInfo?.backendType === "openvino-npu"
? {
...constraints,
backendType: "openvino-npu",
@@ -337,8 +400,12 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
let isReady = false;
let crashError = null;
const isOpenVinoModel = modelInfo?.backendType === "openvino-npu";
const maxStartupPolls = isOpenVinoModel ? 1200 : 240;
const maxStartupPolls = (isOpenVinoModel || isCoreMLModel) ? 1200 : 240;
for (let i = 0; i < maxStartupPolls; i++) {
if (cancelLoadRef.current) {
crashError = "Model load cancelled by the user.";
break;
}
const status = await getBackendStatus();
if (status.loading) {
setModelLoadProgress({
@@ -385,7 +452,9 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
}
} catch (e) {
console.error("Failed to load model:", e);
showAlert({ title: "Model Load Failed", message: e.message || String(e), danger: true });
if (e.message !== "Model load cancelled by the user.") {
showAlert({ title: "Model Load Failed", message: e.message || String(e), danger: true });
}
} finally {
setTimeout(() => setModelLoadProgress(null), 800);
setLoadingModelId(null);
@@ -580,9 +649,18 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
{modelLoadProgress && (
<div className="m3-card" style={{ borderLeft: "4px solid var(--md-sys-color-secondary)", marginTop: "16px" }}>
<h4 style={{ fontWeight: 700, marginBottom: "8px" }}>
Loading Model: {modelLoadProgress.model || loadingModelId}
</h4>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: "16px", marginBottom: "12px" }}>
<h4 style={{ fontWeight: 700, margin: 0 }}>
Loading Model: {modelLoadProgress.model || loadingModelId}
</h4>
<button
className="m3-btn m3-btn-error"
style={{ height: "30px", padding: "0 12px", fontSize: "0.78rem", borderRadius: "8px" }}
onClick={handleCancelLoad}
>
Cancel Loading
</button>
</div>
{(modelLoadProgress.backendMode || modelLoadProgress.backendBinary || modelLoadProgress.device) && (
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px", marginBottom: "10px" }}>
{modelLoadProgress.backendMode && (
@@ -611,7 +689,9 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
</span>
<span>
{modelLoadProgress.total > 0
? `Loaded ${modelLoadProgress.current} / ${modelLoadProgress.total} tensors`
? modelLoadProgress.total <= 10
? `Loading components: ${modelLoadProgress.current} / ${modelLoadProgress.total} (${Math.round(modelLoadProgress.progress)}%)`
: `Loaded ${modelLoadProgress.current} / ${modelLoadProgress.total} tensors`
: `${Math.round(modelLoadProgress.progress)}%`}
</span>
</div>
@@ -723,7 +803,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: "12px" }}>
{section.items.map((model) => {
const installed = modelNames.includes(model.filename);
const downloading = downloadingModelId === model.filename;
const downloading = downloadingModelId === model.filename || downloadingModelId === `${model.filename}.zip`;
return (
<div key={model.filename} style={{ display: "flex", flexDirection: "column", gap: "10px", padding: "14px", background: "var(--md-sys-color-surface-variant)", border: "1px solid var(--md-sys-color-outline-variant)", borderRadius: "var(--md-shape-corner-medium)" }}>
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>