Skip to content

Commit de8affa

Browse files
committed
experimental first pass comfy torch version updatinator
1 parent e9b0077 commit de8affa

3 files changed

Lines changed: 197 additions & 0 deletions

File tree

src/BuiltinExtensions/ComfyUIBackend/Assets/comfy_workflow_editor_helper.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,72 @@
11
/** If true, the workflow iframe is present. If false, the tab has never been opened, or loading failed. */
22
let hasComfyLoaded = false;
33

4+
/** Helper class managing the "ComfyUI Torch Versions" card on the Server Info tab. */
5+
class ComfyTorchManager {
6+
7+
constructor() {
8+
getRequiredElementById('serverinfotabbutton').addEventListener('click', () => this.refresh());
9+
getRequiredElementById('servertabbutton').addEventListener('click', () => this.refresh());
10+
let serverInfoTab = document.getElementById('Server-Info');
11+
let collection = createDiv(null, 'card-collection-inline');
12+
this.card = createDiv('comfy_torch_card', 'card border-secondary mb-3 card-center-container');
13+
this.card.dataset.requiredpermission = 'view_backends_list';
14+
this.card.style.display = 'none';
15+
this.card.innerHTML = `<div class="card-header translate">ComfyUI Torch Versions</div><div class="card-body"><span class="card-text" id="comfy_torch_card_body">(Loading...)</span></div>`;
16+
collection.appendChild(this.card);
17+
serverInfoTab.appendChild(collection);
18+
this.bodyElem = getRequiredElementById('comfy_torch_card_body');
19+
}
20+
21+
/** Refreshes the torch install list from the server and rebuilds the card body. */
22+
refresh() {
23+
if (!permissions.hasPermission('view_backends_list')) {
24+
return;
25+
}
26+
genericRequest('ComfyListTorchInstalls', {}, (data) => {
27+
if (!data.installs || data.installs.length == 0) {
28+
this.card.style.display = 'none';
29+
return;
30+
}
31+
this.card.style.display = '';
32+
let html = `Installed Comfy Torch Version(s):<br><br>`
33+
+ `<table class="simple-table"><tr><th>Install Folder</th><th>Torch Version</th><th>Backend IDs</th><th>Action</th></tr>`;
34+
for (let install of data.installs) {
35+
let action;
36+
if (install.can_update) {
37+
action = `<button class="basic-button translate" onclick="comfyTorchManager.updateTorch(this, '${escapeHtml(install.path)}', ${install.backend_ids[0]})">Update Torch</button>`;
38+
}
39+
else {
40+
action = `(None)`;
41+
}
42+
html += `<tr><td><code>${escapeHtml(install.path)}</code></td><td><code>${escapeHtml(install.torch_version)}</code></td><td>${escapeHtml(install.backend_ids.join(', '))}</td><td>${action}</td></tr>`;
43+
}
44+
html += `</table>`;
45+
this.bodyElem.innerHTML = html;
46+
});
47+
}
48+
49+
/** Triggered by the Update Torch button, to run a torch update for one install folder. */
50+
updateTorch(button, path, backendId) {
51+
if (!confirm(`Are you sure you want to update PyTorch for the ComfyUI install at:\n${path}\n\nThis is experimental! It will take a while, download several gigabytes of data, and might even break things!`)) {
52+
return;
53+
}
54+
button.disabled = true;
55+
button.parentElement.querySelectorAll('.installing_info').forEach(e => e.remove());
56+
let status = createDiv(null, 'installing_info', 'Updating torch (this may take several minutes, check server logs for details)...');
57+
button.parentElement.appendChild(status);
58+
genericRequest('ComfyUpdateTorch', { 'backendId': backendId }, (data) => {
59+
status.innerText = 'Torch updated, backends restarting.';
60+
setTimeout(() => this.refresh(), 3000);
61+
}, 0, (e) => {
62+
status.innerText = 'Failed to update torch: ' + e;
63+
button.disabled = false;
64+
});
65+
}
66+
}
67+
68+
let comfyTorchManager = new ComfyTorchManager();
69+
470
/** Helper class for managing the Comfy workflow tab. */
571
class ComfyWorkflowHelpers {
672

@@ -1357,6 +1423,9 @@ featureSetChangedCallbacks.push(() => {
13571423
comfyHasTriedToLoad = true;
13581424
comfyReloadObjectInfo(false);
13591425
}
1426+
if (isVisible(getRequiredElementById('Server-Info'))) {
1427+
comfyTorchManager.refresh();
1428+
}
13601429
});
13611430

13621431
function comfyListWorkflowsForBrowser(path, isRefresh, callback, depth) {

src/BuiltinExtensions/ComfyUIBackend/ComfyUISelfStartBackend.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ public class ComfyUISelfStartSettings : AutoConfiguration
8787

8888
public static string SwarmValidatedFrontendVersion = "1.42.11";
8989

90+
/// <summary>The current known version of PyTorch.</summary>
91+
public static string CurrentTorchVersion = "2.12.1";
92+
9093
/// <summary>List of known required python packages, as pairs of strings: Item1 is the folder name within python packages to look for, Item2 is the pip install command.</summary>
9194
public static List<(string, string)> RequiredPythonPackages =
9295
[
@@ -721,6 +724,26 @@ public static Version ParseVersion(string vers)
721724
return Version.Parse(vers.Before(".dev"));
722725
}
723726

727+
/// <summary>Get the version of a single installed pip package.</summary>
728+
public static string GetInstalledPackageVersion(string startScript, string package)
729+
{
730+
string lib = NetworkBackendUtils.GetProbableLibFolderFor(startScript);
731+
if (lib is null || lib.Length < 3 || !Directory.Exists(lib))
732+
{
733+
return null;
734+
}
735+
string prefix = $"{package}-";
736+
foreach (string dir in Directory.EnumerateDirectories(lib))
737+
{
738+
string name = dir.Replace('\\', '/').AfterLast('/');
739+
if (name.EndsWith(".dist-info") && name.StartsWith(prefix))
740+
{
741+
return name[prefix.Length..].Before(".dist-info");
742+
}
743+
}
744+
return null;
745+
}
746+
724747
/// <summary>Strict matcher that will block any muckery, excluding URLs and etc.</summary>
725748
public static AsciiMatcher RequirementPartMatcher = new(AsciiMatcher.BothCaseLetters + AsciiMatcher.Digits + ".-_");
726749

src/BuiltinExtensions/ComfyUIBackend/ComfyUIWebAPI.cs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using SwarmUI.Text2Image;
99
using SwarmUI.Utils;
1010
using SwarmUI.WebAPI;
11+
using System.Diagnostics;
1112
using System.IO;
1213
using System.Net.WebSockets;
1314

@@ -26,6 +27,8 @@ public static void Register()
2627
API.RegisterAPICall(ComfyGetNodeTypesForBackend, false, Permissions.ViewBackendsList);
2728
API.RegisterAPICall(ComfyEnsureRefreshable, false, ComfyUIBackendExtension.PermDirectCalls);
2829
API.RegisterAPICall(ComfyInstallFeatures, true, Permissions.InstallFeatures);
30+
API.RegisterAPICall(ComfyListTorchInstalls, false, Permissions.InstallFeatures);
31+
API.RegisterAPICall(ComfyUpdateTorch, true, Permissions.InstallFeatures);
2932
API.RegisterAPICall(DoTensorRTCreateWS, true, Permissions.CreateTRT);
3033
}
3134

@@ -243,6 +246,108 @@ public static async Task<JObject> ComfyInstallFeatures(Session session, string f
243246
}
244247
}
245248

249+
/// <summary>API route to list torch version installs for comfy backends.</summary>
250+
public static async Task<JObject> ComfyListTorchInstalls(Session session)
251+
{
252+
Version target = ComfyUISelfStartBackend.ParseVersion(ComfyUISelfStartBackend.CurrentTorchVersion);
253+
Dictionary<string, List<int>> folders = [];
254+
foreach (BackendHandler.BackendData data in Program.Backends.EnumerateT2IBackends)
255+
{
256+
if (data.AbstractBackend is not ComfyUISelfStartBackend backend)
257+
{
258+
continue;
259+
}
260+
string script = backend.Settings.StartScript;
261+
if (string.IsNullOrWhiteSpace(script))
262+
{
263+
continue;
264+
}
265+
string folder = backend.ComfyPathBase;
266+
folders.GetOrCreate(folder, () => []).Add(data.ID);
267+
}
268+
JArray installs = [];
269+
foreach ((string folder, List<int> backends) in folders.OrderBy(p => p.Key))
270+
{
271+
string torchVersRaw = ComfyUISelfStartBackend.GetInstalledPackageVersion($"{folder}/main.py", "torch");
272+
string torchVers = torchVersRaw?.Before('+');
273+
bool canUpdate = false;
274+
if (torchVers is not null && target is not null)
275+
{
276+
try
277+
{
278+
canUpdate = ComfyUISelfStartBackend.ParseVersion(torchVers) < target;
279+
}
280+
catch (Exception)
281+
{
282+
canUpdate = false;
283+
}
284+
if (!torchVersRaw.ToLowerFast().Contains("+cu")) // TODO: AMD/etc support?
285+
{
286+
canUpdate = false;
287+
}
288+
}
289+
installs.Add(new JObject()
290+
{
291+
["path"] = folder,
292+
["torch_version"] = torchVersRaw ?? "(unknown)",
293+
["can_update"] = canUpdate,
294+
["backend_ids"] = new JArray(backends.OrderBy(i => i).Select(i => (JToken)i).ToArray())
295+
});
296+
}
297+
return new JObject()
298+
{
299+
["target_torch_version"] = ComfyUISelfStartBackend.CurrentTorchVersion,
300+
["installs"] = installs
301+
};
302+
}
303+
304+
/// <summary>API route to update Torch for a single Comfy install folder.</summary>
305+
public static async Task<JObject> ComfyUpdateTorch(Session session, int backendId)
306+
{
307+
await MultiInstallLock.WaitAsync(Program.GlobalProgramCancel);
308+
try
309+
{
310+
if (!Program.Backends.AllBackends.TryGetValue(backendId, out BackendHandler.BackendData backend) || backend.AbstractBackend is not ComfyUISelfStartBackend target)
311+
{
312+
return new() { ["error"] = $"Invalid backend ID {backendId}" };
313+
}
314+
string path = target.ComfyPathBase;
315+
ComfyUISelfStartBackend[] backends = [.. Program.Backends.EnumerateT2IBackends.Select(d => d.AbstractBackend as ComfyUISelfStartBackend).Where(b => b is not null && b.ComfyPathBase == path)];
316+
Logs.Info($"User {session.User.UserID} requested a PyTorch update for ComfyUI install at '{path}' (affecting {backends.Length} backend(s))...");
317+
Logs.Info($"[Torch Update] Shutting down {backends.Length} backend(s) that use this install...");
318+
Task[] shutdownTasks = [.. backends.Select(b => Program.Backends.ShutdownBackendCleanly(b.BackendData))];
319+
await Task.WhenAll(shutdownTasks);
320+
async Task pipCall(string reason, string call)
321+
{
322+
Logs.Info($"[Torch Update] {reason} for ComfyUI install '{path}'...");
323+
Process p = target.DoPythonCall($"-s -m pip {call}");
324+
NetworkBackendUtils.ReportLogsFromProcess(p, $"ComfyUI (Torch Update - {reason})", "");
325+
await p.WaitForExitAsync(Program.GlobalProgramCancel);
326+
Logs.Info($"[Torch Update] Done {reason} for ComfyUI install '{path}'.");
327+
}
328+
try
329+
{
330+
await pipCall("Uninstalling old torch", "uninstall -y torch torchvision torchaudio");
331+
await pipCall("Installing new torch", "install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu132");
332+
}
333+
catch (Exception ex)
334+
{
335+
Logs.Error($"Failed to update torch for ComfyUI install '{path}': {ex.ReadableString()}");
336+
return new JObject() { ["error"] = "Torch update failed, check server logs for details." };
337+
}
338+
Logs.Info($"[Torch Update] Restarting {backends.Length} backend(s)...");
339+
foreach (ComfyUISelfStartBackend back in backends)
340+
{
341+
Program.Backends.DoInitBackend(back.BackendData);
342+
}
343+
return new() { ["success"] = true };
344+
}
345+
finally
346+
{
347+
MultiInstallLock.Release();
348+
}
349+
}
350+
246351
public static Dictionary<string, int> AspectRangeToMultiplier = new()
247352
{
248353
["Exact"] = 1,

0 commit comments

Comments
 (0)