daemon¶
Warm-process render daemon: re-run a scene script without paying startup.
Every fresh python scene.py pays several seconds of library import plus
~20 s of Taichi kernel preparation before the first pixel renders, even with a
warm offline cache. This daemon pays them once: it keeps the process (and
Taichi’s in-process kernel cache) alive and re-executes the scene script on
demand, so from the second render on the only cost is the render itself.
Usage:
.venv/Scripts/python.exe -m algan.daemon # general daemon
.venv/Scripts/python.exe -m algan.daemon scene.py [options] [-- script args]
Launching one by hand is optional. An ordinary python scene.py starts a
general daemon itself when none is running, runs on it, and leaves it warm for
the next script (see algan.daemon_client; ALGAN_AUTO_DAEMON=0
disables it, ALGAN_USE_DAEMON=0 disables the daemon entirely). Launch one
by hand when you want it in a terminal you can watch, or want Enter-to-re-render.
General mode (no SCRIPT) is the one to leave running. The daemon publishes
a state file at $ALGAN_HOME/daemon.json and then serves whatever scripts
come to it: every subsequent python any_scene.py notices the state file
during import algan and hands itself over, so scripts are launched exactly
as they always were and simply start rendering in ~1 s.
Concurrent scripts are queued and run one at a time, in arrival order – which is also what this project needs on Windows, where two live render processes fight over the output mp4. A waiting client is told its position.
Triggers (a render is never interrupted; triggers arriving mid-render coalesce into at most one queued re-run):
Enter in the daemon terminal re-renders;
qquits. This is the primary workflow: edit in your editor, save, switch to the daemon, Enter.A localhost TCP socket (preferred port 46711;
--port, or envALGAN_DAEMON_PORT;--no-servedisables) accepts the line commandsrender/ping/quit, each of which must carry the token from the state file. If the preferred port is taken the daemon binds an ephemeral one instead of exiting, and publishes it in the state file – which is where clients look anyway. Bind an editor key to:algan daemon render # also: algan daemon ping, algan daemon quit
Those subcommands read
$ALGAN_HOME/daemon.jsonfor the port and the token and send the line for you.--watchre-renders when the scene script or any of its sibling helper modules change on disk (polled; coalesced; never interrupts).
When a run ends the daemon restores a clean slate, before it goes idle rather than at the start of the next run – so what it holds while waiting is the warm process and nothing else:
SceneManager.reset()– fresh scene, camera, light and timeline.SETTINGS.snapshot()/SETTINGS.restore()resets every public runtime settings section to its import-time value, so one run cannot leak configuration into the next. Private adaptive renderer state is retained.User helper modules – modules imported from the script’s directory tree are evicted from
sys.modulesso the next run picks up their edits (the daemon prints what it evicted). Modules imported from elsewhere are NOT reloaded.The render’s GPU memory goes back to the driver: one
gc.collect()(the scene’s object graph is cyclic, so refcounting alone frees almost none of it) and onetorch.cuda.empty_cache(). Measured on a 4 GB card, an idle daemon holding 1.6 GB after a 90-frame render now holds ~0.1 GB, at a cost of ~0.15 s and no measurable change to the next render.ALGAN_DAEMON_RELEASE_MEMORY=0keeps the memory cached instead.
Edits to algan itself are handled, not merely warned about. The daemon
fingerprints every algan source file at startup and re-checks at every run
launch; if anything changed it refuses the run and shuts down, so the script
executes in a fresh process that loads the edited code. A new daemon starts on
the next run. This costs a cold start – that is what editing the library has
always cost – but it can no longer render with stale modules or compile
mixed-version kernels from a half-edited *_taichi.py. An edit that lands
during a run is not caught, exactly as it is not caught for a plain python
scene.py. Keep to one rendering process at a time on Windows.
A run served here is meant to be indistinguishable from one in its own
process: sys.argv, the working directory, the caller’s full environment,
stdout/stderr at the descriptor level (so ffmpeg and other subprocesses reach
the caller) and their tty-ness are all reproduced. stdin is not – it is
connected to os.devnull, because the daemon’s own stdin is its re-render
trigger – and atexit handlers do not run, because runpy does not run
them and a warm process never shuts down.
Three more limits that are specific to serving other processes:
Startup-only settings cannot be adopted from a client, with one exception.
ALGAN_ANIMATION_DEVICEand friends are read while Torch/Taichi initialise, i.e. when the daemon started. A script that sets one to a different value is refused with an explanation and runs cold in its own process, rather than being silently rendered on the wrong device.ALGAN_RENDER_DEVICEis adopted (_adopt_render_device()): it only seedsSETTINGS.computing.render_device, which owns the value from then on, and every render re-selects Taichi’s arch from it. A script that wants the other device is served warm; if that crosses the CPU/GPU line its first render pays one kernel-preparation pass, which is still far less than the cold start refusing it used to cost.Neither can settings read while algan is imported. The renderer’s toggles become module-level defaults during
import algan, which in a daemon happened at its launch – so a script that sets one before its ownimport algan, the way every A/B script inbenchmarks/selects an arm, would otherwise be served by a process that never saw it and would render with the daemon’s values instead. Those are refused too (algan.daemon_client.describe_import_env_mismatch()); the swapped-in environment covers every variable read live, so flipping one during a run works here exactly as it does cold. The corollary is that a daemon started by a script with non-default toggles serves only scripts that set the same ones: stop it if you want one baked with the defaults.Anything that can reach 127.0.0.1 can ask the daemon to execute a path. Every request –
run,cancel,render,pingandquitalike – must carry the token from the state file, which lives in the user’s home directory (mode 0600 where the platform honours it). Do not forward the port off-host.The daemon must be the same Algan. The state file records the interpreter, the prefix, the package directory and the version it was started with, and a client whose own differ is not served: it runs cold in its own process rather than executing against another virtualenv’s site-packages (
algan.daemon_client.describe_interpreter_mismatch()).
Functions
- default_port()[source]¶
The trigger socket’s default port.
Read when the command line is parsed rather than at import: it configures the client/daemon transport, not anything a script renders, so there is no reason a warm process could not honour a value set after its own import.
- strip_plumbing_frames(tb)[source]¶
Drop the daemon’s and
runpy’s frames from the top oftb.A script that raises under the daemon showed
algan/daemon.py ... in executeand tworunpyframes above its own first line – frames a plainpython scene.pywould never print and that say nothing about the error. Only the leading run of them is dropped, so a script that itself callsrunpystill shows that call.Returns
tbunchanged if every frame is plumbing: an empty traceback would be worse than an honest one.