Metadata-Version: 2.5
Name: giul
Version: 0.1.0
Summary: One ruler for what an answer cost in joules, across a GPU fleet.
Project-URL: Homepage, https://github.com/todd427/giul
Author: Todd McCaffrey
License-Expression: Apache-2.0
Keywords: energy,gpu,joules,nvml,telemetry
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.11
Provides-Extra: nvml
Requires-Dist: nvidia-ml-py>=12.535; extra == 'nvml'
Provides-Extra: remote
Requires-Dist: httpx>=0.24; extra == 'remote'
Description-Content-Type: text/markdown

# 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.

```bash
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

1. **Charged above idle.** As above.
2. **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) or `unknown` — never a `sampled` zero.
3. **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.

## Use

```python
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.

**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

```bash
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
```

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.
