giul
giúl — Irish for joule. One ruler for "what did this answer cost in energy" across the FoxxeLabs fleet: Aigne, Tuiscint, Gléas.
Two rulers make a comparison an argument instead of evidence. This is one, and all three pin it.
pip install giul # no dependencies; imports fine with no GPU
pip install giul[nvml] # + nvidia-ml-py, for the energy counter
pip install giul[remote] # + httpx, for metering a remote node
What a joule means here
The number a request is charged is energy above idle:
joules = measured − idle_w × seconds
A request pays for the power it caused, not for the power the box burns
existing. idle_w is supplied by the caller, per node — giul never guesses
it, because a wrong idle floor silently rewrites every figure that node
reports.
Three rules
- Charged above idle. As above.
- No fabricated zeros. A window that measured ≤ 0 J above idle did not
measure a free request, it failed to measure one. It degrades to
estimated(with a hint) orunknown— never asampledzero. - Estimates are labelled as estimates. An estimate that reads as a measurement is exactly how an efficiency claim stops being evidence. A sampler that cannot be reached costs a measurement, never the request.
- A shared card is witnessed, not measured. Board power is per-card and
nvidia-smi attributes no watts to a PID, so a foreign process's draw lands
on whatever request is in flight. Such a window records
contended: trueandcontended_by, and is demoted out ofsampled— the number is real, it is simply not yours. (0.2)
Use
from giul import Meter, Node
node = Node(name="iris", gpu_index=0, idle_w=38.0,
is_local=True, power_endpoint=None)
# async
async with Meter.for_node(node, joules_per_1k_hint=4100.0) as m:
await retrieve(); m.mark("retrieve")
await generate(); m.mark("generate")
e = m.result(tokens=412)
# e.joules, e.method, e.backend, e.stages == {"retrieve": Energy, "generate": Energy}
# sync
with Meter.for_node(node, sync=torch.cuda.synchronize).sync() as m:
...; m.mark("verify")
e = m.result(tokens=n)
mark(name) closes the current stage and opens the next. The total is always
computed over the whole window, never by summing stages, so a stage the
sampler was too slow to see cannot corrupt it.
Contention (0.2)
Tell the node what your own serving process looks like, and anything else resident is a tenant:
node = Node(name="iris", gpu_index=0, idle_w=16.0, is_local=True,
serving_match="vllm", # ours
contention_ignore=("chrome", "Xorg"), # a display, not a competitor
contention_floor_mib=512) # too small to matter
A node that sets no serving_match is assumed dedicated and looks for nothing
— 0.1 behaviour, unchanged. Two things learned the hard way and built in:
- A display is excluded by name, not by size. The same idle compositor was
measured at 300 MiB and 594 MiB an hour apart. A floor tuned to exclude it
today admits it tomorrow, and then every window on that node reads
contendedand the measured series is silently empty. - Two processes matching
serving_matchis contention. Another project ran vLLM for offline eval and nvidia-smi reported it asVLLM::EngineCore, character for character what the serving tier reports. When more than one is resident, none can be assumed yours.
Checked at the window's two edges, not in the poll loop. A tenant that arrives and leaves strictly inside one short window is missed — a known gap, and the honest direction to miss in.
Probing a card (0.2)
from giul import probe_node
state = await probe_node(node) # None if unreadable — unknown, not fine
# state["memory_free_mib"], state["utilization_pct"], state["compute_apps"]
For callers deciding whether to spend something on a card. vLLM sizes its
pool as a fraction of total VRAM but refuses to start if that exceeds what is
free, so a wake into a card with a tenant on it does not fail — it
crash-loops until a timeout. probe_node lets the caller refuse in
milliseconds with the shortfall named.
The caller synchronises the GPU before the closing read. On the counter
backend the register only counts work the card has finished; pass
sync=torch.cuda.synchronize when the work is local. An HTTP upstream needs
nothing — the completion returning is the sync point.
Backends
Chosen at runtime in one place (Meter.for_node), needing no per-node
configuration. Support is probed once per card and cached.
backend |
when | how |
|---|---|---|
nvml_counter |
local card, pynvml present, counter answers |
nvmlDeviceGetTotalEnergyConsumption delta — a true measurement of a sub-second window |
smi_sampler |
local card, nvidia-smi on PATH |
integral of power.draw samples |
remote_agent |
node.power_endpoint set |
the same integral, sampled by giul-agent on that node |
none |
otherwise | estimated with a hint, else unknown |
method stays sampled / estimated / unknown. The counter is a
measurement, so it reports method="sampled"; the distinction lives in
backend.
Tools
giul-probe # what this host's cards support; counter vs sampler
giul-agent --port 9402 # expose a node's power draw; stdlib only, read-only
/power returns watts, name, memory_used_mib, memory_total_mib,
memory_free_mib, utilization_pct, and compute_apps (pid, used_mib,
name per resident process). Every 0.1 field is unchanged; the rest is 0.2.
Bind giul-agent to the mesh address, not 0.0.0.0, unless the node is
otherwise firewalled.
Not in 0.1
CPU/RAPL, carbon, € cost, storing series, any UI.
Metadata
Release files for giul 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| giul-0.2.0.tar.gz | 25.2 kB | Without provenance | Details for giul-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| giul-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for giul-0.2.0-py3-none-any.whl |
Total release size: 46.1 kB
Release files / giul-0.2.0.tar.gz
| Download URL | giul-0.2.0.tar.gz |
|---|---|
| Size | 25.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6c1d9c28926d94de623dd6da07c50fa47b97ee740e871dc54481f6aa56d99a9f
|
|
BLAKE2b-256 checksum How to use checksums |
82ebcd976db9931130ac8bf9f865fe773b9d386b3fb1dc2acd54cac30231af45
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / giul-0.2.0-py3-none-any.whl
| Download URL | giul-0.2.0-py3-none-any.whl |
|---|---|
| Size | 20.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b0b241bf1bc80cd534eb9476fbc0f7fbe78765be0db8e0ba163be78a330104d9
|
|
BLAKE2b-256 checksum How to use checksums |
bee358f0dcb4ca0c49729b2fe6b070c2c13378d517590c3be4d41a85ca44848a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for giul 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.