Spaces:
Running on Zero
Running on Zero
Read CPU/RAM from cgroup limits instead of host
Browse files
app.py
CHANGED
|
@@ -39,7 +39,79 @@ def _log(message):
|
|
| 39 |
print(message.encode("ascii", "backslashreplace").decode("ascii"), flush=True)
|
| 40 |
|
| 41 |
|
| 42 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
try:
|
| 44 |
with open("/proc/meminfo", "r", encoding="utf-8") as meminfo:
|
| 45 |
for line in meminfo:
|
|
@@ -91,20 +163,20 @@ API_SPECS = {
|
|
| 91 |
|
| 92 |
def _get_runtime_specs():
|
| 93 |
storage = shutil.disk_usage(os.getcwd())
|
| 94 |
-
host_ram = _get_ram_bytes()
|
| 95 |
-
host_cores = os.cpu_count()
|
| 96 |
-
|
| 97 |
-
# ZeroGPU shares a large host machine. /proc/meminfo and
|
| 98 |
-
# os.cpu_count() report the *full host*, not the Space's slice.
|
| 99 |
-
# The actual allocation depends on ZeroGPU scheduling.
|
| 100 |
hardware = os.environ.get("SPACE_HARDWARE", "zero-a10g")
|
| 101 |
is_zerogpu = "zero" in hardware.lower()
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
return {
|
| 104 |
"platform": platform.platform(),
|
| 105 |
"python_version": platform.python_version(),
|
|
|
|
| 106 |
"host_cpu_cores": host_cores,
|
| 107 |
-
"
|
| 108 |
"storage_total_bytes": storage.total,
|
| 109 |
"storage_used_bytes": storage.used,
|
| 110 |
"storage_free_bytes": storage.free,
|
|
@@ -114,11 +186,6 @@ def _get_runtime_specs():
|
|
| 114 |
"vram_bytes": 48 * 1024**3 if is_zerogpu else None,
|
| 115 |
"shared_zero_gpu": is_zerogpu,
|
| 116 |
} if is_zerogpu else None,
|
| 117 |
-
"note": (
|
| 118 |
-
"Host machine resources shown; Space gets a shared slice "
|
| 119 |
-
"via ZeroGPU dynamic allocation."
|
| 120 |
-
if is_zerogpu else None
|
| 121 |
-
),
|
| 122 |
}
|
| 123 |
|
| 124 |
|
|
|
|
| 39 |
print(message.encode("ascii", "backslashreplace").decode("ascii"), flush=True)
|
| 40 |
|
| 41 |
|
| 42 |
+
def _read_cgroup_file(path):
|
| 43 |
+
try:
|
| 44 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 45 |
+
return f.read().strip()
|
| 46 |
+
except (FileNotFoundError, OSError):
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _parse_cpu_count():
|
| 51 |
+
"""Read actual CPU allocation from cgroup limits."""
|
| 52 |
+
# cgroup v2: /sys/fs/cgroup/cpu.max -> "quota period" or "max period"
|
| 53 |
+
cpu_max = _read_cgroup_file("/sys/fs/cgroup/cpu.max")
|
| 54 |
+
if cpu_max:
|
| 55 |
+
parts = cpu_max.split()
|
| 56 |
+
if len(parts) == 2 and parts[0] != "max":
|
| 57 |
+
try:
|
| 58 |
+
quota, period = int(parts[0]), int(parts[1])
|
| 59 |
+
if period > 0:
|
| 60 |
+
return quota // period
|
| 61 |
+
except ValueError:
|
| 62 |
+
pass
|
| 63 |
+
|
| 64 |
+
# cgroup v1: /sys/fs/cgroup/cpu/cpu.cfs_quota_us and cpu.cfs_period_us
|
| 65 |
+
quota = _read_cgroup_file("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")
|
| 66 |
+
period = _read_cgroup_file("/sys/fs/cgroup/cpu/cpu.cfs_period_us")
|
| 67 |
+
if quota and period:
|
| 68 |
+
try:
|
| 69 |
+
q, p = int(quota), int(period)
|
| 70 |
+
if q > 0 and p > 0:
|
| 71 |
+
return q // p
|
| 72 |
+
except ValueError:
|
| 73 |
+
pass
|
| 74 |
+
|
| 75 |
+
# cpuset: /sys/fs/cgroup/cpuset.cpus -> "0-3" or "0,1,2,3"
|
| 76 |
+
cpuset = _read_cgroup_file("/sys/fs/cgroup/cpuset.cpus")
|
| 77 |
+
if cpuset:
|
| 78 |
+
count = 0
|
| 79 |
+
for part in cpuset.split(","):
|
| 80 |
+
if "-" in part:
|
| 81 |
+
start, end = part.split("-", 1)
|
| 82 |
+
count += int(end) - int(start) + 1
|
| 83 |
+
else:
|
| 84 |
+
count += 1
|
| 85 |
+
if count > 0:
|
| 86 |
+
return count
|
| 87 |
+
|
| 88 |
+
return os.cpu_count()
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _parse_ram_bytes():
|
| 92 |
+
"""Read actual memory limit from cgroup limits."""
|
| 93 |
+
# cgroup v2: /sys/fs/cgroup/memory.max
|
| 94 |
+
mem_max = _read_cgroup_file("/sys/fs/cgroup/memory.max")
|
| 95 |
+
if mem_max and mem_max != "max":
|
| 96 |
+
try:
|
| 97 |
+
val = int(mem_max)
|
| 98 |
+
# cgroup v2 reports in bytes
|
| 99 |
+
if val < 2**62: # sanity check: not unlimited
|
| 100 |
+
return val
|
| 101 |
+
except ValueError:
|
| 102 |
+
pass
|
| 103 |
+
|
| 104 |
+
# cgroup v1: /sys/fs/cgroup/memory/memory.limit_in_bytes
|
| 105 |
+
mem_limit = _read_cgroup_file("/sys/fs/cgroup/memory/memory.limit_in_bytes")
|
| 106 |
+
if mem_limit:
|
| 107 |
+
try:
|
| 108 |
+
val = int(mem_limit)
|
| 109 |
+
if val < 2**62: # sanity check: not unlimited (reported as huge number)
|
| 110 |
+
return val
|
| 111 |
+
except ValueError:
|
| 112 |
+
pass
|
| 113 |
+
|
| 114 |
+
# Fallback to /proc/meminfo
|
| 115 |
try:
|
| 116 |
with open("/proc/meminfo", "r", encoding="utf-8") as meminfo:
|
| 117 |
for line in meminfo:
|
|
|
|
| 163 |
|
| 164 |
def _get_runtime_specs():
|
| 165 |
storage = shutil.disk_usage(os.getcwd())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
hardware = os.environ.get("SPACE_HARDWARE", "zero-a10g")
|
| 167 |
is_zerogpu = "zero" in hardware.lower()
|
| 168 |
|
| 169 |
+
# Read actual container limits from cgroup, falling back to host values
|
| 170 |
+
allocated_cores = _parse_cpu_count()
|
| 171 |
+
allocated_ram = _parse_ram_bytes()
|
| 172 |
+
host_cores = os.cpu_count()
|
| 173 |
+
|
| 174 |
return {
|
| 175 |
"platform": platform.platform(),
|
| 176 |
"python_version": platform.python_version(),
|
| 177 |
+
"cpu_cores": allocated_cores,
|
| 178 |
"host_cpu_cores": host_cores,
|
| 179 |
+
"ram_bytes": allocated_ram,
|
| 180 |
"storage_total_bytes": storage.total,
|
| 181 |
"storage_used_bytes": storage.used,
|
| 182 |
"storage_free_bytes": storage.free,
|
|
|
|
| 186 |
"vram_bytes": 48 * 1024**3 if is_zerogpu else None,
|
| 187 |
"shared_zero_gpu": is_zerogpu,
|
| 188 |
} if is_zerogpu else None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
}
|
| 190 |
|
| 191 |
|