diffusers-workflow 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
- diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
- diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
- diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
- diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
- diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
- dw/__init__.py +440 -0
- dw/adapter_compatibility.py +226 -0
- dw/arguments.py +1231 -0
- dw/assessment_rules.py +159 -0
- dw/assets.py +130 -0
- dw/cache_blocks.json +16 -0
- dw/cache_blocks.py +146 -0
- dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
- dw/content_types.py +150 -0
- dw/dissolve_frame_errors.py +121 -0
- dw/docs/ACCELERATION.md +352 -0
- dw/docs/AGENT_LOOP.md +95 -0
- dw/docs/DEPENDENCIES.md +91 -0
- dw/docs/IP_ADAPTER.md +109 -0
- dw/docs/LORAS.md +131 -0
- dw/docs/MCP.md +517 -0
- dw/docs/PROMPT_WEIGHTING.md +78 -0
- dw/docs/QUANTIZATION.md +230 -0
- dw/docs/RECIPES_24GB.md +201 -0
- dw/docs/RELEASING.md +195 -0
- dw/docs/REMOTE.md +140 -0
- dw/docs/REPL_COMMANDS.md +121 -0
- dw/docs/REPL_WORKER_GUIDE.md +51 -0
- dw/docs/SECURITY.md +272 -0
- dw/docs/SECURITY_QUICKREF.md +112 -0
- dw/docs/SERVER.md +679 -0
- dw/docs/TASKS.md +1741 -0
- dw/docs/TESTING.md +71 -0
- dw/docs/WORKFLOW_GUIDE.md +2038 -0
- dw/docs/WORKSPACES.md +316 -0
- dw/download_watch.py +335 -0
- dw/elision.py +306 -0
- dw/events.py +275 -0
- dw/for_each.py +409 -0
- dw/host_memory.py +258 -0
- dw/host_memory_projection.py +230 -0
- dw/hub_cache.py +432 -0
- dw/introspection.py +1228 -0
- dw/kernel_availability.py +208 -0
- dw/locations.py +599 -0
- dw/log_setup.py +45 -0
- dw/loudness.py +82 -0
- dw/media_audio.py +217 -0
- dw/media_frames.py +367 -0
- dw/media_info.py +297 -0
- dw/pipeline_processors/chain.py +821 -0
- dw/pipeline_processors/config_objects.py +237 -0
- dw/pipeline_processors/pipeline.py +2297 -0
- dw/pipeline_processors/remote.py +46 -0
- dw/plan.py +920 -0
- dw/previous_results.py +411 -0
- dw/probe_paths.py +59 -0
- dw/prompt_schema.json +48 -0
- dw/prompt_weighting.py +378 -0
- dw/prompts.py +159 -0
- dw/realize.py +250 -0
- dw/reference_limits.py +215 -0
- dw/reference_names.py +125 -0
- dw/repl.py +338 -0
- dw/repl_commands.py +836 -0
- dw/repl_worker.py +159 -0
- dw/result.py +1720 -0
- dw/result_fps.py +82 -0
- dw/run.py +162 -0
- dw/runs.py +768 -0
- dw/scalar_result_validation.py +97 -0
- dw/schema.py +283 -0
- dw/security.py +1038 -0
- dw/select_validation.py +115 -0
- dw/serve.py +277 -0
- dw/server/__init__.py +2 -0
- dw/server/app.py +4586 -0
- dw/server/assess.py +132 -0
- dw/server/catalog_shape.py +487 -0
- dw/server/enhancers.py +129 -0
- dw/server/exports.py +480 -0
- dw/server/guides.py +257 -0
- dw/server/jobs.py +1561 -0
- dw/server/mcp_mount.py +95 -0
- dw/server/netinfo.py +124 -0
- dw/server/observed_cost.py +379 -0
- dw/server/sysinfo.py +71 -0
- dw/server/ui/assets/abap-08VXUWAP.js +1 -0
- dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
- dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
- dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
- dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
- dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
- dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
- dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
- dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
- dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
- dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
- dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
- dw/server/ui/assets/css-DIMkf-bt.js +3 -0
- dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
- dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
- dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
- dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
- dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
- dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
- dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
- dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
- dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
- dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
- dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
- dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
- dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
- dw/server/ui/assets/go-C-y9NEjX.js +1 -0
- dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
- dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
- dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
- dw/server/ui/assets/html-PhsdjHSr.js +1 -0
- dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
- dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
- dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
- dw/server/ui/assets/index-DgrYhQd9.js +43 -0
- dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
- dw/server/ui/assets/java-BEtHBSE6.js +1 -0
- dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
- dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
- dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
- dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
- dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
- dw/server/ui/assets/less-B9JPFI3C.js +2 -0
- dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
- dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
- dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
- dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
- dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
- dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
- dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
- dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
- dw/server/ui/assets/monaco--ixms01u.css +1 -0
- dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
- dw/server/ui/assets/msdax-DauUninz.js +1 -0
- dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
- dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
- dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
- dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
- dw/server/ui/assets/perl-oz_6vUea.js +1 -0
- dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
- dw/server/ui/assets/php-nr791fC2.js +1 -0
- dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
- dw/server/ui/assets/postiats-43DmfD33.js +1 -0
- dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
- dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
- dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
- dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
- dw/server/ui/assets/python-Bcn70HdC.js +1 -0
- dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
- dw/server/ui/assets/r-BwWrilGY.js +1 -0
- dw/server/ui/assets/razor-D1HmNnby.js +1 -0
- dw/server/ui/assets/redis-ClamHrr6.js +1 -0
- dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
- dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
- dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
- dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
- dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
- dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
- dw/server/ui/assets/scheme-BeGwcela.js +1 -0
- dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
- dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
- dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
- dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
- dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
- dw/server/ui/assets/sql-NEE52Syq.js +1 -0
- dw/server/ui/assets/st-DbInun42.js +1 -0
- dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
- dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
- dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
- dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
- dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
- dw/server/ui/assets/twig-De2hgUGE.js +1 -0
- dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
- dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
- dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
- dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
- dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
- dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
- dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
- dw/server/ui/index.html +17 -0
- dw/server/updater.py +192 -0
- dw/settings.py +98 -0
- dw/shot_span_preflight.py +116 -0
- dw/shots.py +359 -0
- dw/slice_preflight.py +148 -0
- dw/step.py +187 -0
- dw/step_cache.py +442 -0
- dw/subfolders.py +107 -0
- dw/task_domains.py +307 -0
- dw/tasks/assess.py +826 -0
- dw/tasks/audio_transcription.py +88 -0
- dw/tasks/audio_utils.py +1862 -0
- dw/tasks/background_remover.py +43 -0
- dw/tasks/borders.py +113 -0
- dw/tasks/compose_text.py +74 -0
- dw/tasks/concat_videos.py +300 -0
- dw/tasks/depth_estimator.py +54 -0
- dw/tasks/diffusion_upscale.py +109 -0
- dw/tasks/dissolve_videos.py +342 -0
- dw/tasks/format_messages.py +24 -0
- dw/tasks/gather.py +173 -0
- dw/tasks/grade.py +97 -0
- dw/tasks/image_to_text.py +43 -0
- dw/tasks/image_utils.py +764 -0
- dw/tasks/interpolate_frames.py +252 -0
- dw/tasks/judge.py +68 -0
- dw/tasks/model_cache.py +55 -0
- dw/tasks/pair_audio.py +268 -0
- dw/tasks/qr_code.py +19 -0
- dw/tasks/restore_faces.py +175 -0
- dw/tasks/rife_model.py +192 -0
- dw/tasks/segment.py +121 -0
- dw/tasks/select.py +111 -0
- dw/tasks/speech_generation.py +228 -0
- dw/tasks/stabilize.py +129 -0
- dw/tasks/task.py +920 -0
- dw/tasks/tensor_image.py +57 -0
- dw/tasks/text_generation.py +169 -0
- dw/tasks/text_sections.py +80 -0
- dw/tasks/upscale.py +203 -0
- dw/tasks/video_utils.py +624 -0
- dw/tasks/zoe_depth.py +71 -0
- dw/teacache.py +381 -0
- dw/teacache_models.json +99 -0
- dw/test.py +29 -0
- dw/type_helpers.py +231 -0
- dw/validate.py +68 -0
- dw/variable_constraints.py +444 -0
- dw/variables.py +443 -0
- dw/video_extensions.py +141 -0
- dw/vram_estimate.py +116 -0
- dw/worker.py +764 -0
- dw/workflow.py +2007 -0
- dw/workflow_schema.json +1346 -0
- dw/workflow_sources.py +383 -0
- dw/workflows/h3_context_ir.json +57 -0
- dw/workflows/test.json +31 -0
- dw/workspace.py +730 -0
- dw_mcp/__init__.py +6 -0
- dw_mcp/__main__.py +133 -0
- dw_mcp/assets.py +336 -0
- dw_mcp/authoring.py +114 -0
- dw_mcp/catalog.py +360 -0
- dw_mcp/client.py +486 -0
- dw_mcp/diagnose.py +371 -0
- dw_mcp/exports.py +84 -0
- dw_mcp/guides.py +35 -0
- dw_mcp/media.py +638 -0
- dw_mcp/models.py +97 -0
- dw_mcp/prompts.py +104 -0
- dw_mcp/server.py +1343 -0
- dw_mcp/workspaces.py +212 -0
dw/server/mcp_mount.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Mount the MCP tool surface inside the HTTP server.
|
|
2
|
+
|
|
3
|
+
`dw_mcp` is a pure HTTP client of dw.serve; mounting it here does not
|
|
4
|
+
change that - the tools reach the REST API over the server's own bind
|
|
5
|
+
address with the same token the API requires, a few milliseconds per
|
|
6
|
+
call. This is the only module under dw/ that imports the mcp SDK, and it
|
|
7
|
+
does so lazily: the package is an optional extra.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ipaddress
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def client_base_url(host, port):
|
|
14
|
+
"""The URL the mounted tools use to reach this same server.
|
|
15
|
+
|
|
16
|
+
A loopback or wildcard bind is reached at 127.0.0.1; any other bind
|
|
17
|
+
address has to be used verbatim, since uvicorn is then listening on
|
|
18
|
+
that address alone and a loopback connection would be refused - which
|
|
19
|
+
is exactly the `--host 100.x.y.z` Tailscale setup docs/REMOTE.md
|
|
20
|
+
recommends. An IPv6 literal is bracketed for the URL's authority.
|
|
21
|
+
"""
|
|
22
|
+
# imported here rather than at module scope: dw.server.app imports this
|
|
23
|
+
# module (lazily, inside create_app), so an import back at import time
|
|
24
|
+
# would be a cycle
|
|
25
|
+
from .app import LOOPBACK_HOSTS, WILDCARD_HOSTS
|
|
26
|
+
|
|
27
|
+
host = (host or "").lower()
|
|
28
|
+
if host in WILDCARD_HOSTS | LOOPBACK_HOSTS:
|
|
29
|
+
host = "127.0.0.1"
|
|
30
|
+
try:
|
|
31
|
+
if ipaddress.ip_address(host).version == 6:
|
|
32
|
+
host = f"[{host}]"
|
|
33
|
+
except ValueError:
|
|
34
|
+
# a hostname, not an IP literal - nothing to bracket
|
|
35
|
+
pass
|
|
36
|
+
return f"http://{host}:{port}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _SingleRouteApp:
|
|
40
|
+
"""Send every path the parent routed here to the sub-app's one route.
|
|
41
|
+
|
|
42
|
+
`dw.server.app` routes `/mcp` and anything under it to this wrapper,
|
|
43
|
+
so the SDK's Starlette app - which has a single route at `/` - sees
|
|
44
|
+
`/` whatever spelling the client used. Without it a bare `POST /mcp`
|
|
45
|
+
either 404s or takes a redirect to `/mcp/`, and `http://box:8765/mcp`
|
|
46
|
+
is the URL the docs tell people to configure.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, app):
|
|
50
|
+
self.app = app
|
|
51
|
+
|
|
52
|
+
async def __call__(self, scope, receive, send):
|
|
53
|
+
if scope.get("type") in ("http", "websocket"):
|
|
54
|
+
scope = dict(scope, path="/", raw_path=b"/")
|
|
55
|
+
await self.app(scope, receive, send)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_mcp_app(*, host, port, token):
|
|
59
|
+
"""The ASGI app to serve at /mcp, the MCPServer behind it, and the
|
|
60
|
+
HTTP client the tools use to reach this same server (client_base_url).
|
|
61
|
+
|
|
62
|
+
The SDK's Starlette app carries its own lifespan (the session
|
|
63
|
+
manager), which the parent app does not run for a sub-app it routes
|
|
64
|
+
to - `create_app`'s lifespan enters `server.session_manager.run()`
|
|
65
|
+
itself.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
from mcp.server.transport_security import TransportSecuritySettings
|
|
69
|
+
except ImportError:
|
|
70
|
+
raise SystemExit(
|
|
71
|
+
"--mcp needs the mcp extra: pip install 'diffusers-workflow[mcp]'"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
from dw_mcp.client import DwClient
|
|
75
|
+
from dw_mcp.server import build_server
|
|
76
|
+
|
|
77
|
+
client = DwClient(base_url=client_base_url(host, port), token=token)
|
|
78
|
+
# The tools now run *on* the server rather than on the agent's machine,
|
|
79
|
+
# so a tool that writes a local file is writing on the GPU box. It has to
|
|
80
|
+
# know that to confine where it writes (#113)
|
|
81
|
+
client.mounted = True
|
|
82
|
+
server = build_server(client)
|
|
83
|
+
asgi = server.streamable_http_app(
|
|
84
|
+
# the SDK app routes at "/"; the parent routes /mcp here and
|
|
85
|
+
# _SingleRouteApp rewrites the path to "/" on the way in
|
|
86
|
+
streamable_http_path="/",
|
|
87
|
+
# one JSON reply per request, no sessions to strand on a restart
|
|
88
|
+
json_response=True,
|
|
89
|
+
stateless_http=True,
|
|
90
|
+
# dw.server.app's own Origin/Host middleware runs first and owns this
|
|
91
|
+
transport_security=TransportSecuritySettings(
|
|
92
|
+
enable_dns_rebinding_protection=False
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
return _SingleRouteApp(asgi), server, client
|
dw/server/netinfo.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Which addresses this machine can be reached at.
|
|
2
|
+
|
|
3
|
+
The Server page answers one question - "what URL do I give the other
|
|
4
|
+
machine?" - and the only part of the answer the server itself knows is the
|
|
5
|
+
list of addresses its interfaces carry. URL composition stays in the UI;
|
|
6
|
+
this module only enumerates.
|
|
7
|
+
|
|
8
|
+
Two enumeration methods, in order: psutil when it happens to be installed
|
|
9
|
+
(it is a transitive dependency here, not a declared one, so it is used
|
|
10
|
+
opportunistically and never required), and the stdlib otherwise. Neither
|
|
11
|
+
raises: an address list is a convenience, and a machine whose interfaces
|
|
12
|
+
cannot be read still has a working server, so failure yields [].
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import socket
|
|
16
|
+
import ipaddress
|
|
17
|
+
|
|
18
|
+
__all__ = ["local_addresses"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _usable(address):
|
|
22
|
+
"""A candidate address, normalized, or None if it is not worth showing.
|
|
23
|
+
|
|
24
|
+
Loopback is dropped (it names this machine only - the page's whole
|
|
25
|
+
point is the other machine) and so is link-local (169.254.x / fe80::,
|
|
26
|
+
which needs a scope id to be usable at all). Docker and veth addresses
|
|
27
|
+
survive on purpose: on a box that runs containers they are real
|
|
28
|
+
routes, and guessing which of a machine's networks the user meant is
|
|
29
|
+
not the server's job.
|
|
30
|
+
"""
|
|
31
|
+
if not address:
|
|
32
|
+
return None
|
|
33
|
+
# getaddrinfo and psutil both hand back scoped IPv6 ('fe80::1%eth0')
|
|
34
|
+
address = address.split("%")[0].strip()
|
|
35
|
+
try:
|
|
36
|
+
parsed = ipaddress.ip_address(address)
|
|
37
|
+
except ValueError:
|
|
38
|
+
return None
|
|
39
|
+
if parsed.is_loopback or parsed.is_link_local or parsed.is_unspecified:
|
|
40
|
+
return None
|
|
41
|
+
return str(parsed), "IPv4" if parsed.version == 4 else "IPv6"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _psutil_addresses():
|
|
45
|
+
"""(address, family, interface) triples from psutil, which is the only
|
|
46
|
+
method that can name the interface an address belongs to."""
|
|
47
|
+
import psutil
|
|
48
|
+
|
|
49
|
+
found = []
|
|
50
|
+
for interface, entries in psutil.net_if_addrs().items():
|
|
51
|
+
for entry in entries:
|
|
52
|
+
if entry.family not in (socket.AF_INET, socket.AF_INET6):
|
|
53
|
+
continue
|
|
54
|
+
usable = _usable(entry.address)
|
|
55
|
+
if usable is not None:
|
|
56
|
+
found.append((usable[0], usable[1], interface))
|
|
57
|
+
return found
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _outbound_address():
|
|
61
|
+
"""The address a packet to the outside world would leave from.
|
|
62
|
+
|
|
63
|
+
A UDP socket's connect() only sets the peer for later sends; it does
|
|
64
|
+
no handshake and puts nothing on the wire. It exists because
|
|
65
|
+
getaddrinfo(gethostname()) is silent on machines whose hostname
|
|
66
|
+
resolves to 127.0.1.1 - a Debian default - and that is exactly the
|
|
67
|
+
machine a remote-access page is being read on.
|
|
68
|
+
"""
|
|
69
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
70
|
+
try:
|
|
71
|
+
sock.connect(("192.0.2.1", 9)) # TEST-NET-1, routed nowhere
|
|
72
|
+
return sock.getsockname()[0]
|
|
73
|
+
finally:
|
|
74
|
+
sock.close()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _stdlib_addresses():
|
|
78
|
+
"""The psutil-free fallback: whatever this host's name resolves to,
|
|
79
|
+
plus the primary outbound address. No interface names are available
|
|
80
|
+
this way, so they come back None."""
|
|
81
|
+
found = []
|
|
82
|
+
try:
|
|
83
|
+
infos = socket.getaddrinfo(socket.gethostname(), None)
|
|
84
|
+
except OSError:
|
|
85
|
+
infos = []
|
|
86
|
+
for family, _type, _proto, _canonical, sockaddr in infos:
|
|
87
|
+
if family not in (socket.AF_INET, socket.AF_INET6):
|
|
88
|
+
continue
|
|
89
|
+
usable = _usable(sockaddr[0])
|
|
90
|
+
if usable is not None:
|
|
91
|
+
found.append((usable[0], usable[1], None))
|
|
92
|
+
try:
|
|
93
|
+
usable = _usable(_outbound_address())
|
|
94
|
+
except OSError:
|
|
95
|
+
usable = None
|
|
96
|
+
if usable is not None:
|
|
97
|
+
found.append((usable[0], usable[1], None))
|
|
98
|
+
return found
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def local_addresses():
|
|
102
|
+
"""This machine's non-loopback addresses, IPv4 first, as
|
|
103
|
+
[{"address", "family", "interface"}] - interface is None when the
|
|
104
|
+
method that found it cannot say. Never raises; [] on failure."""
|
|
105
|
+
found = []
|
|
106
|
+
try:
|
|
107
|
+
found = _psutil_addresses()
|
|
108
|
+
except Exception:
|
|
109
|
+
found = []
|
|
110
|
+
if not found:
|
|
111
|
+
try:
|
|
112
|
+
found = _stdlib_addresses()
|
|
113
|
+
except Exception:
|
|
114
|
+
found = []
|
|
115
|
+
seen = set()
|
|
116
|
+
unique = []
|
|
117
|
+
for address, family, interface in found:
|
|
118
|
+
if address in seen:
|
|
119
|
+
continue
|
|
120
|
+
seen.add(address)
|
|
121
|
+
unique.append({"address": address, "family": family, "interface": interface})
|
|
122
|
+
# IPv4 before IPv6, then by address so the list is stable between calls
|
|
123
|
+
unique.sort(key=lambda entry: (entry["family"] != "IPv4", entry["address"]))
|
|
124
|
+
return unique
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""What this server's own history says a workflow costs.
|
|
2
|
+
|
|
3
|
+
`cost` is a maintainer's claim: runs they measured, on cards they name,
|
|
4
|
+
written into the workflow. `dw/workflow_schema.json` says of it *"Never
|
|
5
|
+
derived; absent means unknown"*, and that sentence is load-bearing - so this
|
|
6
|
+
module does not touch it. It reports a sibling, `observed`, which is only ever
|
|
7
|
+
this box's own finished jobs (#93, split out of #91).
|
|
8
|
+
|
|
9
|
+
The report that asked for it: `list_workflows(shape="shot",
|
|
10
|
+
traits="identity-referenced")` answered `cost: null` for seven of eight
|
|
11
|
+
entries, on a box that had run `templates/minimax/reference-to-video` five
|
|
12
|
+
times at one size - 511, 464.9, 478, ~460, ~467 seconds. The consumer is told
|
|
13
|
+
to quote a price before spending GPU minutes and the catalog answered
|
|
14
|
+
"unknown" for the template their series actually used.
|
|
15
|
+
|
|
16
|
+
Four rules, each of which is a way the naive median would lie:
|
|
17
|
+
|
|
18
|
+
- **Comparability.** A 141-frame run does not inform a 124-frame estimate.
|
|
19
|
+
A workflow declares `cost_drivers` - the variables that move its cost - and
|
|
20
|
+
runs are bucketed by those values; the bucket reported is the one a caller
|
|
21
|
+
gets by *default*, so it stays comparable to the curated figure. A workflow
|
|
22
|
+
declaring no drivers falls back to default-arguments-only runs, which is
|
|
23
|
+
thin but never wrong.
|
|
24
|
+
- **Cold is not warm.** The same `text-to-image` run is 13.6 s with the model
|
|
25
|
+
on disk and 6.3 s with it resident. Averaging those describes neither, so
|
|
26
|
+
`cold_minutes` and `warm_minutes` are separate, each with its own run
|
|
27
|
+
count, and the one comparable to curated `cost` (wall clock *including*
|
|
28
|
+
model load) is the one named `cold`.
|
|
29
|
+
- **A cached run is not a run.** A seeded workflow whose every step hit the
|
|
30
|
+
step cache finishes in seconds and wrote nothing. Counting those would
|
|
31
|
+
collapse the figure toward zero for exactly the templates that get re-run
|
|
32
|
+
most.
|
|
33
|
+
- **Legibility over stability.** History is prunable, so the number moves.
|
|
34
|
+
`runs` and `since` are what make that readable rather than surprising.
|
|
35
|
+
|
|
36
|
+
Everything comes off the job row - one query, no per-run JSON reads from the
|
|
37
|
+
run directories, so the figures survive a pruned run directory.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import logging
|
|
42
|
+
import time
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger("dw")
|
|
45
|
+
|
|
46
|
+
COST_DRIVERS_KEY = "cost_drivers"
|
|
47
|
+
# A phase event as `json.dumps` wrote it. Matched in SQL so a run's 200
|
|
48
|
+
# persisted events are never parsed just to ask whether the weights loaded.
|
|
49
|
+
LOADING_MARKER = '"phase": "loading"'
|
|
50
|
+
# Persisted events are trimmed to the newest 200 (`MAX_PERSISTED_EVENTS`), so
|
|
51
|
+
# a long run's `loading` phase can be gone from the record. Such a run counts
|
|
52
|
+
# toward `runs` and toward neither side of the split - claiming it was warm
|
|
53
|
+
# because the evidence was trimmed is the one answer that would be wrong.
|
|
54
|
+
EVENT_CAP = 200
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def declared_drivers(definition):
|
|
58
|
+
"""The variables the author says move this workflow's cost.
|
|
59
|
+
|
|
60
|
+
A name the workflow declares no variable for is dropped rather than
|
|
61
|
+
bucketed on: its effective value is None for every run, so it would put
|
|
62
|
+
every run in one bucket while looking like it had partitioned them.
|
|
63
|
+
`tests/test_observed_cost.py` sweeps the catalog for one, since a driver
|
|
64
|
+
that quietly checks nothing is the failure this whole module exists to
|
|
65
|
+
avoid.
|
|
66
|
+
"""
|
|
67
|
+
drivers = definition.get(COST_DRIVERS_KEY)
|
|
68
|
+
if not isinstance(drivers, list):
|
|
69
|
+
return []
|
|
70
|
+
variables = definition.get("variables") or {}
|
|
71
|
+
return [name for name in drivers if isinstance(name, str) and name in variables]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _bucket_key(definition, arguments):
|
|
75
|
+
"""What makes two runs of this workflow comparable, or None when they
|
|
76
|
+
are not comparable at all.
|
|
77
|
+
|
|
78
|
+
With declared drivers: the effective value of each, which is what the run
|
|
79
|
+
passed or else the stored default. Without: only a run that overrode
|
|
80
|
+
nothing that matters is comparable, and since nothing is declared to
|
|
81
|
+
matter, that means a run that overrode nothing at all.
|
|
82
|
+
"""
|
|
83
|
+
variables = definition.get("variables") or {}
|
|
84
|
+
drivers = declared_drivers(definition)
|
|
85
|
+
if drivers:
|
|
86
|
+
return tuple(
|
|
87
|
+
(name, _comparable(arguments.get(name, variables.get(name))))
|
|
88
|
+
for name in sorted(drivers)
|
|
89
|
+
)
|
|
90
|
+
if any(key in variables for key in arguments):
|
|
91
|
+
return None
|
|
92
|
+
return ()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _comparable(value):
|
|
96
|
+
"""A driver value as something hashable and stable across JSON round
|
|
97
|
+
trips - 124 and "124" are one bucket, since the engine coerces.
|
|
98
|
+
|
|
99
|
+
A *list* driver buckets on its length, not its contents: a `shots` list
|
|
100
|
+
costs what its number of entries costs, and two four-shot runs of one
|
|
101
|
+
template are comparable however different their prompts. Bucketing on
|
|
102
|
+
contents would give every run its own bucket and so a permanent
|
|
103
|
+
`runs: 1`, which is the same as reporting nothing.
|
|
104
|
+
"""
|
|
105
|
+
if isinstance(value, bool) or value is None:
|
|
106
|
+
return value
|
|
107
|
+
if isinstance(value, (int, float)):
|
|
108
|
+
return float(value)
|
|
109
|
+
if isinstance(value, list):
|
|
110
|
+
return ("entries", len(value))
|
|
111
|
+
if isinstance(value, str):
|
|
112
|
+
try:
|
|
113
|
+
return float(value)
|
|
114
|
+
except ValueError:
|
|
115
|
+
return value
|
|
116
|
+
return json.dumps(value, sort_keys=True, default=str)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _is_task_only(definition):
|
|
120
|
+
"""Whether every declared step is a plain task - no `pipeline`,
|
|
121
|
+
`pipeline_reference` or composed `workflow` step anywhere.
|
|
122
|
+
|
|
123
|
+
Such a workflow never logs a `loading` phase, so every run of it lands
|
|
124
|
+
in `had_load: False` and the cold/warm split below would bucket it all
|
|
125
|
+
as warm forever - silently withholding `cold_minutes`, the only figure
|
|
126
|
+
`_observed` (dw/plan.py) will quote as `basis: "observed"` (#439). A
|
|
127
|
+
task-only run has no model-load component to separate from its wall
|
|
128
|
+
clock, so its whole duration is what a curated `cost` figure would be
|
|
129
|
+
measured against, and is reported as `cold` rather than `warm`.
|
|
130
|
+
|
|
131
|
+
False when `steps` is missing or empty - an inline or otherwise
|
|
132
|
+
step-less definition is not evidence either way, so the ordinary
|
|
133
|
+
`had_load` split stays in force rather than guessing.
|
|
134
|
+
"""
|
|
135
|
+
steps = definition.get("steps")
|
|
136
|
+
if not isinstance(steps, list) or not steps:
|
|
137
|
+
return False
|
|
138
|
+
for step in steps:
|
|
139
|
+
if not isinstance(step, dict):
|
|
140
|
+
return False
|
|
141
|
+
if "pipeline" in step or "pipeline_reference" in step or "workflow" in step:
|
|
142
|
+
return False
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _every_step_was_reused(manifest_text):
|
|
147
|
+
"""Whether this run generated nothing - every manifest entry a step-cache
|
|
148
|
+
hit republishing an earlier run's files."""
|
|
149
|
+
try:
|
|
150
|
+
manifest = json.loads(manifest_text or "[]")
|
|
151
|
+
except (TypeError, ValueError):
|
|
152
|
+
return False
|
|
153
|
+
if not isinstance(manifest, list) or not manifest:
|
|
154
|
+
return False
|
|
155
|
+
return all(
|
|
156
|
+
isinstance(entry, dict) and entry.get("reused") is True for entry in manifest
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _median(values):
|
|
161
|
+
ordered = sorted(values)
|
|
162
|
+
middle = len(ordered) // 2
|
|
163
|
+
if len(ordered) % 2:
|
|
164
|
+
return ordered[middle]
|
|
165
|
+
return (ordered[middle - 1] + ordered[middle]) / 2
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _minutes_block(prefix, durations):
|
|
169
|
+
"""One side of the cold/warm split, or {} when it has no runs."""
|
|
170
|
+
if not durations:
|
|
171
|
+
return {}
|
|
172
|
+
return {
|
|
173
|
+
f"{prefix}_minutes": round(_median(durations) / 60, 2),
|
|
174
|
+
f"{prefix}_runs": len(durations),
|
|
175
|
+
f"{prefix}_range_minutes": [
|
|
176
|
+
round(min(durations) / 60, 2),
|
|
177
|
+
round(max(durations) / 60, 2),
|
|
178
|
+
],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def observed_for(definition, rows, device=None, device_name=None, arguments=None):
|
|
183
|
+
"""This server's history for one workflow, as the `observed` block, or
|
|
184
|
+
None when it has nothing comparable to report.
|
|
185
|
+
|
|
186
|
+
`rows` are that workflow's finished runs as `history_rows` yields them.
|
|
187
|
+
The device is the server's current one: the history holds no device
|
|
188
|
+
column, so a figure is about whatever accelerator this box has now - true
|
|
189
|
+
for every box that has not had its card swapped, and `runs`/`since` are
|
|
190
|
+
what let a reader notice if it has.
|
|
191
|
+
|
|
192
|
+
`arguments` are the run being asked about, and they choose the bucket:
|
|
193
|
+
the listing asks with none, so its figure is the one the *defaults* give
|
|
194
|
+
and stays comparable to a curated `cost`, while a plan asks with the
|
|
195
|
+
caller's own values and gets the figure for the run it is quoting, or
|
|
196
|
+
None when this box has never run that shape (#154).
|
|
197
|
+
"""
|
|
198
|
+
wanted = _bucket_key(definition, arguments or {})
|
|
199
|
+
if wanted is None:
|
|
200
|
+
# No declared drivers and the caller overrode something: nothing here
|
|
201
|
+
# is comparable to the run being asked about
|
|
202
|
+
return None
|
|
203
|
+
task_only = _is_task_only(definition)
|
|
204
|
+
cold, warm, unclassified, earliest = [], [], 0, None
|
|
205
|
+
for row in rows:
|
|
206
|
+
if _every_step_was_reused(row["manifest"]):
|
|
207
|
+
continue
|
|
208
|
+
try:
|
|
209
|
+
row_arguments = json.loads(row["arguments"] or "{}")
|
|
210
|
+
except (TypeError, ValueError):
|
|
211
|
+
continue
|
|
212
|
+
if not isinstance(row_arguments, dict):
|
|
213
|
+
continue
|
|
214
|
+
if _bucket_key(definition, row_arguments) != wanted:
|
|
215
|
+
continue
|
|
216
|
+
duration = row["duration"]
|
|
217
|
+
if duration is None or duration <= 0:
|
|
218
|
+
continue
|
|
219
|
+
if task_only or row["had_load"]:
|
|
220
|
+
cold.append(duration)
|
|
221
|
+
elif row["events_at_cap"]:
|
|
222
|
+
unclassified += 1
|
|
223
|
+
else:
|
|
224
|
+
warm.append(duration)
|
|
225
|
+
finished = row["finished_at"]
|
|
226
|
+
if finished and (earliest is None or finished < earliest):
|
|
227
|
+
earliest = finished
|
|
228
|
+
|
|
229
|
+
runs = len(cold) + len(warm) + unclassified
|
|
230
|
+
if not runs:
|
|
231
|
+
return None
|
|
232
|
+
variables = definition.get("variables") or {}
|
|
233
|
+
drivers = declared_drivers(definition)
|
|
234
|
+
observed = {
|
|
235
|
+
"device": device,
|
|
236
|
+
"name": device_name,
|
|
237
|
+
"runs": runs,
|
|
238
|
+
"comparable": "drivers" if drivers else "default-arguments",
|
|
239
|
+
**_minutes_block("cold", cold),
|
|
240
|
+
**_minutes_block("warm", warm),
|
|
241
|
+
}
|
|
242
|
+
if drivers:
|
|
243
|
+
# A list driver is reported as the length it buckets on, so the
|
|
244
|
+
# figure says what it is a figure *for* without carrying a whole
|
|
245
|
+
# default shot list into the listing
|
|
246
|
+
# The *effective* driver values - the caller's where they supplied
|
|
247
|
+
# one, else the stored default - so the block says which run it is a
|
|
248
|
+
# figure for rather than always describing the defaults
|
|
249
|
+
effective = {
|
|
250
|
+
name: (arguments or {}).get(name, variables.get(name))
|
|
251
|
+
for name in sorted(drivers)
|
|
252
|
+
}
|
|
253
|
+
observed["drivers"] = {
|
|
254
|
+
name: len(value) if isinstance(value, list) else value
|
|
255
|
+
for name, value in effective.items()
|
|
256
|
+
}
|
|
257
|
+
if unclassified:
|
|
258
|
+
# Says the numbers do not add up, and why, rather than letting a
|
|
259
|
+
# reader assume a trimmed run was a warm one
|
|
260
|
+
observed["unclassified_runs"] = unclassified
|
|
261
|
+
if earliest:
|
|
262
|
+
observed["since"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(earliest))
|
|
263
|
+
return observed
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class ObservedCosts:
|
|
267
|
+
"""The `observed` block per workflow name, recomputed when the job table
|
|
268
|
+
has moved.
|
|
269
|
+
|
|
270
|
+
One query for the whole listing rather than one per workflow, cached
|
|
271
|
+
against the jobs table's own high-water mark - the same reason
|
|
272
|
+
`workflow_details` caches against a file's mtime, except that a job
|
|
273
|
+
landing changes every figure and no file changes at all.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def __init__(self, history):
|
|
277
|
+
self.history = history
|
|
278
|
+
self._mark = None
|
|
279
|
+
self._rows = {}
|
|
280
|
+
self._device = None
|
|
281
|
+
|
|
282
|
+
def device(self):
|
|
283
|
+
"""The accelerator every figure here is about, as (type, card name).
|
|
284
|
+
|
|
285
|
+
Computed once: it cannot change inside a process, and the listing is
|
|
286
|
+
the agent's hot path - asking torch for the card's name on every call
|
|
287
|
+
would be a torch round trip per catalog read. The history holds no
|
|
288
|
+
device column, so this is the server's current accelerator rather
|
|
289
|
+
than the one each run used; a box whose card was swapped mid-history
|
|
290
|
+
is the one case that misreports, and `runs`/`since` are what let a
|
|
291
|
+
reader notice.
|
|
292
|
+
"""
|
|
293
|
+
if self._device is None:
|
|
294
|
+
from .. import device_memory_stats, get_device, get_device_type
|
|
295
|
+
|
|
296
|
+
kind, card = None, None
|
|
297
|
+
try:
|
|
298
|
+
kind = get_device_type(get_device())
|
|
299
|
+
except Exception:
|
|
300
|
+
logger.debug("observed cost: could not read the device type")
|
|
301
|
+
try:
|
|
302
|
+
# The card's marketing name, which only CUDA reports. Its
|
|
303
|
+
# absence is not the device's absence, so the two are asked
|
|
304
|
+
# for separately - one try around both let an ImportError on
|
|
305
|
+
# the second answer `device: null` for a box plainly running
|
|
306
|
+
# on CUDA, which is the silent-null shape this whole field
|
|
307
|
+
# exists to replace
|
|
308
|
+
card = device_memory_stats().get("device_name")
|
|
309
|
+
except Exception:
|
|
310
|
+
logger.debug("observed cost: could not read the device name")
|
|
311
|
+
self._device = (kind, card)
|
|
312
|
+
return self._device
|
|
313
|
+
|
|
314
|
+
def refresh(self):
|
|
315
|
+
"""Bring the cached rows up to the table's watermark, once.
|
|
316
|
+
|
|
317
|
+
Returns False when there is no history to read or the read failed -
|
|
318
|
+
the caller then has no rows and knows why. A listing calls this
|
|
319
|
+
once and then asks `rows_for(name, fresh=False)` per entry, so the
|
|
320
|
+
COUNT(*) under the history lock happens once per request rather
|
|
321
|
+
than once per workflow."""
|
|
322
|
+
if self.history is None:
|
|
323
|
+
return False
|
|
324
|
+
try:
|
|
325
|
+
mark = self.history.watermark()
|
|
326
|
+
except Exception:
|
|
327
|
+
logger.debug("observed cost: could not read the job watermark")
|
|
328
|
+
return False
|
|
329
|
+
if mark != self._mark:
|
|
330
|
+
try:
|
|
331
|
+
self._rows = self.history.finished_runs()
|
|
332
|
+
except Exception:
|
|
333
|
+
logger.debug("observed cost: could not read job history")
|
|
334
|
+
self._rows = {}
|
|
335
|
+
return False
|
|
336
|
+
self._mark = mark
|
|
337
|
+
return True
|
|
338
|
+
|
|
339
|
+
def rows_for(self, name, *, fresh=True, workspace=None):
|
|
340
|
+
"""That workflow's comparable-candidate runs, refreshing the cache
|
|
341
|
+
when the table has moved (unless the caller already did).
|
|
342
|
+
|
|
343
|
+
`workspace` is the identity a workspace-writable workflow's own copy
|
|
344
|
+
is scoped to (#274): two workspaces that each save a workflow called
|
|
345
|
+
the same thing are different workflows, so passing a workspace name
|
|
346
|
+
answers only for that one's own history and no other. Omitting it -
|
|
347
|
+
what a shared, read-only catalog source (a template or example) is
|
|
348
|
+
asked with - keeps #154's behavior: every workspace's runs of that
|
|
349
|
+
catalog entry are one pool, unioned across whichever workspaces
|
|
350
|
+
happen to have run it.
|
|
351
|
+
"""
|
|
352
|
+
if fresh and not self.refresh():
|
|
353
|
+
return []
|
|
354
|
+
if workspace is not None:
|
|
355
|
+
return self._rows.get((workspace, name), [])
|
|
356
|
+
rows = []
|
|
357
|
+
for (row_workspace, row_name), workspace_rows in self._rows.items():
|
|
358
|
+
if row_name == name:
|
|
359
|
+
rows.extend(workspace_rows)
|
|
360
|
+
return rows
|
|
361
|
+
|
|
362
|
+
def observed(self, name, definition, arguments=None, *, fresh=True, workspace=None):
|
|
363
|
+
rows = self.rows_for(name, fresh=fresh, workspace=workspace)
|
|
364
|
+
if not rows:
|
|
365
|
+
return None
|
|
366
|
+
device, device_name = self.device()
|
|
367
|
+
try:
|
|
368
|
+
return observed_for(
|
|
369
|
+
definition,
|
|
370
|
+
rows,
|
|
371
|
+
device=device,
|
|
372
|
+
device_name=device_name,
|
|
373
|
+
arguments=arguments,
|
|
374
|
+
)
|
|
375
|
+
except Exception:
|
|
376
|
+
# A figure is a nicety; a listing that 500s because one row had a
|
|
377
|
+
# shape nobody expected is not
|
|
378
|
+
logger.debug("observed cost: could not aggregate %s", name, exc_info=True)
|
|
379
|
+
return None
|
dw/server/sysinfo.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Runtime environment details - Python, torch, CUDA/driver and other
|
|
2
|
+
installed package versions - for diagnosing environment mismatches between
|
|
3
|
+
boxes (#222). Read-only, gathered fresh on every call rather than cached,
|
|
4
|
+
since an install can change underneath a long-running server (see
|
|
5
|
+
`update_diffusers`).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import platform
|
|
9
|
+
import subprocess
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
|
|
12
|
+
# Packages worth reporting beyond torch (which gets its own field): the
|
|
13
|
+
# ones most likely to cause a version-mismatch failure or be silently
|
|
14
|
+
# absent on a given box.
|
|
15
|
+
PACKAGES = (
|
|
16
|
+
"diffusers",
|
|
17
|
+
"transformers",
|
|
18
|
+
"accelerate",
|
|
19
|
+
"bitsandbytes",
|
|
20
|
+
"peft",
|
|
21
|
+
"safetensors",
|
|
22
|
+
"sentencepiece",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _package_version(name):
|
|
27
|
+
try:
|
|
28
|
+
return version(name)
|
|
29
|
+
except PackageNotFoundError:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _nvidia_driver_version():
|
|
34
|
+
"""The NVIDIA driver version, via nvidia-smi when it's on PATH. Distinct
|
|
35
|
+
from torch's own `cuda_version`, which is the CUDA toolkit torch was
|
|
36
|
+
built against, not the driver actually installed on this box."""
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["nvidia-smi", "--query-gpu=driver_version", "--format=csv,noheader"],
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
timeout=5,
|
|
43
|
+
)
|
|
44
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
45
|
+
return None
|
|
46
|
+
if result.returncode != 0:
|
|
47
|
+
return None
|
|
48
|
+
lines = result.stdout.strip().splitlines()
|
|
49
|
+
return lines[0].strip() if lines else None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def runtime_info():
|
|
53
|
+
"""Python, torch, CUDA/driver and other installed package versions -
|
|
54
|
+
the detail `device`/`version` alone doesn't answer, like "is
|
|
55
|
+
bitsandbytes even installed here" or "which CUDA build is torch"."""
|
|
56
|
+
try:
|
|
57
|
+
import torch
|
|
58
|
+
|
|
59
|
+
torch_version = torch.__version__
|
|
60
|
+
cuda_version = torch.version.cuda
|
|
61
|
+
except ImportError:
|
|
62
|
+
torch_version = None
|
|
63
|
+
cuda_version = None
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"python_version": platform.python_version(),
|
|
67
|
+
"torch_version": torch_version,
|
|
68
|
+
"cuda_version": cuda_version,
|
|
69
|
+
"driver_version": _nvidia_driver_version(),
|
|
70
|
+
"packages": {name: _package_version(name) for name in PACKAGES},
|
|
71
|
+
}
|