Skip to content

Commit 1a8888e

Browse files
committed
merge in develop
Signed-off-by: Neil South <[email protected]>
2 parents 63a587a + 4c630ca commit 1a8888e

File tree

7 files changed

+47
-105
lines changed

7 files changed

+47
-105
lines changed

docs/setup/mwm-workflow-spec.md

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ A workflow is a standard template that contains a list of tasks that can be ran
2424

2525
The first task to be ran, will always be the first task in the list. The next task/tasks to be ran must be listed in the [Task Destinations](#task-destinations) of the task. A workflow requires at least one task.
2626

27-
Workflows can be created or updated via the [Workflow API](https://github.com/Project-MONAI/monai-deploy-workflow-manager/blob/develop/docs/api/rest/workflow.md).
27+
Workflows can be created or updated via the [Workflow API](https://github.com/Project-MONAI/monai-deploy-workflow-manager/blob/develop/docs/api/rest/workflow.md).
2828

2929
# Contents
3030

@@ -142,7 +142,7 @@ The following is an example of the structure of a workflow.
142142
The following is an example of a complete workflow:
143143
![scenario1](../images/workflow_examples/scenario1.png)
144144

145-
An example of a workflow with two tasks:
145+
An example of a workflow with two tasks:
146146

147147
1. Argo task
148148
2. Export Task
@@ -240,7 +240,7 @@ It also defines the "PROD_PACS" output destination, meaning that it can be used:
240240
Tasks are the basic building block of a workflow. They are provided as a list - the first Task in the list is executed when the workflow is triggered.
241241
Subsequent tasks are triggered by the `task_destinations` specified by previous tasks.
242242

243-
# Task Object
243+
# Task Object
244244

245245
### Task Types
246246
These tasks are borken down into different types:
@@ -292,8 +292,8 @@ The following are examples of the task json structure including required args fo
292292
]
293293
},
294294
"task_destinations": [
295-
{
296-
"name": "export-task-id"
295+
{
296+
"name": "export-task-id"
297297
}
298298
]
299299
}
@@ -364,7 +364,7 @@ Depending of the type of task, the task object may contain additional fields.
364364
Router tasks don't have additional fields. They are used to contain `task_destinations` so that workflow processing can be directed to the desired next step.
365365

366366
#### Export
367-
These are task types that allow for artifacts to be exported based on the input artifacts list. This task type should not have Out artifacts listed.
367+
These are task types that allow for artifacts to be exported based on the input artifacts list. This task type should not have Out artifacts listed.
368368
The task also requires these extra attributes:-
369369

370370
| Property | Type | Description |
@@ -403,7 +403,7 @@ Example (output sent to another task if the patient is female, otherwise to PACS
403403
Export destinations define an external location to which the output of the task can be sent. This will take the form of an event published to a pub/sub service notifying of an available export to a specific destination reference. Most commonly, the export location will be a PACs system and the notification will be picked up by the Monai Informatics Gateway.
404404

405405
#### Plugin
406-
These are tasks are Named the same as the installed Pluging.
406+
These are tasks are Named the same as the installed Pluging.
407407
The task also requires these extra attributes:-
408408

409409
| Property | Type | Description |
@@ -414,7 +414,7 @@ The task also requires these extra attributes:-
414414
The args requirements for argo plugin can be found [here](#argo).
415415

416416
### Task Arguments
417-
Each task plugin requires specific arguments to be provided in the args dictionary. This allows all task types to support as many additional values as necessary without the need to bloat the workflow spec.
417+
Each task plugin requires specific arguments to be provided in the args dictionary. This allows all task types to support as many additional values as necessary without the need to bloat the workflow spec.
418418

419419
#### Argo
420420
The Argo plugin triggers workflows pre-deployed onto an [Argo workflow server](https://argoproj.github.io/argo-events/).
@@ -425,25 +425,11 @@ The Task's "args" object should contain the following fields:
425425

426426
| Property | Type | Required | Description |
427427
|------|------|------|------|
428-
|workflow_template_name|str|Yes|The ID of this workflow as registered on the Argo server.|
429-
|namespace|str|Yes|The namespace of the argo workflow.|
430-
|server_url|url|Yes|The URL of the Argo server.|
431-
|allow_insecure|bool|No|Allow insecure connections to argo from the plug-in.|
432-
|parameters|dictionary|No|Key value pairs, Argo parameters that will be passed on to the Argo workflow.|
433-
|priority_class|string|No|The name of a valid Kubernetes priority class to be assigned to the Argo workflow pods|
434-
|resources|dictionary|No|A resource requests & limits object (see below). These will be applied to the Argo workflow pods|
435-
436-
##### Resource Request Object
437-
438-
Resource request parameters should be included in the task args object dictionary, as a string dictionary. The resources dictionary and all included values below are optional.
439-
440-
| Property | Type | Description |
441-
|------|------|------|
442-
|memory_reservation|str|A valid [Kubernetes memory request value](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory).|
443-
|cpu_reservation|url|A valid [Kubernetes CPU request value](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu).|
444-
|gpu_limit|dictionary|The number of GPUs to be used by this task.|
445-
|memory_limit|string|The maximum amount of memory this task may use|
446-
|cpu_limit|object|The maximum amount of CPU this task may use. See |
428+
|workflow_template_name|string|Yes|The ID of this workflow as registered on the Argo server.|
429+
|priority_class|string|No|The name of a valid Kubernetes priority class to be assigned to the Argo workflow pods.|
430+
|gpu_required|string|No|Whether a GPU is to be used by this task.|
431+
|memory_gb|string|No|The maximum amount of memory in gigabytes this task may use.|
432+
|cpu|string|No|The maximum amount of CPU this task may use.|
447433

448434
For more information about Kubernetes requests & limits, see https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.
449435

@@ -501,7 +487,7 @@ As you can see in the example below, input artifacts require a _value_. This is
501487

502488
#### DICOM Input
503489

504-
If payload DICOM inputs are to be used in a given task, the value of the input must be `context.input.dicom`. This will to resolve to the `{payloadId}/dcm` folder within Minio / S3.
490+
If payload DICOM inputs are to be used in a given task, the value of the input must be `context.input.dicom`. This will to resolve to the `{payloadId}/dcm` folder within Minio / S3.
505491

506492
Example:
507493
```json
@@ -700,11 +686,11 @@ The following examples both function the same and act as an AND condition.
700686
## Evaluators
701687
Conditional evaluators are logical statement strings that may be used to determine which tasks are executed. They can make use of the execution context _metadata_ and dicom tags. All conditions must evaluate to true in order for the task to be triggered.
702688

703-
[A detailed breakdown of conditional logic can be found here.](https://github.com/Project-MONAI/monai-deploy-workflow-manager/blob/develop/guidelines/mwm-conditionals.md)
689+
[A detailed breakdown of conditional logic can be found here.](https://github.com/Project-MONAI/monai-deploy-workflow-manager/blob/develop/guidelines/mwm-conditionals.md)
704690

705691
### Supported Evaulators
706692

707-
693+
708694
Conditional evaluators should support evaluating workflow variables against predefined values with the following operators:
709695

710696
< (Valid for integers)
@@ -765,9 +751,9 @@ Example (status):
765751

766752
#### Result Metadata & Execution Stats - Using Dictionary Values
767753

768-
The Result Metadata and Execution Stats are populated by the plugin and are added to the workflow instance once a task is completed to provide some output of a task. Each plugin will have its own implementation to populate the result metadata.
754+
The Result Metadata and Execution Stats are populated by the plugin and are added to the workflow instance once a task is completed to provide some output of a task. Each plugin will have its own implementation to populate the result metadata.
769755

770-
Because `result` and `execution_stats` are a dictionary, the section after `context.executions.task_id.result` or `context.executions.task_id.execution_stats` is the key to be checked in the result/execution_stats dictionary.
756+
Because `result` and `execution_stats` are a dictionary, the section after `context.executions.task_id.result` or `context.executions.task_id.execution_stats` is the key to be checked in the result/execution_stats dictionary.
771757

772758
For conditional statements, the key specified is case sensitive and must match exactly to the key which has been output by the model and saved in the result/execution_stats dictionary.
773759

@@ -807,9 +793,9 @@ The result metadata for an Argo task is populated by a `metadata.json` that is i
807793
}
808794
```
809795

810-
If metadata is to be used in a conditional the `metadata.json` must be present somewhere in the output directory and a valid JSON dictionary. It will automatically be imported if it is in the directory.
796+
If metadata is to be used in a conditional the `metadata.json` must be present somewhere in the output directory and a valid JSON dictionary. It will automatically be imported if it is in the directory.
811797

812-
An example format of the metadata.json can be found below:
798+
An example format of the metadata.json can be found below:
813799

814800
execution stats are populated from the argo execution values returned automatically.
815801

@@ -916,4 +902,4 @@ Name:
916902
Description:
917903
```python
918904
{{context.workflow.description}} == 'This workflow is a valid workflow'
919-
```
905+
```

src/Shared/Shared/ValidationConstants.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public static class ValidationConstants
6666
/// <summary>
6767
/// Key for the GPU.
6868
/// </summary>
69-
public static readonly string Gpu = "gpu";
69+
public static readonly string GpuRequired = "gpu_required";
7070

7171
public enum ModeValues
7272
{

src/TaskManager/Plug-ins/Argo/ArgoPlugin.cs

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -432,46 +432,23 @@ private async Task<Workflow> BuildWorkflowWrapper(CancellationToken cancellation
432432
private void ProcessTaskPluginArguments(Workflow workflow)
433433
{
434434
Guard.Against.Null(workflow);
435-
436-
var resources = Event.GetTaskPluginArgumentsParameter<Dictionary<string, string>>(Keys.ArgoResource);
437435
var priorityClassName = Event.GetTaskPluginArgumentsParameter(Keys.TaskPriorityClassName) ?? "standard";
438-
var argoParameters = Event.GetTaskPluginArgumentsParameter<Dictionary<string, string>>(Keys.ArgoParameters);
439-
440-
if (argoParameters is not null)
441-
{
442-
foreach (var item in argoParameters)
443-
{
444-
if (workflow.Spec.Arguments is null)
445-
{
446-
workflow.Spec.Arguments = new Arguments();
447-
}
448-
449-
if (workflow.Spec.Arguments.Parameters is null)
450-
{
451-
workflow.Spec.Arguments.Parameters = new List<Parameter>();
452-
}
453-
454-
workflow.Spec.Arguments.Parameters.Add(new Parameter() { Name = item.Key, Value = item.Value });
455-
}
456-
}
457436

458437
foreach (var template in workflow.Spec.Templates)
459438
{
460-
AddLimit(resources, template, ResourcesKeys.CpuLimit);
461-
AddLimit(resources, template, ResourcesKeys.MemoryLimit);
462-
AddRequest(resources, template, ResourcesKeys.CpuReservation);
463-
AddRequest(resources, template, ResourcesKeys.MemoryReservation);
464-
AddRequest(resources, template, ResourcesKeys.GpuLimit);
439+
AddLimit(template, ResourcesKeys.CpuLimit);
440+
AddLimit(template, ResourcesKeys.MemoryLimit);
441+
AddLimit(template, ResourcesKeys.GpuLimit);
465442
template.PriorityClassName = priorityClassName;
466443
}
467444
workflow.Spec.PodPriorityClassName = priorityClassName;
468445
}
469446

470-
private static void AddLimit(Dictionary<string, string>? resources, Template2 template, ResourcesKey key)
447+
private void AddLimit(Template2 template, ResourcesKey key)
471448
{
472449
Guard.Against.Null(template);
473450
Guard.Against.Null(key);
474-
if (template.Container is null || resources is null || !resources.TryGetValue(key.TaskKey, out var value))
451+
if (template.Container is null || !Event.TaskPluginArguments.TryGetValue(key.TaskKey, out var value) || string.IsNullOrWhiteSpace(value))
475452
{
476453
return;
477454
}
@@ -484,28 +461,13 @@ private static void AddLimit(Dictionary<string, string>? resources, Template2 te
484461
template.Container.Resources.Limits = new Dictionary<string, string>();
485462
}
486463

487-
template.Container.Resources.Limits.Add(key.ArgoKey, value);
488-
}
489-
490-
private static void AddRequest(Dictionary<string, string>? resources, Template2 template, ResourcesKey key)
491-
{
492-
Guard.Against.Null(template);
493-
Guard.Against.Null(key);
494-
if (template.Container is null || resources is null || !resources.TryGetValue(key.TaskKey, out var value) || string.IsNullOrWhiteSpace(value))
464+
// Convert true / false value to 0 or 1 for number of GPU
465+
if (key.TaskKey == ResourcesKeys.GpuLimit.TaskKey)
495466
{
496-
return;
467+
value = bool.TryParse(value, out bool gpuRequired) && gpuRequired ? "1" : "0";
497468
}
498469

499-
if (template.Container.Resources is null)
500-
{
501-
template.Container.Resources = new ResourceRequirements();
502-
}
503-
if (template.Container.Resources.Requests is null)
504-
{
505-
template.Container.Resources.Requests = new Dictionary<string, string>();
506-
}
507-
508-
template.Container.Resources.Requests.Add(key.ArgoKey, value);
470+
template.Container.Resources.Limits.Add(key.ArgoKey, value);
509471
}
510472

511473
private async Task AddMainWorkflowTemplate(Workflow workflow, CancellationToken cancellationToken)

src/TaskManager/Plug-ins/Argo/StaticValues/ResourcesKeys.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ public static class ResourcesKeys
2222

2323
public static readonly ResourcesKey CpuReservation = new() { TaskKey = "cpu_reservation", ArgoKey = "requests.cpu" };
2424

25-
public static readonly ResourcesKey GpuLimit = new() { TaskKey = "gpu_limit", ArgoKey = "nvidia.com/gpu" };
25+
public static readonly ResourcesKey GpuLimit = new() { TaskKey = "gpu_required", ArgoKey = "nvidia.com/gpu" };
2626

27-
public static readonly ResourcesKey MemoryLimit = new() { TaskKey = "memory_limit", ArgoKey = "limits.memory" };
27+
public static readonly ResourcesKey MemoryLimit = new() { TaskKey = "memory_gb", ArgoKey = "limits.memory" };
2828

29-
public static readonly ResourcesKey CpuLimit = new() { TaskKey = "cpu_limit", ArgoKey = "limits.cpu" };
29+
public static readonly ResourcesKey CpuLimit = new() { TaskKey = "cpu", ArgoKey = "limits.cpu" };
3030
}
3131
}

src/WorkflowManager/WorkflowManager/Validators/WorkflowValidator.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -353,21 +353,18 @@ private void ValidateArgoTask(TaskObject currentTask)
353353
{
354354
if (
355355
currentTask.Args.TryGetValue(key, out var val) &&
356-
!string.IsNullOrEmpty(val) &&
357-
double.TryParse(val, out double parsedVal) &&
358-
(parsedVal < 1 || Math.Truncate(parsedVal) != parsedVal))
356+
(string.IsNullOrEmpty(val) ||
357+
(double.TryParse(val, out double parsedVal) && (parsedVal < 1 || Math.Truncate(parsedVal) != parsedVal))))
359358
{
360359
Errors.Add($"Task: '{currentTask.Id}' value '{val}' provided for argument '{key}' is not valid. The value needs to be a whole number greater than 0.");
361360
}
362361
});
363362

364363
if (
365-
currentTask.Args.TryGetValue(Gpu, out var gpu) &&
366-
!string.IsNullOrEmpty(gpu) &&
367-
double.TryParse(gpu, out double parsedGpu) &&
368-
(parsedGpu != 0 || parsedGpu != 1))
364+
currentTask.Args.TryGetValue(GpuRequired, out var gpuRequired) &&
365+
(string.IsNullOrEmpty(gpuRequired) || !bool.TryParse(gpuRequired, out var _)))
369366
{
370-
Errors.Add($"Task: '{currentTask.Id}' value '{gpu}' provided for argument '{Gpu}' is not valid. The value needs to be 0 or 1.");
367+
Errors.Add($"Task: '{currentTask.Id}' value '{gpuRequired}' provided for argument '{GpuRequired}' is not valid. The value needs to be 'true' or 'false'.");
371368
}
372369
}
373370

tests/UnitTests/TaskManager.Argo.Tests/ArgoPluginTest.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,9 @@ public async Task ArgoPlugin_ExecuteTask_WorkflowTemplates(string filename, int
272272
Assert.NotNull(argoTemplate);
273273

274274
var message = GenerateTaskDispatchEventWithValidArguments(withoutDefaultArguments);
275-
message.TaskPluginArguments["resources"] = "{\"memory_reservation\": \"string\",\"cpu_reservation\": \"string\",\"gpu_limit\": 1,\"memory_limit\": \"string\",\"cpu_limit\": \"string\"}";
275+
message.TaskPluginArguments["gpu_required"] = "true";
276+
message.TaskPluginArguments["memory_gb"] = "1";
277+
message.TaskPluginArguments["cpu"] = "1";
276278
message.TaskPluginArguments["priority"] = "Helo";
277279
Workflow? submittedArgoTemplate = null;
278280

@@ -367,18 +369,13 @@ private static void ValidateSimpleTemplate(TaskDispatchEvent message, Workflow w
367369
{
368370
Assert.True(template.Container.Resources is not null);
369371
Assert.True(template.Container.Resources?.Limits is not null);
370-
Assert.True(template.Container.Resources?.Requests is not null);
371372
var value = "";
372373

373-
Assert.True(template.Container.Resources?.Requests?.TryGetValue("requests.memory", out value));
374-
Assert.True(value == "string");
375-
Assert.True(template.Container.Resources?.Requests?.TryGetValue("requests.cpu", out value));
376-
Assert.True(value == "string");
377374
Assert.True(template.Container.Resources?.Limits?.TryGetValue("limits.memory", out value));
378-
Assert.True(value == "string");
375+
Assert.True(value == "1");
379376
Assert.True(template.Container.Resources?.Limits?.TryGetValue("limits.cpu", out value));
380-
Assert.True(value == "string");
381-
Assert.True(template.Container.Resources?.Requests?.TryGetValue("nvidia.com/gpu", out value));
377+
Assert.True(value == "1");
378+
Assert.True(template.Container.Resources?.Limits?.TryGetValue("nvidia.com/gpu", out value));
382379
Assert.True(value == "1");
383380

384381
Assert.True(template.PriorityClassName == "Helo");

tests/UnitTests/WorkflowManager.Tests/Validators/WorkflowValidatorTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ public async Task ValidateWorkflow_ValidatesAWorkflow_ReturnsErrorsAndHasCorrect
207207
{ "example", "value" },
208208
{ "cpu", "0.1" },
209209
{ "memory_gb", "0.1" },
210-
{ "gpu", "2" }
210+
{ "gpu_required", "2" }
211211
},
212212
TaskDestinations = new TaskDestination[]
213213
{
@@ -392,7 +392,7 @@ public async Task ValidateWorkflow_ValidatesAWorkflow_ReturnsErrorsAndHasCorrect
392392
var invalidArgoArg2 = "Task: 'test-argo-task' value '0.1' provided for argument 'memory_gb' is not valid. The value needs to be a whole number greater than 0.";
393393
Assert.Contains(invalidArgoArg2, errors);
394394

395-
var invalidArgoArg3 = "Task: 'test-argo-task' value '2' provided for argument 'gpu' is not valid. The value needs to be 0 or 1.";
395+
var invalidArgoArg3 = "Task: 'test-argo-task' value '2' provided for argument 'gpu_required' is not valid. The value needs to be 'true' or 'false'.";
396396
Assert.Contains(invalidArgoArg3, errors);
397397

398398
var incorrectClinicalReviewValueFormat = $"Invalid Value property on input artifact 'Invalid Value Format' in task: 'test-clinical-review'. Incorrect format.";

0 commit comments

Comments
 (0)