Skip to content

Commit f2e8f9e

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

8 files changed

Lines changed: 88 additions & 14 deletions

File tree

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: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ def __init__(self, ceUniqueID):
6161
self.taskID = 0
6262
self.processorsPerTask = {}
6363
self.userNumberPerTask = {}
64-
self.ram = 1024 # Default RAM in GB (this is an arbitrary large value in case of no limit)
64+
self.ram = (
65+
1024 # Available RAM for the node, in GB. The default value is an arbitrary large value in case of no limit
66+
)
6567
self.ramPerTask = {}
6668

6769
# This CE will effectively submit to another "Inner"CE
@@ -77,9 +79,8 @@ def _reset(self):
7779
self.processors = int(self.ceParameters.get("NumberOfProcessors", self.processors))
7880
self.ceParameters["MaxTotalJobs"] = self.processors
7981
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
82+
if max_ram:
83+
self.ram = self.ceParameters["MaxRAM"]
8384
# Indicates that the submission is done asynchronously
8485
# The result is not immediately available
8586
self.ceParameters["AsyncSubmission"] = True
@@ -190,14 +191,21 @@ def _getMemoryForJobs(self, kwargs):
190191
"""
191192

192193
# # job requirements
193-
requestedMemory = kwargs.get("MaxRAM", 0)
194+
requestedMemory = kwargs.get("MinRAM", 0)
194195

195196
# # now check what the slot can provide
196197
# Do we have enough memory?
197198
availableMemory = self.ram - sum(self.ramPerTask.values())
198199
if availableMemory < requestedMemory:
199200
return None
200201

202+
# if there's a MaxRAM requested, we allocate it all (if it fits)
203+
if kwargs.get("MaxRAM", 0):
204+
if availableMemory >= kwargs.get("MaxRAM"):
205+
requestedMemory = kwargs.get("MaxRAM")
206+
else:
207+
return None
208+
201209
return requestedMemory
202210

203211
def finalizeJob(self, taskID, future):
@@ -206,10 +214,11 @@ def finalizeJob(self, taskID, future):
206214
:param future: evaluating the future result
207215
"""
208216
nProc = self.processorsPerTask.pop(future)
217+
ram = self.ramPerTask.pop(future, None)
209218

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

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_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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,20 @@ def getAvailableRAM(siteName=None, gridCE=None, queue=None):
267267
# 4) return 0
268268
gLogger.info("AvailableRAM 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)