dde-cargo 0.1.0__py3-none-any.whl
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.
- dde_cargo/__init__.py +1 -0
- dde_cargo/ddfacet-memory-plan.yml +335 -0
- dde_cargo/ddfacet.yml +881 -0
- dde_cargo/ddfacet_stimela_inputs_schema.yml +1119 -0
- dde_cargo/killms-memory-plan.yml +331 -0
- dde_cargo/killms_stimela_schema.yml +292 -0
- dde_cargo-0.1.0.dist-info/METADATA +126 -0
- dde_cargo-0.1.0.dist-info/RECORD +9 -0
- dde_cargo-0.1.0.dist-info/WHEEL +4 -0
dde_cargo/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Copyright (c) 2026 Tim Molteno (tim@elec.ac.nz)
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# Copied verbatim from the installed package in ddfacet:v0.10.0.6
|
|
2
|
+
# (/opt/venv/lib/python3.13/site-packages/DDFacet/stimela/ddfacet-memory-plan.yml):
|
|
3
|
+
# v0.10.0.6 ships both the cab and the module, so nothing is vendored any more.
|
|
4
|
+
# The image pin follows the ddfacet pin (v0.12.1 since 2026-09-25): the planner is
|
|
5
|
+
# exercised on the same line the imaging runs, so its memory model matches.
|
|
6
|
+
# stimela cab: ddfacet-memory-plan
|
|
7
|
+
#
|
|
8
|
+
# Suggests --Parallel-NCPU, --Data-ChunkHours and (only when the band has to
|
|
9
|
+
# give) --Selection-ChanStep for a DDFacet run, from the parset that run will
|
|
10
|
+
# use and a memory budget. This is the command-line form of
|
|
11
|
+
# DDFacet/ToolsDir/ModEstimateMemory.py, and it ships with the package so a
|
|
12
|
+
# recipe can list it directly:
|
|
13
|
+
#
|
|
14
|
+
# - /opt/venv/lib/python3.13/site-packages/DDFacet/stimela/ddfacet-memory-plan.yml
|
|
15
|
+
#
|
|
16
|
+
# or copy the cab into the recipe's own cabs/ directory (that is what
|
|
17
|
+
# meerkat_imaging does with the other DDFacet cabs).
|
|
18
|
+
#
|
|
19
|
+
# Wiring it into a recipe, ahead of the imaging step:
|
|
20
|
+
#
|
|
21
|
+
# memory-plan:
|
|
22
|
+
# cab: ddfacet-memory-plan
|
|
23
|
+
# params:
|
|
24
|
+
# Parset: =steps.<something>.parset_out
|
|
25
|
+
# MS: =recipe.target-ms
|
|
26
|
+
# RamGB: 62
|
|
27
|
+
# ddfacet-target:
|
|
28
|
+
# params:
|
|
29
|
+
# Parallel.NCPU: =steps.memory-plan.NCPU
|
|
30
|
+
# Data.ChunkHours: =steps.memory-plan.ChunkHours
|
|
31
|
+
# Selection.ChanStep: =steps.memory-plan.ChanStep
|
|
32
|
+
#
|
|
33
|
+
# The estimate is +-40 % (see MANAGING_LIMITED_MEMORY.md): use it to choose
|
|
34
|
+
# between "NCPU 3" and "NCPU 8", not to plan within a gigabyte of the limit.
|
|
35
|
+
#
|
|
36
|
+
# THREADED (the rayon line, DDFacet >= v0.12): NCPU IS NOT A MEMORY KNOB ANY MORE.
|
|
37
|
+
# ModEstimateMemory's model is `main + NCPU x per-worker + chunk buffers`,
|
|
38
|
+
# calibrated on the process pool, where every worker held its own copies. On the
|
|
39
|
+
# rayon line the workers are threads in one process, and a sweep of the same
|
|
40
|
+
# dirty image on moist (8 cores, uv1500 fast profile, target 1, 2026-09-27)
|
|
41
|
+
# measured:
|
|
42
|
+
#
|
|
43
|
+
# NCPU ChunkHours wall CPU largest process
|
|
44
|
+
# 2 2.0 3:35 115% 13.8 GB
|
|
45
|
+
# 4 2.0 1:49 205% 13.0 GB
|
|
46
|
+
# 8 2.0 1:11 422% 13.3 GB
|
|
47
|
+
# 2 0.5 3:37 115% 13.7 GB
|
|
48
|
+
# 8 0.5 1:11 423% 13.3 GB
|
|
49
|
+
#
|
|
50
|
+
# i.e. flat in NCPU, 3x faster with every core -- while the model predicts
|
|
51
|
+
# 14.1 GB at NCPU 1 (right) and 2.18 GB per extra worker (wrong: 28.6 GB at 8),
|
|
52
|
+
# so with an 18 GB budget the unmodified planner answered `NCPU 1`. With
|
|
53
|
+
# `Threaded: true` (the default) the memory is therefore planned at ONE worker --
|
|
54
|
+
# ChunkHours and, if the band must give, ChanStep -- and NCPU is every usable
|
|
55
|
+
# core (or WantNCPU). `Threaded: false` restores the process-pool behaviour.
|
|
56
|
+
#
|
|
57
|
+
# RamGB and WantNCPU default to what the host actually offers when the step runs
|
|
58
|
+
# (MemAvailable, capped by a cgroup memory limit if there is one; the CPU
|
|
59
|
+
# affinity set), so the same recipe plans correctly on a 62 GB/12-core run box
|
|
60
|
+
# and on a 30 GB/8-core desktop. MemWatermarkGB is an output (15 % of the
|
|
61
|
+
# budget): wire it to --Parallel-MemWatermarkGB instead of a fixed value, which
|
|
62
|
+
# deadlocks a machine with less than that free (the pool workers pause for
|
|
63
|
+
# memory the parent holds while it waits for them).
|
|
64
|
+
#
|
|
65
|
+
# Bootstrapping, since a step cannot be planned from the parset it is about to
|
|
66
|
+
# write: plan each step from the parset of the *previous* run of that step (the
|
|
67
|
+
# pipeline's pattern), and for a fresh observation -- where no earlier parset
|
|
68
|
+
# exists -- pass the geometry instead of a parset (NPix, and optionally
|
|
69
|
+
# NFacets/NBand/OverS/Support/Nw/ChunkHoursIn). PlanFile is written by the
|
|
70
|
+
# command, so it is declared `must_exist: false`: a File input that stimela
|
|
71
|
+
# checks for existence would fail the step before the planner ran (DDFacet#17).
|
|
72
|
+
|
|
73
|
+
vars:
|
|
74
|
+
ddfacet-memory-plan:
|
|
75
|
+
image:
|
|
76
|
+
registry: ghcr.io/tmolteno
|
|
77
|
+
name: ddfacet
|
|
78
|
+
# Pin this to whatever the recipe uses; a recipe with its own image var
|
|
79
|
+
# should override `image:` on the cab instead.
|
|
80
|
+
version: v0.16.1
|
|
81
|
+
|
|
82
|
+
cabs:
|
|
83
|
+
ddfacet-memory-plan:
|
|
84
|
+
info: |
|
|
85
|
+
Suggest --Parallel-NCPU, --Data-ChunkHours and (only if the band has to
|
|
86
|
+
give) --Selection-ChanStep for a DDFacet run, from its parset and a
|
|
87
|
+
memory budget. Reads the MS for its row/channel/antenna counts when
|
|
88
|
+
one is given; pass NRows/NChan/NA instead when it cannot be read.
|
|
89
|
+
Needs an image carrying DDFacet/ToolsDir/ModEstimateMemory.py.
|
|
90
|
+
image:
|
|
91
|
+
_use: vars.ddfacet-memory-plan.image
|
|
92
|
+
inputs:
|
|
93
|
+
Parset:
|
|
94
|
+
dtype: File
|
|
95
|
+
required: false
|
|
96
|
+
info: |
|
|
97
|
+
the parset whose geometry is being planned (e.g. the parset_out
|
|
98
|
+
of a ddfacet step). Optional: with no parset, the geometry is
|
|
99
|
+
taken from the inputs below, which is the bootstrap case for a
|
|
100
|
+
fresh observation that has no earlier parset to plan from.
|
|
101
|
+
MS:
|
|
102
|
+
dtype: MS
|
|
103
|
+
required: false
|
|
104
|
+
info: measurement set to size the estimate with
|
|
105
|
+
RamGB:
|
|
106
|
+
dtype: float
|
|
107
|
+
required: false
|
|
108
|
+
info: >
|
|
109
|
+
memory the run may use. Default: what the host has available
|
|
110
|
+
when the step runs -- MemAvailable, capped by the cgroup memory
|
|
111
|
+
limit if there is one (a container or systemd scope)
|
|
112
|
+
NPix:
|
|
113
|
+
dtype: int
|
|
114
|
+
required: false
|
|
115
|
+
info: image size, when there is no Parset to read it from
|
|
116
|
+
NFacets:
|
|
117
|
+
dtype: int
|
|
118
|
+
required: false
|
|
119
|
+
NBand:
|
|
120
|
+
dtype: int
|
|
121
|
+
required: false
|
|
122
|
+
OverS:
|
|
123
|
+
dtype: int
|
|
124
|
+
required: false
|
|
125
|
+
Support:
|
|
126
|
+
dtype: int
|
|
127
|
+
required: false
|
|
128
|
+
Nw:
|
|
129
|
+
dtype: int
|
|
130
|
+
required: false
|
|
131
|
+
ChunkHoursIn:
|
|
132
|
+
dtype: float
|
|
133
|
+
required: false
|
|
134
|
+
info: the chunk size you intend, when planning without a Parset
|
|
135
|
+
default: 0.5
|
|
136
|
+
WantNCPU:
|
|
137
|
+
dtype: int
|
|
138
|
+
required: false
|
|
139
|
+
info: >
|
|
140
|
+
worker count you would like; also the cap. Default: the usable
|
|
141
|
+
cores (the CPU affinity set, so a taskset/cpuset is respected)
|
|
142
|
+
Threaded:
|
|
143
|
+
dtype: bool
|
|
144
|
+
default: true
|
|
145
|
+
info: >
|
|
146
|
+
the rayon line (DDFacet >= v0.12), where workers are threads in one
|
|
147
|
+
process and NCPU costs no measurable memory: plan the memory at one
|
|
148
|
+
worker and return every usable core as NCPU. false = the process
|
|
149
|
+
pool, where each worker costs memory (the model's original form)
|
|
150
|
+
Prefer:
|
|
151
|
+
dtype: str
|
|
152
|
+
default: fidelity
|
|
153
|
+
info: "'fidelity' keeps the full band while any worker count fits; 'speed' takes the workers first and subsamples the band if it must"
|
|
154
|
+
NDirections:
|
|
155
|
+
dtype: int
|
|
156
|
+
default: 1
|
|
157
|
+
NCorr:
|
|
158
|
+
dtype: int
|
|
159
|
+
default: 2
|
|
160
|
+
NRows:
|
|
161
|
+
dtype: int
|
|
162
|
+
required: false
|
|
163
|
+
NChan:
|
|
164
|
+
dtype: int
|
|
165
|
+
required: false
|
|
166
|
+
NA:
|
|
167
|
+
dtype: int
|
|
168
|
+
required: false
|
|
169
|
+
PlanOut:
|
|
170
|
+
dtype: File
|
|
171
|
+
default: ddfacet-memory-plan.json
|
|
172
|
+
# the command *writes* this file, so stimela must not require it
|
|
173
|
+
# to exist before the step starts (DDFacet#17)
|
|
174
|
+
must_exist: false
|
|
175
|
+
info: where to write the plan, as JSON
|
|
176
|
+
outputs:
|
|
177
|
+
NCPU:
|
|
178
|
+
dtype: int
|
|
179
|
+
info: pass to --Parallel-NCPU
|
|
180
|
+
ChunkHours:
|
|
181
|
+
dtype: float
|
|
182
|
+
info: pass to --Data-ChunkHours
|
|
183
|
+
ChanStep:
|
|
184
|
+
dtype: int
|
|
185
|
+
info: pass to --Selection-ChanStep
|
|
186
|
+
MemWatermarkGB:
|
|
187
|
+
dtype: float
|
|
188
|
+
info: >
|
|
189
|
+
pass to --Parallel-MemWatermarkGB: 15 % of the budget, so the
|
|
190
|
+
throttle scales with the machine instead of a fixed floor that
|
|
191
|
+
can exceed what is free (and then deadlocks)
|
|
192
|
+
RamGBUsed:
|
|
193
|
+
dtype: float
|
|
194
|
+
info: the budget the plan was made for (RamGB, or what the host offered)
|
|
195
|
+
PeakGB:
|
|
196
|
+
dtype: float
|
|
197
|
+
info: predicted peak as per-process accounting sees it (RSS sums / stimela's Mem GB)
|
|
198
|
+
HostRAMGB:
|
|
199
|
+
dtype: float
|
|
200
|
+
info: predicted peak as `free -g` sees it -- the shared arrays count as RAM only when /dev/shm is a tmpfs
|
|
201
|
+
Flags:
|
|
202
|
+
dtype: str
|
|
203
|
+
info: the three flags, ready to paste
|
|
204
|
+
Plan:
|
|
205
|
+
dtype: File
|
|
206
|
+
must_exist: false
|
|
207
|
+
info: the JSON plan (chosen settings, every combination that fits, notes)
|
|
208
|
+
flavour:
|
|
209
|
+
kind: python-code
|
|
210
|
+
command: |
|
|
211
|
+
import json
|
|
212
|
+
import math
|
|
213
|
+
from DDFacet.ToolsDir.ModEstimateMemory import suggestParameters
|
|
214
|
+
|
|
215
|
+
# Optional inputs may arrive unset, in which case stimela does not
|
|
216
|
+
# necessarily define the name at all -- look them up defensively.
|
|
217
|
+
_g = globals()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _opt(name, default=None):
|
|
221
|
+
value = _g.get(name, default)
|
|
222
|
+
return default if value is None else value
|
|
223
|
+
|
|
224
|
+
# No Parset? Build the geometry from the inputs instead, so a fresh
|
|
225
|
+
# observation can be planned before any parset exists (DDFacet#17).
|
|
226
|
+
_geometry = {}
|
|
227
|
+
for _key in ("NPix", "NFacets", "NBand", "OverS", "Support", "Nw"):
|
|
228
|
+
_value = _opt(_key)
|
|
229
|
+
if _value is not None:
|
|
230
|
+
_geometry[_key] = _value
|
|
231
|
+
if _opt("Parset") is None and not _geometry:
|
|
232
|
+
raise RuntimeError(
|
|
233
|
+
"ddfacet-memory-plan: give either Parset or the geometry "
|
|
234
|
+
"(NPix, and optionally NFacets/NBand/OverS/Support/Nw)")
|
|
235
|
+
_pardict = None
|
|
236
|
+
if _opt("Parset") is None:
|
|
237
|
+
_pardict = {
|
|
238
|
+
"Image": {"NPix": _geometry.get("NPix", 5000)},
|
|
239
|
+
"Facets": {"NFacets": _geometry.get("NFacets", 3), "Padding": 1.7},
|
|
240
|
+
"Freq": {"NBand": _geometry.get("NBand", 1)},
|
|
241
|
+
"Output": {"NGridPol": 1},
|
|
242
|
+
"CF": {"OverS": _geometry.get("OverS", 11),
|
|
243
|
+
"Support": _geometry.get("Support", 7),
|
|
244
|
+
"Nw": _geometry.get("Nw", 100)},
|
|
245
|
+
"Data": {"ChunkHours": _opt("ChunkHoursIn", 0.5)},
|
|
246
|
+
"Parallel": {"NCPU": 1},
|
|
247
|
+
}
|
|
248
|
+
print("no Parset given: planning from the inputs (NPix %d, %d "
|
|
249
|
+
"facet(s), %d band(s))" % (_pardict["Image"]["NPix"],
|
|
250
|
+
_pardict["Facets"]["NFacets"],
|
|
251
|
+
_pardict["Freq"]["NBand"]))
|
|
252
|
+
|
|
253
|
+
import os
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _usable_cores():
|
|
257
|
+
try:
|
|
258
|
+
return len(os.sched_getaffinity(0))
|
|
259
|
+
except AttributeError:
|
|
260
|
+
return os.cpu_count() or 1
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _available_gb():
|
|
264
|
+
"""MemAvailable, capped by the cgroup (v2) limit: what this run can use."""
|
|
265
|
+
avail = None
|
|
266
|
+
with open("/proc/meminfo") as _fh:
|
|
267
|
+
for _line in _fh:
|
|
268
|
+
if _line.startswith("MemAvailable:"):
|
|
269
|
+
avail = int(_line.split()[1]) * 1024
|
|
270
|
+
try:
|
|
271
|
+
_cg = open("/proc/self/cgroup").read().strip().split("::", 1)[-1]
|
|
272
|
+
_dir = "/sys/fs/cgroup" + _cg
|
|
273
|
+
_max = open(_dir + "/memory.max").read().strip()
|
|
274
|
+
if _max != "max":
|
|
275
|
+
_free = int(_max) - int(open(_dir + "/memory.current").read())
|
|
276
|
+
avail = _free if avail is None else min(avail, _free)
|
|
277
|
+
except (OSError, ValueError):
|
|
278
|
+
pass
|
|
279
|
+
return avail / float(1 << 30)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
ram_gb = _opt("RamGB")
|
|
283
|
+
ram_source = "RamGB"
|
|
284
|
+
if ram_gb is None:
|
|
285
|
+
ram_gb = _available_gb()
|
|
286
|
+
ram_source = "available on this host"
|
|
287
|
+
cores = int(_opt("WantNCPU") or _usable_cores())
|
|
288
|
+
threaded = bool(_opt("Threaded", True))
|
|
289
|
+
print("budget %.1f GB (%s); %d core(s)%s; %s model"
|
|
290
|
+
% (ram_gb, ram_source, cores,
|
|
291
|
+
"" if _opt("WantNCPU") else " usable",
|
|
292
|
+
"threaded (memory planned at one worker)" if threaded
|
|
293
|
+
else "process-pool"))
|
|
294
|
+
|
|
295
|
+
# Threaded: the memory does not move with NCPU (measured flat, header),
|
|
296
|
+
# so the feasibility search runs at one worker and NCPU is every core.
|
|
297
|
+
suggestion = suggestParameters(
|
|
298
|
+
GD=_pardict, parset=_opt("Parset"), ms=_opt("MS"), ram_gb=ram_gb,
|
|
299
|
+
ncpu=1 if threaded else cores, max_ncpu=1 if threaded else cores,
|
|
300
|
+
prefer=_opt("Prefer", "fidelity"),
|
|
301
|
+
ndirections=_opt("NDirections", 1), ncorr=_opt("NCorr", 2),
|
|
302
|
+
nrows=_opt("NRows"), nchan=_opt("NChan"), na=_opt("NA"))
|
|
303
|
+
if threaded:
|
|
304
|
+
print("the memory model's own summary, at ONE worker (its NCPU line is "
|
|
305
|
+
"not the plan -- see 'plan:' below):")
|
|
306
|
+
print(suggestion)
|
|
307
|
+
|
|
308
|
+
NCPU = cores if threaded else int(suggestion.best["ncpu"])
|
|
309
|
+
ChunkHours = float(suggestion.best["chunk_hours"])
|
|
310
|
+
ChanStep = int(suggestion.best["chan_step"])
|
|
311
|
+
peak = float(suggestion.best["peak_gb"])
|
|
312
|
+
PeakGB = peak if math.isfinite(peak) else 0.0
|
|
313
|
+
host = float(suggestion.best.get("host_ram_gb", peak))
|
|
314
|
+
HostRAMGB = host if math.isfinite(host) else 0.0
|
|
315
|
+
MemWatermarkGB = float(max(1, round(0.15 * ram_gb)))
|
|
316
|
+
RamGBUsed = float(ram_gb)
|
|
317
|
+
Flags = " ".join(
|
|
318
|
+
(["--Selection-ChanStep %d" % ChanStep] if ChanStep != 1 else [])
|
|
319
|
+
+ ["--Parallel-NCPU %d" % NCPU, "--Data-ChunkHours %g" % ChunkHours,
|
|
320
|
+
"--Parallel-MemWatermarkGB %g" % MemWatermarkGB])
|
|
321
|
+
print("plan: %s" % Flags)
|
|
322
|
+
Plan = _opt("PlanOut", "ddfacet-memory-plan.json")
|
|
323
|
+
with open(Plan, "w") as _fh:
|
|
324
|
+
json.dump(dict(ncpu=NCPU, chunk_hours=ChunkHours, chan_step=ChanStep,
|
|
325
|
+
peak_gb=PeakGB, host_ram_gb=HostRAMGB,
|
|
326
|
+
mem_watermark_gb=MemWatermarkGB, ram_gb=RamGBUsed,
|
|
327
|
+
ram_source=ram_source, threaded=threaded,
|
|
328
|
+
flags=Flags, notes=suggestion.notes,
|
|
329
|
+
feasible=[{k: v for k, v in f.items() if k != "estimate"}
|
|
330
|
+
for f in suggestion.feasible]),
|
|
331
|
+
_fh, indent=2)
|
|
332
|
+
if not suggestion.feasible:
|
|
333
|
+
raise RuntimeError(
|
|
334
|
+
"ddfacet-memory-plan: nothing in the search space fits %.1f GB "
|
|
335
|
+
"-- see the plan at %s" % (ram_gb, Plan))
|