Skip to content

Commit e41e706

Browse files
committed
feat: Computing Elements use MaxRAM
1 parent dde87cb commit e41e706

11 files changed

Lines changed: 137 additions & 31 deletions

File tree

docs/source/UserGuide/Tutorials/JobManagementAdvanced/index.rst

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,16 +335,15 @@ Jobs that can (or should) run using more than 1 processor should be described as
335335
using the "setNumberOfProcessors" method of the API::
336336

337337
j = Job()
338-
j.setCPUTime(500)
339-
j.setExecutable('echo',arguments='hello')
340-
j.setExecutable('ls',arguments='-l')
341-
j.setExecutable('echo', arguments='hello again')
342-
j.setName('MP test')
338+
...
343339
j.setNumberOfProcessors(16)
344340

345341
Calling ``Job().setNumberOfProcessors()``, with a value bigger than 1,
346342
will translate into adding also the "MultiProcessor" tag to the job description.
347343

344+
``Job().setNumberOfProcessors()`` takes at most 3 arguments, in this order:
345+
``numberOfProcessors`` is the exact number of requested processors, ``minNumberOfProcessors`` is the minimum allowed, ``maxNumberOfProcessors`` the maximum.
346+
348347
Users can specify in the job descriptions NumberOfProcessors and WholeNode parameters, e.g.::
349348

350349
NumberOfProcessors = 16;
@@ -356,6 +355,20 @@ This will be translated internally into 16Processors and WholeNode tags.
356355
This would allow resources (WN's) to put flexibly requirements on jobs to be taken, for example, avoiding single-core jobs on a multi-core nodes.
357356

358357

358+
Setting memory limits
359+
@@@@@@@@@@@@@@@@@@@@@
360+
361+
Jobs can (and probably should) set RAM limits.
362+
using the "setRAMRequrements" method of the API::
363+
364+
j = Job()
365+
...
366+
j.setRAMRequrements(2, 4)
367+
368+
Calling ``Job().setRAMRequirements()`` takes 2 values, where the first is the minimum required amount of RAM (in GB) that the job requests.
369+
The second value instead specifies the limit that should not be surpassed.
370+
371+
359372
Submitting jobs with specifc requirements (e.g. GPU)
360373
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
361374

src/DIRAC/Core/Utilities/CGroups2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ def systemCall(self, *args, **kwargs):
300300
if "ceParameters" in kwargs:
301301
if cpuLimit := kwargs["ceParameters"].get("CPULimit", None):
302302
cores = float(cpuLimit)
303+
# MemoryLimitMB should be the job upper limit
303304
if memoryMB := int(kwargs["ceParameters"].get("MemoryLimitMB", 0)):
304305
memory = memoryMB * 1024 * 1024
305306
if kwargs["ceParameters"].get("MemoryNoSwap", "no").lower() in ("yes", "true"):

src/DIRAC/Interfaces/API/Job.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,18 +522,49 @@ def setDestination(self, destination):
522522
return S_OK()
523523

524524
#############################################################################
525-
def setRAMRequirements(self, ramRequired: int = 0):
525+
def setRAMRequirements(self, ramRequired: int = 0, maxRAM: int = 0):
526526
"""Helper function.
527-
Specify the RAM requirements for the job in GB. 0 (default) means no specific requirements.
527+
Specify the RAM requirements for the job. 0 (default) means no specific requirements.
528+
529+
Example usage:
530+
531+
>>> job = Job()
532+
>>> job.setRAMRequirements(ramRequired=2)
533+
means that the job needs at least 2 GBs of RAM to work. This is taken into consideration at job's matching time.
534+
The job definition does not specify an upper limit.
535+
From a user's point of view this is fine (normally, not for admins).
536+
537+
>>> job.setRAMRequirements(ramRequired=2, maxRAM=4)
538+
means that the job needs 2 GBs of RAM to work. 4 GBs will then be the upper limit for CG2 limits.
539+
540+
>>> job.setRAMRequirements(ramRequired=4, maxRAM=4)
541+
means that we should match this job if there is at least 4 available GBs of run. At the same time, CG2 will not allow to use more than that.
542+
543+
>>> job.setRAMRequirements(maxRAM=4)
544+
means that the job does not set a min amount of RAM (so can match--run "everywhere"), but the 4 GBs will then be the upper limit for CG2 limits.
545+
546+
>>> job.setRAMRequirements(ramRequired=8, maxRAM=4)
547+
Makes no sense, an error will be raised
528548
"""
549+
if ramRequired and maxRAM and ramRequired > maxRAM:
550+
return self._reportError("Invalid settings, ramRequired is higher than maxRAM")
551+
529552
if ramRequired:
530553
self._addParameter(
531554
self.workflow,
532-
"MaxRAM",
555+
"MinRAM",
533556
"JDL",
534557
ramRequired,
535558
"GBs of RAM requested",
536559
)
560+
if maxRAM:
561+
self._addParameter(
562+
self.workflow,
563+
"MaxRAM",
564+
"JDL",
565+
maxRAM,
566+
"Max GBs of RAM to be used",
567+
)
537568

538569
def setNumberOfProcessors(self, numberOfProcessors=None, minNumberOfProcessors=None, maxNumberOfProcessors=None):
539570
"""Helper function.

src/DIRAC/Resources/Computing/ComputingElement.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,14 +236,14 @@ def setParameters(self, ceOptions):
236236
generalCEDict.update(self.ceParameters)
237237
self.ceParameters = generalCEDict
238238

239-
# If NumberOfProcessors/GPUs/RAM is present in the description but is equal to zero
239+
# If NumberOfProcessors/GPUs/MaxRAM is present in the description but is equal to zero
240240
# interpret it as needing local evaluation
241241
if self.ceParameters.get("NumberOfProcessors", -1) == 0:
242242
self.ceParameters["NumberOfProcessors"] = getNumberOfProcessors()
243243
if self.ceParameters.get("NumberOfGPUs", -1) == 0:
244244
self.ceParameters["NumberOfGPUs"] = getNumberOfGPUs()
245-
if self.ceParameters.get("RAM", -1) == 0:
246-
self.ceParameters["RAM"] = getAvailableRAM()
245+
if self.ceParameters.get("MaxRAM", 0) == 0:
246+
self.ceParameters["MaxRAM"] = getAvailableRAM()
247247

248248
for key in ceOptions:
249249
if key in INTEGER_PARAMETERS:

src/DIRAC/Resources/Computing/InProcessComputingElement.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def __init__(self, ceUniqueID):
2222
self.runningJobs = 0
2323

2424
self.processors = int(self.ceParameters.get("NumberOfProcessors", 1))
25+
self.maxRAM = int(self.ceParameters.get("MaxRAM", 0))
2526
self.ceParameters["MaxTotalJobs"] = 1
2627

2728
def submitJob(self, executableFile, proxy=None, inputs=None, **kwargs):
@@ -118,4 +119,6 @@ def getCEStatus(self):
118119
result["WaitingJobs"] = 0
119120
# processors
120121
result["AvailableProcessors"] = self.processors
122+
# RAM
123+
result["MaxRAM"] = self.maxRAM
121124
return result

src/DIRAC/Resources/Computing/PoolComputingElement.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
**Code Documentation**
2121
"""
2222
import concurrent.futures
23+
import copy
2324
import functools
2425

2526
from DIRAC import S_ERROR, S_OK
@@ -29,8 +30,8 @@
2930
from DIRAC.Resources.Computing.SingularityComputingElement import SingularityComputingElement
3031

3132

32-
def executeJob(executableFile, proxy, taskID, inputs, **kwargs):
33-
"""wrapper around ce.submitJob: decides which CE to use (InProcess or Singularity)
33+
def executeJob(executableFile, proxy, taskID, inputs, innerCEParameters, **kwargs):
34+
"""wrapper around ce.submitJob: decides which inner CE to use (InProcess or Singularity)
3435
3536
:param str executableFile: location of the executable file
3637
:param str proxy: proxy file location to be used for job submission
@@ -46,6 +47,8 @@ def executeJob(executableFile, proxy, taskID, inputs, **kwargs):
4647
else:
4748
ce = InProcessComputingElement("Task-" + str(taskID))
4849

50+
ce.ceParameters["NumberOfProcessors"] = innerCEParameters["NumberOfProcessors"]
51+
ce.ceParameters["MaxRAM"] = innerCEParameters["MaxRAM"]
4952
return ce.submitJob(executableFile, proxy, inputs=inputs, **kwargs)
5053

5154

@@ -60,8 +63,9 @@ def __init__(self, ceUniqueID):
6063
self.pPool = None
6164
self.taskID = 0
6265
self.processorsPerTask = {}
63-
self.userNumberPerTask = {}
64-
self.ram = 1024 # Default RAM in GB (this is an arbitrary large value in case of no limit)
66+
self.ram = (
67+
1024 # Available RAM for the node, in GB. The default value is an arbitrary large value in case of no limit
68+
)
6569
self.ramPerTask = {}
6670

6771
# This CE will effectively submit to another "Inner"CE
@@ -76,10 +80,8 @@ def _reset(self):
7680

7781
self.processors = int(self.ceParameters.get("NumberOfProcessors", self.processors))
7882
self.ceParameters["MaxTotalJobs"] = self.processors
79-
max_ram = int(self.ceParameters.get("MaxRAM", 0))
80-
if max_ram > 0:
81-
self.ram = max_ram // 1024 # Convert from MB to GB
82-
self.ceParameters["MaxRAM"] = self.ram
83+
if int(self.ceParameters.get("MaxRAM", 0)):
84+
self.ram = self.ceParameters["MaxRAM"]
8385
# Indicates that the submission is done asynchronously
8486
# The result is not immediately available
8587
self.ceParameters["AsyncSubmission"] = True
@@ -128,12 +130,20 @@ def submitJob(self, executableFile, proxy=None, inputs=None, **kwargs):
128130
if not res["OK"]:
129131
self.log.error("Could not dump cfg to pilot.cfg", res["Message"])
130132

133+
# Define the innerCEParameters
134+
innerCEParameters = {}
135+
innerCEParameters = copy.deepcopy(self.ceParameters)
136+
innerCEParameters["NumberOfProcessors"] = processorsForJob
137+
innerCEParameters["MaxRAM"] = memoryForJob
138+
131139
# Here we define task kwargs: adding complex objects like thread.Lock can trigger errors in the task
132140
taskKwargs = {"InnerCESubmissionType": self.innerCESubmissionType}
133141
taskKwargs["jobDesc"] = kwargs.get("jobDesc", {})
134142

135143
# Submission
136-
future = self.pPool.submit(executeJob, executableFile, proxy, self.taskID, inputs, **taskKwargs)
144+
future = self.pPool.submit(
145+
executeJob, executableFile, proxy, self.taskID, inputs, innerCEParameters, **taskKwargs
146+
)
137147
self.processorsPerTask[future] = processorsForJob
138148
self.ramPerTask[future] = memoryForJob
139149
future.add_done_callback(functools.partial(self.finalizeJob, self.taskID))
@@ -190,14 +200,21 @@ def _getMemoryForJobs(self, kwargs):
190200
"""
191201

192202
# # job requirements
193-
requestedMemory = kwargs.get("MaxRAM", 0)
203+
requestedMemory = kwargs.get("MinRAM", 0)
194204

195205
# # now check what the slot can provide
196206
# Do we have enough memory?
197207
availableMemory = self.ram - sum(self.ramPerTask.values())
198208
if availableMemory < requestedMemory:
199209
return None
200210

211+
# if there's a MaxRAM requested, we allocate it all (if it fits)
212+
if kwargs.get("MaxRAM", 0):
213+
if availableMemory >= kwargs.get("MaxRAM"):
214+
requestedMemory = kwargs.get("MaxRAM")
215+
else:
216+
return None
217+
201218
return requestedMemory
202219

203220
def finalizeJob(self, taskID, future):
@@ -206,23 +223,26 @@ def finalizeJob(self, taskID, future):
206223
:param future: evaluating the future result
207224
"""
208225
nProc = self.processorsPerTask.pop(future)
226+
ram = self.ramPerTask.pop(future, None)
209227

210228
result = future.result() # This would be the result of the e.g. InProcess.submitJob()
211229
if result["OK"]:
212-
self.log.info("Task finished successfully:", f"{taskID}; {nProc} processor(s) freed")
230+
self.log.info("Task finished successfully:", f"{taskID}; {nProc} processor(s) and {ram}GB freed")
213231
else:
214232
self.log.error("Task failed submission:", f"{taskID}; message: {result['Message']}")
215233
self.taskResults[taskID] = result
216234

217235
def getCEStatus(self):
218236
"""Method to return information on running and waiting jobs,
219-
as well as the number of processors (used, and available).
237+
as well as the number of processors (used, and available),
238+
and the RAM (used, and available)
220239
221240
:return: dictionary of numbers of jobs per status and processors (used, and available)
222241
"""
223242

224243
result = S_OK()
225244
nJobs = 0
245+
226246
for _j, value in self.processorsPerTask.items():
227247
if value > 0:
228248
nJobs += 1
@@ -234,6 +254,10 @@ def getCEStatus(self):
234254
processorsInUse = sum(self.processorsPerTask.values())
235255
result["UsedProcessors"] = processorsInUse
236256
result["AvailableProcessors"] = self.processors - processorsInUse
257+
# dealing with RAM
258+
result["RAMInUse"] = sum(self.ramPerTask.values())
259+
result["RAMAvailable"] = self.ram - sum(self.ramPerTask.values())
260+
237261
return result
238262

239263
def getDescription(self):

src/DIRAC/Resources/Computing/SingularityComputingElement.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def __init__(self, ceUniqueID):
115115
self.__installDIRACInContainer = False
116116

117117
self.processors = int(self.ceParameters.get("NumberOfProcessors", 1))
118+
self.maxRAM = int(self.ceParameters.get("MaxRAM", 0))
118119

119120
@staticmethod
120121
def __findInstallBaseDir():
@@ -413,6 +414,12 @@ def submitJob(self, executableFile, proxy=None, **kwargs):
413414

414415
self.log.debug(f"Execute singularity command: {cmd}")
415416
self.log.debug(f"Execute singularity env: {self.__getEnv()}")
417+
# systemCall below uses ceParameters["MemoryLimitMB"] as CG2 upper memory limit
418+
# if there's a max RAM available to the job, use that
419+
if self.maxRAM:
420+
self.ceParameters["MemoryLimitMB"] = min(
421+
self.maxRAM * 1024, self.ceParameters.get("MemoryLimitMB", 1024 * 1024)
422+
) # 1024 * 1024 is an arbitrary large number
416423
result = CG2Manager().systemCall(
417424
0, cmd, callbackFunction=self.sendOutput, env=self.__getEnv(), ceParameters=self.ceParameters
418425
)
@@ -449,4 +456,6 @@ def getCEStatus(self):
449456
result["WaitingJobs"] = 0
450457
# processors
451458
result["AvailableProcessors"] = self.processors
459+
# RAM
460+
result["MaxRAM"] = self.maxRAM
452461
return result

src/DIRAC/Resources/Computing/test/Test_InProcessComputingElement.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def test_submitJob():
5252
maxNumberOfProcessors=8,
5353
wholeNode=False,
5454
mpTag=True,
55+
MinRAM=2,
56+
MaxRAM=4,
5557
jobDesc={"jobParams": jobParams, "resourceParams": resourceParams, "optimizerParams": optimizerParams},
5658
)
5759
assert res["OK"] is True

src/DIRAC/Resources/Computing/test/Test_PoolComputingElement.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,14 +378,18 @@ def test_executeJob_WholeNodeJobs(createAndDelete):
378378
(None, None, {"mpTag": True, "MaxRAM": 8}, 1, 8),
379379
(None, None, {"mpTag": True, "wholeNode": True}, 16, 0),
380380
(None, None, {"mpTag": True, "wholeNode": False}, 1, 0),
381+
(None, None, {"mpTag": True, "numberOfProcessors": 4, "MinRAM": 2}, 4, 2),
381382
(None, None, {"mpTag": True, "numberOfProcessors": 4, "MaxRAM": 4}, 4, 4),
383+
(None, None, {"mpTag": True, "numberOfProcessors": 4, "MaxRAM": 36}, 4, None),
384+
(None, None, {"mpTag": True, "numberOfProcessors": 4, "MinRAM": 2, "MaxRAM": 4}, 4, 4),
382385
(None, None, {"mpTag": True, "numberOfProcessors": 4, "maxNumberOfProcessors": 8}, 8, 0),
383386
(None, None, {"mpTag": True, "numberOfProcessors": 4, "maxNumberOfProcessors": 32}, 16, 0),
384387
({1: 4}, {1: 4}, {"mpTag": True, "wholeNode": True}, 0, 0),
385388
({1: 4}, {1: 4}, {"mpTag": True, "wholeNode": False}, 1, 0),
386-
({1: 4}, {1: 4}, {"mpTag": True, "numberOfProcessors": 2, "MaxRAM": 8}, 2, 8),
387-
({1: 4}, {1: 4}, {"mpTag": True, "numberOfProcessors": 16, "MaxRAM": 12}, 0, 12),
389+
({1: 4}, {1: 4}, {"mpTag": True, "numberOfProcessors": 2, "MinRAM": 8}, 2, 8),
390+
({1: 4}, {1: 4}, {"mpTag": True, "numberOfProcessors": 16, "MinRAM": 8, "MaxRAM": 12}, 0, 12),
388391
({1: 4}, {1: 4}, {"mpTag": True, "maxNumberOfProcessors": 2, "MaxRAM": 16}, 2, 16),
392+
({1: 4}, {1: 4}, {"mpTag": True, "numberOfProcessors": 2, "MaxRAM": 8}, 2, 8),
389393
({1: 4}, {1: 4}, {"mpTag": True, "maxNumberOfProcessors": 16, "MaxRAM": 32}, 12, None),
390394
({1: 4, 2: 8}, {1: 4}, {"mpTag": True, "numberOfProcessors": 2}, 2, 0),
391395
({1: 4, 2: 8}, {1: 4}, {"mpTag": True, "numberOfProcessors": 4}, 4, 0),

src/DIRAC/WorkloadManagementSystem/Utilities/JobParameters.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ def getAvailableRAM(siteName=None, gridCE=None, queue=None):
213213
The siteName/gridCE/queue parameters are normally not necessary.
214214
215215
Tries to find it in this order:
216-
1) from the /Resources/Computing/CEDefaults/AvailableRAM (which is what the pilot might fill up)
217-
2) if not present looks in CS for "AvailableRAM" Queue or CE option
216+
1) from the /Resources/Computing/CEDefaults/MaxRAM (which is what the pilot might fill up)
217+
2) if not present looks in CS for "MemoryLimitMB" Queue or CE or site option
218218
3) if not present but there's WholeNode tag, look what the WN provides using _getMemoryFromProc()
219219
4) return 0
220220
"""
@@ -238,9 +238,9 @@ def getAvailableRAM(siteName=None, gridCE=None, queue=None):
238238

239239
grid = siteName.split(".")[0]
240240
csPaths = [
241-
f"/Resources/Sites/{grid}/{siteName}/CEs/{gridCE}/Queues/{queue}/MaxRAM",
242-
f"/Resources/Sites/{grid}/{siteName}/CEs/{gridCE}/MaxRAM",
243-
f"/Resources/Sites/{grid}/{siteName}/MaxRAM",
241+
f"/Resources/Sites/{grid}/{siteName}/CEs/{gridCE}/Queues/{queue}/MemoryLimitMB",
242+
f"/Resources/Sites/{grid}/{siteName}/CEs/{gridCE}/MemoryLimitMB",
243+
f"/Resources/Sites/{grid}/{siteName}/MemoryLimitMB",
244244
]
245245
for csPath in csPaths:
246246
gLogger.info("Looking in", csPath)
@@ -265,5 +265,22 @@ def getAvailableRAM(siteName=None, gridCE=None, queue=None):
265265
return _getMemoryFromProc()
266266

267267
# 4) return 0
268-
gLogger.info("AvailableRAM could not be found in CS, and WholeNode tag not found")
268+
gLogger.info("RAM limits could not be found in CS, and WholeNode tag not found")
269269
return 0
270+
271+
272+
def getRAMForJob(jobID):
273+
"""Gets the RAM allowed for the job.
274+
This can be used to communicate to your job payload the RAM it's allowed to use,
275+
so this function should be called from your extension.
276+
277+
If the JobAgent is using "InProcess" CE (which is the default),
278+
then what's returned will basically be the same of what's returned by the getAvailableRAM() function above
279+
"""
280+
281+
# from /Resources/Computing/JobLimits/jobID/MaxRAM (set by PoolComputingElement)
282+
ram = gConfig.getValue(f"Resources/Computing/JobLimits/{jobID}/MaxRAM")
283+
if ram:
284+
return ram
285+
286+
return getAvailableRAM()

0 commit comments

Comments
 (0)