Skip to content

Commit 20c8037

Browse files
authored
Merge branch 'main' into dirigent_workflow_rps_support
2 parents 7aa0410 + f2113a7 commit 20c8037

File tree

12 files changed

+598
-26
lines changed

12 files changed

+598
-26
lines changed

.github/configs/wordlist.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,4 +827,6 @@ InData
827827
RpsFile
828828
RpsRequestedGpu
829829
gpus
830-
SoCC
830+
SoCC
831+
SweepOptions
832+
SweepType

.github/workflows/tools-tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
strategy:
2323
fail-fast: false
2424
matrix:
25-
module: [ tools/plotter, tools/multi_loader/runner]
25+
module: [ tools/plotter, tools/multi_loader/runner, tools/multi_loader/common]
2626
steps:
2727
- name: Set up Golang
2828
uses: actions/setup-go@v5

docs/multi_loader.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ As a wrapper around loader, multi-loader requires the initial cluster setup to b
2828
| Verbosity | string | "info", "debug", "trace" | "info" | (Optional) Verbosity level for logging the experiment |
2929
| PreScript | string | any bash Command | "" | (Optional) Local script that runs this specific experiment |
3030
| PostScript | string | any bash Command | "" | (Optional) Local script that runs this specific experiment |
31+
| Sweep | [][SweepOptions](#sweepoptions) | N/A | N/A | (Optional) List of sweep options for the experiment |
32+
| SweepType | string | "linear", "grid" | "grid" | (Optional) Determines how the sweep options are applied: `grid` means all permutations, while `linear` pairs corresponding elements from sweep options, similar to Python's zip() function |
3133

3234
> **_Important_**:
3335
>
@@ -51,6 +53,37 @@ As a wrapper around loader, multi-loader requires the initial cluster setup to b
5153
> Any field defined in `Config` will override the corresponding value from the configuration in `BaseConfigPath`, but only for that specific experiment.
5254
> For example, if `BaseConfigPath` has `ExperimentDuration` set to 5 minutes, and you define `ExperimentDuration` as 10 minutes in `Config`, that particular experiment will run for 10 minutes instead.
5355
56+
### SweepOptions
57+
58+
| Parameter name | Data type | Possible values | Default value | Description |
59+
|----------------|---------------|-------------------------------|---------------|----------------------------------------------------------------------------|
60+
| Field | string | Any valid field name | N/A | Any field in [LoaderConfiguration](https://github.com/vhive-serverless/invitro/blob/main/docs/configuration.md#loader-configuration-file-format), `PreScript` and `PostScript`. Cannot be `TracePath`, `OutputDir` or `Platform` as they are reserved fields |
61+
| Values | []interface{} | Any valid value type | N/A | List of values for the sweep options. These will be used as the sweep values for the experiment |
62+
| Format | string | Any string containing `{}` | N/A | Optional format string to customize the sweep. The `{}` placeholder should be included to replace with sweep values |
63+
64+
#### Example:
65+
- `Field` should not be empty.
66+
- Reserved fields like `TracePath` and `OutputDir` cannot be used.
67+
- `Values` must contain at least one item.
68+
- If `Format` is provided, it must include the placeholder `{}`.
69+
70+
```json
71+
{
72+
"Field": "ExperimentDuration",
73+
"Values": [10, 20],
74+
},
75+
{
76+
"Field": "Prescript",
77+
"Values": ["example1", "example2", "example3"],
78+
"Format": "command_{}_to_run"
79+
}
80+
```
81+
82+
> **_Note_**:
83+
> You can use PreScript and PostScript as sweep options; however, these scripts will only execute at the local level. This means that the global PreScript and PostScript will not be overridden by the sweep-specific scripts.
84+
85+
86+
5487
## Command Flags
5588

5689
The multi-loader accepts the following command-line flags.

pkg/common/utilities.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,14 @@ func DeepCopy[T any](a T) (T, error) {
162162
return b, err
163163
}
164164

165-
func RunScript(command string) {
165+
func RunCommand(command string) {
166166
if command == "" {
167167
return
168168
}
169169
logger.Debug("Running command ", command)
170-
cmd, err := exec.Command("/bin/sh", command).Output()
170+
cmd, err := exec.Command("sh", "-c", command).Output()
171171
if err != nil {
172-
log.Fatal(err)
172+
logger.Fatal(err)
173173
}
174174
logger.Debug(string(cmd))
175175
}

tools/multi_loader/common/constants.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,10 @@ package common
33
const (
44
TraceFormatString = "{}"
55
)
6+
7+
const (
8+
GridSweepType = "grid"
9+
LinearSweepType = "linear"
10+
)
11+
12+
var SweepTypes = []string{GridSweepType, LinearSweepType}

tools/multi_loader/common/utils.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package common
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"os"
7+
"path"
8+
"path/filepath"
69

710
log "github.com/sirupsen/logrus"
811

@@ -50,3 +53,75 @@ func DeterminePlatformFromConfig(multiLoaderConfig types.MultiLoaderConfiguratio
5053
}
5154
return loaderConfig.Platform
5255
}
56+
57+
/**
58+
* NextCProduct generates the next Cartesian product of the given limits
59+
**/
60+
func NextCProduct(limits []int) func() []int {
61+
permutations := make([]int, len(limits))
62+
indices := make([]int, len(limits))
63+
done := false
64+
65+
return func() []int {
66+
// Check if there are more permutations
67+
if done {
68+
return nil
69+
}
70+
71+
// Generate the current permutation
72+
copy(permutations, indices)
73+
74+
// Generate the next permutation
75+
for i := len(indices) - 1; i >= 0; i-- {
76+
indices[i]++
77+
if indices[i] <= limits[i] {
78+
break
79+
}
80+
indices[i] = 0
81+
if i == 0 {
82+
// All permutations have been generated
83+
done = true
84+
}
85+
}
86+
87+
return permutations
88+
}
89+
}
90+
91+
func SplitPath(path string) []string {
92+
dir, last := filepath.Split(path)
93+
if dir == "" {
94+
return []string{last}
95+
}
96+
return append(SplitPath(filepath.Clean(dir)), last)
97+
}
98+
99+
func SweepOptionsToPostfix(sweepOptions []types.SweepOptions, selectedSweepValues []int) string {
100+
var postfix string
101+
for i, sweepOption := range sweepOptions {
102+
postfix += fmt.Sprintf("_%s_%v", sweepOption.Field, sweepOption.Values[selectedSweepValues[i]])
103+
}
104+
return postfix
105+
}
106+
107+
func UpdateExperimentWithSweepIndices(experiment *types.LoaderExperiment, sweepOptions []types.SweepOptions, selectedSweepValues []int) {
108+
experimentPostFix := SweepOptionsToPostfix(sweepOptions, selectedSweepValues)
109+
110+
experiment.Name = experiment.Name + experimentPostFix
111+
paths := SplitPath(experiment.Config["OutputPathPrefix"].(string))
112+
// update the last two paths with the sweep indices
113+
paths[len(paths)-2] = paths[len(paths)-2] + experimentPostFix
114+
115+
experiment.Config["OutputPathPrefix"] = path.Join(paths...)
116+
117+
for sweepOptionI, sweepValueI := range selectedSweepValues {
118+
sweepValue := sweepOptions[sweepOptionI].GetValue(sweepValueI)
119+
if sweepOptions[sweepOptionI].Field == "PreScript" {
120+
experiment.PreScript = sweepValue.(string)
121+
} else if sweepOptions[sweepOptionI].Field == "PostScript" {
122+
experiment.PostScript = sweepValue.(string)
123+
} else {
124+
experiment.Config[sweepOptions[sweepOptionI].Field] = sweepValue
125+
}
126+
}
127+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package common
2+
3+
import (
4+
"path"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/vhive-serverless/loader/tools/multi_loader/types"
10+
)
11+
12+
func TestNextProduct(t *testing.T) {
13+
ints := []int{2, 1}
14+
nextProduct := NextCProduct(ints)
15+
expectedArrs := [][]int{{0, 0}, {0, 1}, {1, 0}, {1, 1}, {2, 0}, {2, 1}}
16+
curI := 0
17+
18+
for {
19+
product := nextProduct()
20+
if len(product) == 0 {
21+
if curI != len(expectedArrs) {
22+
t.Fatalf("Expected %d products, got %d", len(expectedArrs), curI)
23+
}
24+
break
25+
}
26+
if len(product) != len(ints) {
27+
t.Fatalf("Expected product length %d, got %d", len(ints), len(product))
28+
}
29+
for i, v := range product {
30+
if v != expectedArrs[curI][i] {
31+
t.Fatalf("Expected %v, got %v", expectedArrs[curI], product)
32+
}
33+
}
34+
curI++
35+
}
36+
}
37+
38+
func TestSplitPath(t *testing.T) {
39+
assert.Equal(t, []string{"file.txt"}, SplitPath("file.txt"), "Expected ['file.txt'] for single file")
40+
assert.Equal(t, []string{"home", "user", "docs", "file.txt"}, SplitPath(filepath.Join("home", "user", "docs", "file.txt")), "Expected full path split")
41+
}
42+
43+
func TestSweepOptionsToPostfix(t *testing.T) {
44+
t.Run("Test Post Fix Naming Util", func(t *testing.T) {
45+
result := SweepOptionsToPostfix(
46+
[]types.SweepOptions{
47+
{Field: "PreScript", Values: []interface{}{"PreValue_1", "PreValue_2"}},
48+
{Field: "CPULimit", Values: []interface{}{"1vCPU", "2vCPU", "4vCPU"}},
49+
{Field: "ExperimentDuration", Values: []interface{}{"10", "20", "30"}},
50+
{Field: "PostScript", Values: []interface{}{"PostValue_1", "PostValue_2", "PostValue_3"}},
51+
},
52+
[]int{1, 2, 0, 2},
53+
)
54+
assert.Equal(t, "_PreScript_PreValue_2_CPULimit_4vCPU_ExperimentDuration_10_PostScript_PostValue_3", result, "Unexpected postfix result")
55+
})
56+
}
57+
58+
func TestUpdateExperimentWithSweepIndices(t *testing.T) {
59+
experiment := &types.LoaderExperiment{
60+
Name: "test_experiment",
61+
Config: map[string]interface{}{"OutputPathPrefix": "first/second/third"},
62+
}
63+
64+
// Test sweep options
65+
sweepOptions := []types.SweepOptions{
66+
{Field: "PreScript", Values: []interface{}{"pre1", "pre2"}, Format: "test_{}"},
67+
{Field: "PostScript", Values: []interface{}{"post1", "post2"}, Format: ""},
68+
{Field: "ExperimentDuration", Values: []interface{}{"1", "2"}, Format: ""},
69+
}
70+
71+
selectedSweepValues := []int{1, 0, 1}
72+
73+
UpdateExperimentWithSweepIndices(experiment, sweepOptions, selectedSweepValues)
74+
75+
expectedPostfix := "_PreScript_pre2_PostScript_post1_ExperimentDuration_2"
76+
77+
// Check experiment name
78+
assert.Equal(t, "test_experiment"+expectedPostfix, experiment.Name)
79+
80+
// Check OutputPathPrefix
81+
expectedOutputPathPrefix := path.Join("first", "second"+expectedPostfix, "third")
82+
assert.Equal(t, expectedOutputPathPrefix, experiment.Config["OutputPathPrefix"])
83+
84+
// Verify that the sweep options have been updated
85+
assert.Equal(t, "test_pre2", experiment.PreScript)
86+
assert.Equal(t, "post1", experiment.PostScript)
87+
assert.Equal(t, "2", experiment.Config["ExperimentDuration"])
88+
}

tools/multi_loader/common/validators.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package common
22

33
import (
44
"path"
5+
"slices"
56
"strings"
67

78
log "github.com/sirupsen/logrus"
@@ -51,6 +52,21 @@ func CheckMultiLoaderConfig(multiLoaderConfig types.MultiLoaderConfiguration) {
5152
log.Warn("Setting default output directory to ", study.OutputDir)
5253
}
5354
}
55+
56+
// Check sweep options
57+
for _, sweep := range study.Sweep {
58+
if err := sweep.Validate(); err != nil {
59+
log.Fatal(err)
60+
}
61+
}
62+
// check sweep type
63+
CheckSweepType(study.SweepType)
5464
}
5565
log.Debug("All experiments configs are valid")
5666
}
67+
68+
func CheckSweepType(sweepType string) {
69+
if sweepType != "" && !slices.Contains(SweepTypes, sweepType) {
70+
log.Fatal("Invalid Sweep Type ", sweepType)
71+
}
72+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package common
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
log "github.com/sirupsen/logrus"
9+
)
10+
11+
func TestSweepTypeValidator(t *testing.T) {
12+
t.Run("Check Grid Sweep Type", func(t *testing.T) {
13+
CheckSweepType("grid")
14+
})
15+
t.Run("Check Random Sweep Type", func(t *testing.T) {
16+
expectFatal(t, func() {
17+
CheckSweepType("random")
18+
})
19+
})
20+
t.Run(("Check Linear Sweep Type"), func(t *testing.T) {
21+
CheckSweepType("linear")
22+
})
23+
24+
}
25+
26+
func expectFatal(t *testing.T, funcToTest func()) {
27+
fatal := false
28+
originalExitFunc := log.StandardLogger().ExitFunc
29+
log.Info("Expecting a fatal message during the test, overriding the exit function")
30+
// Replace logrus exit function
31+
log.StandardLogger().ExitFunc = func(int) {
32+
fatal = true
33+
t.SkipNow()
34+
}
35+
defer func() {
36+
log.StandardLogger().ExitFunc = originalExitFunc
37+
assert.True(t, fatal, "Expected log.Fatal to be called")
38+
}()
39+
funcToTest()
40+
}

0 commit comments

Comments
 (0)