lumis-ledger 0.1.0__tar.gz
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.
- lumis_ledger-0.1.0/LICENSE +21 -0
- lumis_ledger-0.1.0/PKG-INFO +90 -0
- lumis_ledger-0.1.0/README.md +74 -0
- lumis_ledger-0.1.0/lumis_ledger.egg-info/PKG-INFO +90 -0
- lumis_ledger-0.1.0/lumis_ledger.egg-info/SOURCES.txt +11 -0
- lumis_ledger-0.1.0/lumis_ledger.egg-info/dependency_links.txt +1 -0
- lumis_ledger-0.1.0/lumis_ledger.egg-info/entry_points.txt +2 -0
- lumis_ledger-0.1.0/lumis_ledger.egg-info/requires.txt +2 -0
- lumis_ledger-0.1.0/lumis_ledger.egg-info/top_level.txt +1 -0
- lumis_ledger-0.1.0/lumis_ledger.py +256 -0
- lumis_ledger-0.1.0/pyproject.toml +24 -0
- lumis_ledger-0.1.0/setup.cfg +4 -0
- lumis_ledger-0.1.0/tests/test_ledger.py +41 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Axiom Photonics Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lumis-ledger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An honest, read-only power ledger for your machine, from Axiom Photonics.
|
|
5
|
+
Author: Axiom Photonics Ltd
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Pr3ndy-ctrl/lumis-ledger
|
|
8
|
+
Project-URL: SDK, https://pypi.org/project/axiom-lumis/
|
|
9
|
+
Keywords: power,energy,monitor,photonics,lumis,cli
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: psutil>=5.9
|
|
14
|
+
Requires-Dist: rich>=13.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# lumis-ledger
|
|
18
|
+
|
|
19
|
+
An honest, read-only power ledger for your machine. A small cross-platform
|
|
20
|
+
terminal tool from [Axiom Photonics](https://axiomprocessor.com).
|
|
21
|
+
|
|
22
|
+
It shows where your compute is going, and it is honest about what it cannot
|
|
23
|
+
measure. On most platforms software cannot read true wall power, so it says so
|
|
24
|
+
instead of inventing a number. Where the OS does expose energy (Linux RAPL) it
|
|
25
|
+
reports the measured figure.
|
|
26
|
+
|
|
27
|
+
It also carries the one thing a normal system monitor does not: a **LUMIS
|
|
28
|
+
estimator**. Give it a layer size and it prints what that linear layer would
|
|
29
|
+
cost on a LUMIS Rev 1 photonic node, as a labelled pre-silicon design target,
|
|
30
|
+
beside a rough digital figure.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install lumis-ledger
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
lumis-ledger # one snapshot of this machine
|
|
40
|
+
lumis-ledger --watch # refresh until Ctrl-C
|
|
41
|
+
lumis-ledger estimate --size 64 # what a 64x64 layer costs on a photonic node
|
|
42
|
+
lumis-ledger estimate --size 128 --rate 1e10
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What it measures, and what it does not
|
|
46
|
+
|
|
47
|
+
- **Compute load, memory, swap, load average, battery, top consumers**: read
|
|
48
|
+
live from the OS through `psutil`. These are real.
|
|
49
|
+
- **Total power in watts**: reported only where the OS exposes it. On Linux with
|
|
50
|
+
RAPL you get a measured figure. On macOS, Windows and most virtual machines
|
|
51
|
+
the wall power is not available to software, so the tool shows `—` and says
|
|
52
|
+
"not directly measurable" rather than guessing. That honesty is the point.
|
|
53
|
+
- **The platform idle floor**: not exposed by any consumer OS, so it is left
|
|
54
|
+
blank rather than modelled.
|
|
55
|
+
|
|
56
|
+
## Read only
|
|
57
|
+
|
|
58
|
+
This tool changes nothing on your system, runs no privileged operations, and
|
|
59
|
+
installs no background service. It reads counters and prints them. That is all.
|
|
60
|
+
|
|
61
|
+
## The LUMIS estimator
|
|
62
|
+
|
|
63
|
+
`lumis-ledger estimate` applies the same energy model as the Axiom photonic
|
|
64
|
+
compiler. A photonic mesh does the matrix multiply by propagating light, so the
|
|
65
|
+
arithmetic is effectively free; what the node draws is a fixed floor (drivers,
|
|
66
|
+
laser and electro-optic hold) that does not grow with the compute rate. So the
|
|
67
|
+
comparison against a digital machine is not a flat "we win", it is a breakeven:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
A 64x64 linear layer on a photonic node (LUMIS Rev 1 design targets)
|
|
71
|
+
MZIs in the mesh 2016
|
|
72
|
+
DIGITAL @ 1e9/s 18.842 W (scales with the rate)
|
|
73
|
+
PHOTONIC floor 7.000 W (flat vs the rate)
|
|
74
|
+
BREAKEVEN 3.72e8 matvec/s -> above it the photonic node wins
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Every hardware figure is a **design target for a chip that is not yet
|
|
78
|
+
fabricated**, and every one is reproducible with the free SDK:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
pip install axiom-lumis
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The SDK is the real compiler (analytic Clements decomposition, machine-precision
|
|
85
|
+
reconstruction, admission against the 8 W node envelope). This tool is just a
|
|
86
|
+
friendly window onto its energy model, on hardware you already own.
|
|
87
|
+
|
|
88
|
+
## Licence
|
|
89
|
+
|
|
90
|
+
MIT. Free to use, fork and ship.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# lumis-ledger
|
|
2
|
+
|
|
3
|
+
An honest, read-only power ledger for your machine. A small cross-platform
|
|
4
|
+
terminal tool from [Axiom Photonics](https://axiomprocessor.com).
|
|
5
|
+
|
|
6
|
+
It shows where your compute is going, and it is honest about what it cannot
|
|
7
|
+
measure. On most platforms software cannot read true wall power, so it says so
|
|
8
|
+
instead of inventing a number. Where the OS does expose energy (Linux RAPL) it
|
|
9
|
+
reports the measured figure.
|
|
10
|
+
|
|
11
|
+
It also carries the one thing a normal system monitor does not: a **LUMIS
|
|
12
|
+
estimator**. Give it a layer size and it prints what that linear layer would
|
|
13
|
+
cost on a LUMIS Rev 1 photonic node, as a labelled pre-silicon design target,
|
|
14
|
+
beside a rough digital figure.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pip install lumis-ledger
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Use
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
lumis-ledger # one snapshot of this machine
|
|
24
|
+
lumis-ledger --watch # refresh until Ctrl-C
|
|
25
|
+
lumis-ledger estimate --size 64 # what a 64x64 layer costs on a photonic node
|
|
26
|
+
lumis-ledger estimate --size 128 --rate 1e10
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What it measures, and what it does not
|
|
30
|
+
|
|
31
|
+
- **Compute load, memory, swap, load average, battery, top consumers**: read
|
|
32
|
+
live from the OS through `psutil`. These are real.
|
|
33
|
+
- **Total power in watts**: reported only where the OS exposes it. On Linux with
|
|
34
|
+
RAPL you get a measured figure. On macOS, Windows and most virtual machines
|
|
35
|
+
the wall power is not available to software, so the tool shows `—` and says
|
|
36
|
+
"not directly measurable" rather than guessing. That honesty is the point.
|
|
37
|
+
- **The platform idle floor**: not exposed by any consumer OS, so it is left
|
|
38
|
+
blank rather than modelled.
|
|
39
|
+
|
|
40
|
+
## Read only
|
|
41
|
+
|
|
42
|
+
This tool changes nothing on your system, runs no privileged operations, and
|
|
43
|
+
installs no background service. It reads counters and prints them. That is all.
|
|
44
|
+
|
|
45
|
+
## The LUMIS estimator
|
|
46
|
+
|
|
47
|
+
`lumis-ledger estimate` applies the same energy model as the Axiom photonic
|
|
48
|
+
compiler. A photonic mesh does the matrix multiply by propagating light, so the
|
|
49
|
+
arithmetic is effectively free; what the node draws is a fixed floor (drivers,
|
|
50
|
+
laser and electro-optic hold) that does not grow with the compute rate. So the
|
|
51
|
+
comparison against a digital machine is not a flat "we win", it is a breakeven:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
A 64x64 linear layer on a photonic node (LUMIS Rev 1 design targets)
|
|
55
|
+
MZIs in the mesh 2016
|
|
56
|
+
DIGITAL @ 1e9/s 18.842 W (scales with the rate)
|
|
57
|
+
PHOTONIC floor 7.000 W (flat vs the rate)
|
|
58
|
+
BREAKEVEN 3.72e8 matvec/s -> above it the photonic node wins
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Every hardware figure is a **design target for a chip that is not yet
|
|
62
|
+
fabricated**, and every one is reproducible with the free SDK:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
pip install axiom-lumis
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The SDK is the real compiler (analytic Clements decomposition, machine-precision
|
|
69
|
+
reconstruction, admission against the 8 W node envelope). This tool is just a
|
|
70
|
+
friendly window onto its energy model, on hardware you already own.
|
|
71
|
+
|
|
72
|
+
## Licence
|
|
73
|
+
|
|
74
|
+
MIT. Free to use, fork and ship.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lumis-ledger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An honest, read-only power ledger for your machine, from Axiom Photonics.
|
|
5
|
+
Author: Axiom Photonics Ltd
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Pr3ndy-ctrl/lumis-ledger
|
|
8
|
+
Project-URL: SDK, https://pypi.org/project/axiom-lumis/
|
|
9
|
+
Keywords: power,energy,monitor,photonics,lumis,cli
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: psutil>=5.9
|
|
14
|
+
Requires-Dist: rich>=13.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# lumis-ledger
|
|
18
|
+
|
|
19
|
+
An honest, read-only power ledger for your machine. A small cross-platform
|
|
20
|
+
terminal tool from [Axiom Photonics](https://axiomprocessor.com).
|
|
21
|
+
|
|
22
|
+
It shows where your compute is going, and it is honest about what it cannot
|
|
23
|
+
measure. On most platforms software cannot read true wall power, so it says so
|
|
24
|
+
instead of inventing a number. Where the OS does expose energy (Linux RAPL) it
|
|
25
|
+
reports the measured figure.
|
|
26
|
+
|
|
27
|
+
It also carries the one thing a normal system monitor does not: a **LUMIS
|
|
28
|
+
estimator**. Give it a layer size and it prints what that linear layer would
|
|
29
|
+
cost on a LUMIS Rev 1 photonic node, as a labelled pre-silicon design target,
|
|
30
|
+
beside a rough digital figure.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install lumis-ledger
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
lumis-ledger # one snapshot of this machine
|
|
40
|
+
lumis-ledger --watch # refresh until Ctrl-C
|
|
41
|
+
lumis-ledger estimate --size 64 # what a 64x64 layer costs on a photonic node
|
|
42
|
+
lumis-ledger estimate --size 128 --rate 1e10
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What it measures, and what it does not
|
|
46
|
+
|
|
47
|
+
- **Compute load, memory, swap, load average, battery, top consumers**: read
|
|
48
|
+
live from the OS through `psutil`. These are real.
|
|
49
|
+
- **Total power in watts**: reported only where the OS exposes it. On Linux with
|
|
50
|
+
RAPL you get a measured figure. On macOS, Windows and most virtual machines
|
|
51
|
+
the wall power is not available to software, so the tool shows `—` and says
|
|
52
|
+
"not directly measurable" rather than guessing. That honesty is the point.
|
|
53
|
+
- **The platform idle floor**: not exposed by any consumer OS, so it is left
|
|
54
|
+
blank rather than modelled.
|
|
55
|
+
|
|
56
|
+
## Read only
|
|
57
|
+
|
|
58
|
+
This tool changes nothing on your system, runs no privileged operations, and
|
|
59
|
+
installs no background service. It reads counters and prints them. That is all.
|
|
60
|
+
|
|
61
|
+
## The LUMIS estimator
|
|
62
|
+
|
|
63
|
+
`lumis-ledger estimate` applies the same energy model as the Axiom photonic
|
|
64
|
+
compiler. A photonic mesh does the matrix multiply by propagating light, so the
|
|
65
|
+
arithmetic is effectively free; what the node draws is a fixed floor (drivers,
|
|
66
|
+
laser and electro-optic hold) that does not grow with the compute rate. So the
|
|
67
|
+
comparison against a digital machine is not a flat "we win", it is a breakeven:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
A 64x64 linear layer on a photonic node (LUMIS Rev 1 design targets)
|
|
71
|
+
MZIs in the mesh 2016
|
|
72
|
+
DIGITAL @ 1e9/s 18.842 W (scales with the rate)
|
|
73
|
+
PHOTONIC floor 7.000 W (flat vs the rate)
|
|
74
|
+
BREAKEVEN 3.72e8 matvec/s -> above it the photonic node wins
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Every hardware figure is a **design target for a chip that is not yet
|
|
78
|
+
fabricated**, and every one is reproducible with the free SDK:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
pip install axiom-lumis
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The SDK is the real compiler (analytic Clements decomposition, machine-precision
|
|
85
|
+
reconstruction, admission against the 8 W node envelope). This tool is just a
|
|
86
|
+
friendly window onto its energy model, on hardware you already own.
|
|
87
|
+
|
|
88
|
+
## Licence
|
|
89
|
+
|
|
90
|
+
MIT. Free to use, fork and ship.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
lumis_ledger.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
lumis_ledger.egg-info/PKG-INFO
|
|
6
|
+
lumis_ledger.egg-info/SOURCES.txt
|
|
7
|
+
lumis_ledger.egg-info/dependency_links.txt
|
|
8
|
+
lumis_ledger.egg-info/entry_points.txt
|
|
9
|
+
lumis_ledger.egg-info/requires.txt
|
|
10
|
+
lumis_ledger.egg-info/top_level.txt
|
|
11
|
+
tests/test_ledger.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lumis_ledger
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lumis-ledger: an honest, read-only power ledger for your machine.
|
|
3
|
+
|
|
4
|
+
A small cross-platform terminal tool from Axiom Photonics. It shows where your
|
|
5
|
+
machine's compute is going, and it is honest about what it cannot measure: on
|
|
6
|
+
most platforms software cannot read true wall power, so it says so rather than
|
|
7
|
+
inventing a number. Where the OS does expose energy (Linux RAPL), it reports the
|
|
8
|
+
measured figure.
|
|
9
|
+
|
|
10
|
+
It also carries the one thing a normal system monitor does not: a LUMIS
|
|
11
|
+
estimator. Give it a layer size and it prints what that linear layer would cost
|
|
12
|
+
on a LUMIS Rev 1 photonic node, as a labelled pre-silicon design target, beside
|
|
13
|
+
a rough digital figure. Those numbers are reproducible with the free SDK:
|
|
14
|
+
pip install axiom-lumis.
|
|
15
|
+
|
|
16
|
+
This tool is READ ONLY. It changes nothing on your system and needs no elevated
|
|
17
|
+
privileges. MIT licensed. https://github.com/Pr3ndy-ctrl/lumis-ledger
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import glob
|
|
23
|
+
import os
|
|
24
|
+
import time
|
|
25
|
+
|
|
26
|
+
import psutil
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
SDK_URL = "pip install axiom-lumis"
|
|
30
|
+
|
|
31
|
+
# --- LUMIS Rev 1 design-target constants (identical to the lumis-nano ledger) ---
|
|
32
|
+
# EO phase shifters hold a voltage, not a current, so static hold is tiny.
|
|
33
|
+
# Every figure here is a design target for a chip that is not yet fabricated.
|
|
34
|
+
P_DRIVE_LASER = 6.8 # W, modulator drivers + laser coupling floor per node
|
|
35
|
+
HOLD_PER_MZI = 0.2 / 2016 # W, EO static hold per MZI (<0.2 W across a full mesh)
|
|
36
|
+
E_MAC_DIGITAL = 4.6e-12 # J per fp32 multiply-accumulate (45 nm, Horowitz 2014)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ----------------------------------------------------------------- power reading
|
|
40
|
+
def read_rapl_power(interval: float = 0.3):
|
|
41
|
+
"""Measured package power in watts via Linux RAPL, or None where unavailable.
|
|
42
|
+
|
|
43
|
+
RAPL exposes a monotonically increasing energy counter in microjoules; the
|
|
44
|
+
power is the difference over a short interval. Absent on macOS, Windows,
|
|
45
|
+
most VMs and this value is simply not reported there."""
|
|
46
|
+
paths = sorted(glob.glob("/sys/class/powercap/intel-rapl:*/energy_uj"))
|
|
47
|
+
paths = [p for p in paths if "intel-rapl:" in p and p.count(":") == 1]
|
|
48
|
+
if not paths:
|
|
49
|
+
return None
|
|
50
|
+
try:
|
|
51
|
+
def total_uj():
|
|
52
|
+
return sum(int(open(p).read()) for p in paths)
|
|
53
|
+
a = total_uj()
|
|
54
|
+
time.sleep(interval)
|
|
55
|
+
b = total_uj()
|
|
56
|
+
if b < a: # counter wrapped
|
|
57
|
+
return None
|
|
58
|
+
return (b - a) / 1e6 / interval
|
|
59
|
+
except (OSError, ValueError, PermissionError):
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sample(interval: float = 0.6) -> dict:
|
|
64
|
+
"""One telemetry snapshot. The interval is the window CPU load is measured
|
|
65
|
+
over, so the first call blocks for `interval` seconds to get a real number."""
|
|
66
|
+
# prime per-process cpu counters, then measure over the window
|
|
67
|
+
procs = list(psutil.process_iter(["name"]))
|
|
68
|
+
for p in procs:
|
|
69
|
+
try:
|
|
70
|
+
p.cpu_percent(None)
|
|
71
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
72
|
+
pass
|
|
73
|
+
cpu_busy = psutil.cpu_percent(interval) # blocks `interval`, returns 0..100
|
|
74
|
+
|
|
75
|
+
consumers = []
|
|
76
|
+
ncpu = psutil.cpu_count() or 1
|
|
77
|
+
for p in procs:
|
|
78
|
+
try:
|
|
79
|
+
pct = p.cpu_percent(None) / ncpu # normalise to whole-machine share
|
|
80
|
+
name = p.info.get("name") or "?"
|
|
81
|
+
if pct > 0:
|
|
82
|
+
consumers.append((pct, name))
|
|
83
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
84
|
+
pass
|
|
85
|
+
consumers.sort(reverse=True)
|
|
86
|
+
|
|
87
|
+
vm = psutil.virtual_memory()
|
|
88
|
+
sw = psutil.swap_memory()
|
|
89
|
+
try:
|
|
90
|
+
load = os.getloadavg()[0]
|
|
91
|
+
except (OSError, AttributeError):
|
|
92
|
+
load = None
|
|
93
|
+
try:
|
|
94
|
+
batt = psutil.sensors_battery()
|
|
95
|
+
except (AttributeError, NotImplementedError):
|
|
96
|
+
batt = None
|
|
97
|
+
|
|
98
|
+
watts = read_rapl_power()
|
|
99
|
+
return {
|
|
100
|
+
"cpu_busy": cpu_busy,
|
|
101
|
+
"ncpu": ncpu,
|
|
102
|
+
"consumers": consumers,
|
|
103
|
+
"mem_used_gb": (vm.total - vm.available) / 1e9,
|
|
104
|
+
"mem_total_gb": vm.total / 1e9,
|
|
105
|
+
"swap_gb": sw.used / 1e9,
|
|
106
|
+
"load": load,
|
|
107
|
+
"battery": batt,
|
|
108
|
+
"watts": watts,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ----------------------------------------------------------------- LUMIS estimate
|
|
113
|
+
def lumis_estimate(n: int, rate_hz: float = 1e9) -> dict:
|
|
114
|
+
"""What an n-mode linear layer costs on a LUMIS Rev 1 node (design targets)."""
|
|
115
|
+
macs = n * n
|
|
116
|
+
mzis = n * (n - 1) // 2
|
|
117
|
+
p_static = P_DRIVE_LASER + mzis * HOLD_PER_MZI # flat vs rate
|
|
118
|
+
p_digital = macs * E_MAC_DIGITAL * rate_hz # scales with rate
|
|
119
|
+
breakeven = p_static / (macs * E_MAC_DIGITAL) # matvec/s where they cross
|
|
120
|
+
return {
|
|
121
|
+
"n": n, "macs": macs, "mzis": mzis, "rate_hz": rate_hz,
|
|
122
|
+
"p_static": p_static, "p_digital": p_digital, "breakeven": breakeven,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ----------------------------------------------------------------------- rendering
|
|
127
|
+
def _bar(frac: float, width: int = 28) -> str:
|
|
128
|
+
frac = max(0.0, min(1.0, frac))
|
|
129
|
+
fill = int(round(frac * width))
|
|
130
|
+
return "[#6aa9ff]" + "█" * fill + "[/]" + "[#394150]" + "─" * (width - fill) + "[/]"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def render(console, s: dict) -> None:
|
|
134
|
+
from rich.panel import Panel
|
|
135
|
+
from rich.table import Table
|
|
136
|
+
from rich import box
|
|
137
|
+
|
|
138
|
+
# --- power ledger ---
|
|
139
|
+
if s["watts"] is not None:
|
|
140
|
+
total = f"[bold #6aa9ff]{s['watts']:.1f}[/] W [dim](measured, RAPL)[/]"
|
|
141
|
+
floor = "[dim]not exposed by the OS[/]"
|
|
142
|
+
else:
|
|
143
|
+
total = "[bold]—[/] W [#d98a5b]not directly measurable in software here[/]"
|
|
144
|
+
floor = "[dim]platform idle draw, not exposed by the OS[/]"
|
|
145
|
+
busy = s["cpu_busy"] / 100.0
|
|
146
|
+
pwr = Table.grid(padding=(0, 1))
|
|
147
|
+
pwr.add_column(justify="left")
|
|
148
|
+
pwr.add_row("[dim]AXIOM LEDGER[/] [dim]P = floor + compute[/]")
|
|
149
|
+
pwr.add_row("[bold]Power ledger[/]")
|
|
150
|
+
pwr.add_row("")
|
|
151
|
+
pwr.add_row(f"TOTAL {total}")
|
|
152
|
+
pwr.add_row(f"COMPUTE {_bar(busy)} [bold]{s['cpu_busy']:.0f}%[/] [dim]CPU load[/]")
|
|
153
|
+
pwr.add_row(f"FLOOR {floor}")
|
|
154
|
+
console.print(Panel(pwr, box=box.ROUNDED, border_style="#394150", padding=(1, 2)))
|
|
155
|
+
|
|
156
|
+
# --- system row ---
|
|
157
|
+
sysg = Table.grid(expand=True)
|
|
158
|
+
sysg.add_column(ratio=1)
|
|
159
|
+
sysg.add_column(ratio=1)
|
|
160
|
+
b = s["battery"]
|
|
161
|
+
if b is not None:
|
|
162
|
+
plug = "AC" if b.power_plugged else "battery"
|
|
163
|
+
batt_s = f"[bold]{b.percent:.0f}%[/] [dim]{plug}[/]"
|
|
164
|
+
else:
|
|
165
|
+
batt_s = "[dim]no battery (desktop or server)[/]"
|
|
166
|
+
mem_s = f"[bold]{s['mem_used_gb']:.1f} / {s['mem_total_gb']:.1f} GB[/]"
|
|
167
|
+
load_s = f"swap {s['swap_gb']:.2f} GB" + (f" · load {s['load']:.1f}" if s["load"] is not None else "")
|
|
168
|
+
left = Table.grid(); left.add_column()
|
|
169
|
+
left.add_row("[dim]BATTERY[/]"); left.add_row(batt_s)
|
|
170
|
+
right = Table.grid(); right.add_column()
|
|
171
|
+
right.add_row("[dim]MEMORY[/]"); right.add_row(mem_s); right.add_row(f"[dim]{load_s}[/]")
|
|
172
|
+
sysg.add_row(
|
|
173
|
+
Panel(left, box=box.ROUNDED, border_style="#394150", padding=(0, 2)),
|
|
174
|
+
Panel(right, box=box.ROUNDED, border_style="#394150", padding=(0, 2)),
|
|
175
|
+
)
|
|
176
|
+
console.print(sysg)
|
|
177
|
+
|
|
178
|
+
# --- top consumers ---
|
|
179
|
+
tc = Table(box=box.ROUNDED, border_style="#394150", expand=True,
|
|
180
|
+
title="[dim]TOP CONSUMERS[/]", title_justify="left", show_header=False)
|
|
181
|
+
tc.add_column(justify="right", width=7)
|
|
182
|
+
tc.add_column()
|
|
183
|
+
for pct, name in s["consumers"][:8]:
|
|
184
|
+
colour = "#d9a441" if pct >= 20 else "white"
|
|
185
|
+
tc.add_row(f"[{colour}]{pct:.1f}%[/]", name)
|
|
186
|
+
if not s["consumers"][:8]:
|
|
187
|
+
tc.add_row("", "[dim]no active processes sampled[/]")
|
|
188
|
+
console.print(tc)
|
|
189
|
+
|
|
190
|
+
console.print(
|
|
191
|
+
f"[dim]read only, nothing on your system was changed. "
|
|
192
|
+
f"compare against a photonic node:[/] [bold]lumis-ledger estimate --size 64[/] "
|
|
193
|
+
f"[dim]· {SDK_URL}[/]"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def render_estimate(console, e: dict) -> None:
|
|
198
|
+
from rich.panel import Panel
|
|
199
|
+
from rich.table import Table
|
|
200
|
+
from rich import box
|
|
201
|
+
verdict = "photonic wins" if e["rate_hz"] > e["breakeven"] else "digital still cheaper"
|
|
202
|
+
g = Table.grid(padding=(0, 1))
|
|
203
|
+
g.add_column()
|
|
204
|
+
g.add_row("[dim]AXIOM LEDGER · LUMIS Rev 1 design targets[/]")
|
|
205
|
+
g.add_row(f"[bold]A {e['n']}x{e['n']} linear layer on a photonic node[/]")
|
|
206
|
+
g.add_row("")
|
|
207
|
+
g.add_row(f"MZIs in the mesh [bold]{e['mzis']}[/] [dim](N(N-1)/2)[/]")
|
|
208
|
+
g.add_row(f"DIGITAL @ {e['rate_hz']:.0e}/s [bold]{e['p_digital']:.3f} W[/] [dim]{e['macs']} MACs x 4.6 pJ x rate, scales with rate[/]")
|
|
209
|
+
g.add_row(f"PHOTONIC floor [bold]{e['p_static']:.3f} W[/] [dim]6.8 W drivers+laser + EO hold, flat vs rate[/]")
|
|
210
|
+
g.add_row(f"BREAKEVEN [bold]{e['breakeven']:.2e} matvec/s[/] [dim]at {e['rate_hz']:.0e}/s: {verdict}[/]")
|
|
211
|
+
g.add_row("")
|
|
212
|
+
g.add_row("[dim]The arithmetic is free on a photonic mesh; light does the multiply.[/]")
|
|
213
|
+
g.add_row("[dim]The node is not: it draws a fixed floor whatever the rate. All figures[/]")
|
|
214
|
+
g.add_row(f"[dim]are Rev 1 design targets for an unfabricated chip. Reproduce them: {SDK_URL}[/]")
|
|
215
|
+
console.print(Panel(g, box=box.ROUNDED, border_style="#394150", padding=(1, 2)))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------------------- main
|
|
219
|
+
def main(argv=None) -> int:
|
|
220
|
+
from rich.console import Console
|
|
221
|
+
ap = argparse.ArgumentParser(
|
|
222
|
+
prog="lumis-ledger",
|
|
223
|
+
description="An honest, read-only power ledger for your machine, from Axiom Photonics.")
|
|
224
|
+
sub = ap.add_subparsers(dest="cmd")
|
|
225
|
+
ap.add_argument("--watch", action="store_true", help="refresh continuously until Ctrl-C")
|
|
226
|
+
ap.add_argument("--interval", type=float, default=2.0, help="watch refresh seconds (default 2)")
|
|
227
|
+
ap.add_argument("--version", action="version", version=f"lumis-ledger {__version__}")
|
|
228
|
+
|
|
229
|
+
est = sub.add_parser("estimate", help="what a linear layer costs on a LUMIS photonic node")
|
|
230
|
+
est.add_argument("--size", type=int, default=64, help="mesh size N (2..?), default 64")
|
|
231
|
+
est.add_argument("--rate", type=float, default=1e9, help="matrix-vector ops per second, default 1e9")
|
|
232
|
+
|
|
233
|
+
a = ap.parse_args(argv)
|
|
234
|
+
console = Console()
|
|
235
|
+
|
|
236
|
+
if a.cmd == "estimate":
|
|
237
|
+
if a.size < 2:
|
|
238
|
+
ap.error("--size must be at least 2")
|
|
239
|
+
render_estimate(console, lumis_estimate(a.size, a.rate))
|
|
240
|
+
return 0
|
|
241
|
+
|
|
242
|
+
if a.watch:
|
|
243
|
+
try:
|
|
244
|
+
while True:
|
|
245
|
+
console.clear()
|
|
246
|
+
render(console, sample())
|
|
247
|
+
time.sleep(max(0.5, a.interval))
|
|
248
|
+
except KeyboardInterrupt:
|
|
249
|
+
return 0
|
|
250
|
+
else:
|
|
251
|
+
render(console, sample())
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
if __name__ == "__main__":
|
|
256
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lumis-ledger"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "An honest, read-only power ledger for your machine, from Axiom Photonics."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Axiom Photonics Ltd" }]
|
|
13
|
+
keywords = ["power", "energy", "monitor", "photonics", "lumis", "cli"]
|
|
14
|
+
dependencies = ["psutil>=5.9", "rich>=13.0"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/Pr3ndy-ctrl/lumis-ledger"
|
|
18
|
+
SDK = "https://pypi.org/project/axiom-lumis/"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
lumis-ledger = "lumis_ledger:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
py-modules = ["lumis_ledger"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Tests for lumis-ledger. Run: python -m pytest -q"""
|
|
2
|
+
import math
|
|
3
|
+
import sys, os
|
|
4
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
5
|
+
|
|
6
|
+
import lumis_ledger as ll
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_estimate_matches_sdk_figures_at_64():
|
|
10
|
+
e = ll.lumis_estimate(64, 1e9)
|
|
11
|
+
assert e["mzis"] == 2016
|
|
12
|
+
assert e["macs"] == 4096
|
|
13
|
+
assert math.isclose(e["p_static"], 7.000, abs_tol=1e-3) # 6.8 + 2016*(0.2/2016)
|
|
14
|
+
assert math.isclose(e["p_digital"], 18.8416, abs_tol=1e-3) # 4096*4.6e-12*1e9
|
|
15
|
+
assert math.isclose(e["breakeven"], 3.7149e8, rel_tol=1e-3)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_estimate_small_mesh_loses():
|
|
19
|
+
e = ll.lumis_estimate(8, 1e9)
|
|
20
|
+
assert math.isclose(e["p_static"], 6.8028, abs_tol=1e-3)
|
|
21
|
+
assert e["breakeven"] > e["rate_hz"] # a tiny mesh does not win at 1e9
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_estimate_constants_match_lumis_nano():
|
|
25
|
+
# the whole point: these must equal the SDK's honest ledger constants
|
|
26
|
+
assert ll.P_DRIVE_LASER == 6.8
|
|
27
|
+
assert math.isclose(ll.HOLD_PER_MZI, 0.2 / 2016)
|
|
28
|
+
assert ll.E_MAC_DIGITAL == 4.6e-12
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_sample_returns_expected_keys():
|
|
32
|
+
s = ll.sample(interval=0.1)
|
|
33
|
+
for k in ("cpu_busy", "consumers", "mem_used_gb", "mem_total_gb", "watts"):
|
|
34
|
+
assert k in s
|
|
35
|
+
assert 0.0 <= s["cpu_busy"] <= 100.0 * (s["ncpu"] + 1)
|
|
36
|
+
assert s["mem_total_gb"] > 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_power_reading_is_float_or_none():
|
|
40
|
+
w = ll.read_rapl_power(interval=0.05)
|
|
41
|
+
assert w is None or (isinstance(w, float) and w >= 0)
|