Skip to content

Commit 6480561

Browse files
authored
implement cluset functionality (#68)
Signed-off-by: Dmitry Shmulevich <[email protected]>
1 parent cfad294 commit 6480561

File tree

5 files changed

+208
-194
lines changed

5 files changed

+208
-194
lines changed

internal/cluset/cluset.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package cluset
18+
19+
import (
20+
"fmt"
21+
"regexp"
22+
"sort"
23+
"strconv"
24+
"strings"
25+
)
26+
27+
var compactRegexp, expandRegexp *regexp.Regexp
28+
29+
func init() {
30+
compactRegexp = regexp.MustCompile(`^(.*?)(0*\d+)$`)
31+
expandRegexp = regexp.MustCompile(`^(.*)\[(.*?)\]$`)
32+
}
33+
34+
// Compact compresses a list of nodes into a compact range format
35+
func Compact(nodes []string) []string {
36+
groups := make(map[string][]string)
37+
38+
// Parse node names into groups
39+
for _, node := range nodes {
40+
matches := compactRegexp.FindStringSubmatch(node)
41+
if matches != nil {
42+
prefix, numStr := matches[1], matches[2]
43+
groups[prefix] = append(groups[prefix], numStr)
44+
} else {
45+
groups[node] = nil
46+
}
47+
}
48+
49+
// Sort prefixes for consistent output
50+
prefixes := make([]string, 0, len(groups))
51+
for prefix := range groups {
52+
prefixes = append(prefixes, prefix)
53+
}
54+
sort.Strings(prefixes)
55+
56+
// Format compressed ranges
57+
var result []string
58+
for _, prefix := range prefixes {
59+
numbers := groups[prefix]
60+
sort.Slice(numbers, func(i, j int) bool {
61+
return atoi(numbers[i]) < atoi(numbers[j])
62+
})
63+
compressed := compressRange(numbers)
64+
result = append(result, fmt.Sprintf("%s%s", prefix, compressed))
65+
}
66+
67+
return result
68+
}
69+
70+
// Expand decompresses a list of compacted node names back to individual entries
71+
func Expand(compressed []string) []string {
72+
var result []string
73+
74+
for _, entry := range compressed {
75+
matches := expandRegexp.FindStringSubmatch(entry)
76+
if matches != nil {
77+
prefix, ranges := matches[1], matches[2]
78+
rangesParts := strings.Split(ranges, ",")
79+
for _, part := range rangesParts {
80+
if strings.Contains(part, "-") {
81+
bounds := strings.Split(part, "-")
82+
width := len(bounds[0]) // Preserve leading zeros
83+
for i := atoi(bounds[0]); i <= atoi(bounds[1]); i++ {
84+
result = append(result, fmt.Sprintf("%s%0*d", prefix, width, i))
85+
}
86+
} else {
87+
result = append(result, fmt.Sprintf("%s%s", prefix, part))
88+
}
89+
}
90+
} else {
91+
result = append(result, entry)
92+
}
93+
}
94+
return result
95+
}
96+
97+
// compressRange converts a list of numbers into a compact range format
98+
func compressRange(numbers []string) string {
99+
switch len(numbers) {
100+
case 0:
101+
return ""
102+
case 1:
103+
return numbers[0]
104+
default:
105+
var parts []string
106+
start, end := numbers[0], numbers[0]
107+
108+
for i := 1; i < len(numbers); i++ {
109+
if atoi(numbers[i]) == atoi(end)+1 {
110+
end = numbers[i]
111+
} else {
112+
parts = append(parts, formatRange(start, end))
113+
start, end = numbers[i], numbers[i]
114+
}
115+
}
116+
parts = append(parts, formatRange(start, end))
117+
118+
return "[" + strings.Join(parts, ",") + "]"
119+
}
120+
}
121+
122+
// formatRange formats a range into a string
123+
func formatRange(start, end string) string {
124+
if start == end {
125+
return start
126+
}
127+
return fmt.Sprintf("%s-%s", start, end)
128+
}
129+
130+
// atoi converts a zero-padded string number to an integer
131+
func atoi(s string) int {
132+
num, _ := strconv.Atoi(s)
133+
return num
134+
}

internal/cluset/cluset_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package cluset
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestCompactExpand(t *testing.T) {
10+
testCases := []struct {
11+
name string
12+
expanded, compacted []string
13+
}{
14+
{
15+
name: "Case 1: empty list",
16+
},
17+
{
18+
name: "Case 2: ranges",
19+
expanded: []string{"abc0507", "abc0509", "abc0482", "abc0483", "abc0508", "abc0484"},
20+
compacted: []string{"abc[0482-0484,0507-0509]"},
21+
},
22+
{
23+
name: "Case 3: singles",
24+
expanded: []string{"abc0507", "abc0509", "xyz0482"},
25+
compacted: []string{"abc[0507,0509]", "xyz0482"},
26+
},
27+
{
28+
name: "Case 4: mix1",
29+
expanded: []string{"abc0507", "abc0509", "def", "abc0482", "abc0508"},
30+
compacted: []string{"abc[0482,0507-0509]", "def"},
31+
},
32+
{
33+
name: "Case 5: mix2",
34+
expanded: []string{"abc0507", "abc0509", "abc0508", "abc0482", "xyz8", "xyz9", "xyz10"},
35+
compacted: []string{"abc[0482,0507-0509]", "xyz[8-10]"},
36+
},
37+
}
38+
39+
for _, tc := range testCases {
40+
t.Run(tc.name, func(t *testing.T) {
41+
require.Equal(t, tc.compacted, Compact(tc.expanded))
42+
require.Equal(t, toMap(tc.expanded), toMap(Expand(tc.compacted)))
43+
})
44+
}
45+
}
46+
47+
func toMap(arr []string) map[string]struct{} {
48+
m := make(map[string]struct{})
49+
for _, str := range arr {
50+
m[str] = struct{}{}
51+
}
52+
return m
53+
}

pkg/server/http_server_test.go

+13-13
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func TestServer(t *testing.T) {
9090
}
9191
`,
9292
expected: `SwitchName=S1 Switches=S[2-3]
93-
SwitchName=S2 Nodes=Node[201-202],Node205
93+
SwitchName=S2 Nodes=Node[201-202,205]
9494
SwitchName=S3 Nodes=Node[304-306]
9595
`,
9696
},
@@ -155,29 +155,29 @@ SwitchName=sw14 Nodes=n14-[1-2]
155155
}
156156
`,
157157
expected: `# block001=nvl-1-1
158-
BlockName=block001 Nodes=n1-1-0[1-8]
158+
BlockName=block001 Nodes=n1-1-[01-08]
159159
# block002=nvl-1-2
160-
BlockName=block002 Nodes=n1-2-0[1-8]
160+
BlockName=block002 Nodes=n1-2-[01-08]
161161
# block003=nvl-2-1
162-
BlockName=block003 Nodes=n2-1-0[1-8]
162+
BlockName=block003 Nodes=n2-1-[01-08]
163163
# block004=nvl-2-2
164-
BlockName=block004 Nodes=n2-2-0[1-8]
164+
BlockName=block004 Nodes=n2-2-[01-08]
165165
# block005=nvl-3-1
166-
BlockName=block005 Nodes=n3-1-0[1-8]
166+
BlockName=block005 Nodes=n3-1-[01-08]
167167
# block006=nvl-3-2
168-
BlockName=block006 Nodes=n3-2-0[1-8]
168+
BlockName=block006 Nodes=n3-2-[01-08]
169169
# block007=nvl-4-1
170-
BlockName=block007 Nodes=n4-1-0[1-8]
170+
BlockName=block007 Nodes=n4-1-[01-08]
171171
# block008=nvl-4-2
172-
BlockName=block008 Nodes=n4-2-0[1-8]
172+
BlockName=block008 Nodes=n4-2-[01-08]
173173
# block009=nvl-5-1
174-
BlockName=block009 Nodes=n5-1-0[1-8]
174+
BlockName=block009 Nodes=n5-1-[01-08]
175175
# block010=nvl-5-2
176-
BlockName=block010 Nodes=n5-2-0[1-8]
176+
BlockName=block010 Nodes=n5-2-[01-08]
177177
# block011=nvl-6-1
178-
BlockName=block011 Nodes=n6-1-0[1-8]
178+
BlockName=block011 Nodes=n6-1-[01-08]
179179
# block012=nvl-6-2
180-
BlockName=block012 Nodes=n6-2-0[1-8]
180+
BlockName=block012 Nodes=n6-2-[01-08]
181181
BlockSizes=8,16,32
182182
`,
183183
},

pkg/translate/output.go

+4-92
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"k8s.io/klog/v2"
2828

29+
"github.com/NVIDIA/topograph/internal/cluset"
2930
"github.com/NVIDIA/topograph/pkg/metrics"
3031
"github.com/NVIDIA/topograph/pkg/topology"
3132
)
@@ -54,7 +55,7 @@ func printBlock(wr io.Writer, block *topology.Vertex, domainVisited map[string]i
5455
if len(block.Name) != 0 {
5556
comment = fmt.Sprintf("# %s=%s\n", block.ID, block.Name)
5657
}
57-
_, err := wr.Write([]byte(fmt.Sprintf("%sBlockName=%s Nodes=%s\n", comment, block.ID, strings.Join(compress(nodes), ","))))
58+
_, err := wr.Write([]byte(fmt.Sprintf("%sBlockName=%s Nodes=%s\n", comment, block.ID, strings.Join(cluset.Compact(nodes), ","))))
5859
if err != nil {
5960
return err
6061
}
@@ -266,7 +267,7 @@ func toTreeTopology(wr io.Writer, root *topology.Vertex) error {
266267
comment = ""
267268
switchName = sw
268269
}
269-
_, err := wr.Write([]byte(fmt.Sprintf("%sSwitchName=%s Nodes=%s\n", comment, switchName, strings.Join(compress(nodes), ","))))
270+
_, err := wr.Write([]byte(fmt.Sprintf("%sSwitchName=%s Nodes=%s\n", comment, switchName, strings.Join(cluset.Compact(nodes), ","))))
270271
if err != nil {
271272
return err
272273
}
@@ -295,103 +296,14 @@ func writeSwitch(wr io.Writer, v *topology.Vertex) error {
295296
} else {
296297
comment = fmt.Sprintf("# %s=%s\n", v.Name, v.ID)
297298
}
298-
_, err := wr.Write([]byte(fmt.Sprintf("%sSwitchName=%s Switches=%s\n", comment, v.Name, strings.Join(compress(arr), ","))))
299+
_, err := wr.Write([]byte(fmt.Sprintf("%sSwitchName=%s Switches=%s\n", comment, v.Name, strings.Join(cluset.Compact(arr), ","))))
299300
if err != nil {
300301
return err
301302
}
302303

303304
return nil
304305
}
305306

306-
// compress finds contiguos numerical suffixes in names and presents then as ranges.
307-
// example: ["eos0507", "eos0509", "eos0508"] -> ["eos0[507-509"]
308-
func compress(input []string) []string {
309-
ret := []string{}
310-
keys := []string{}
311-
m := make(map[string][]int) // map of prefix : array of numerical suffix
312-
for _, name := range input {
313-
if prefix, suffix := split(name); len(suffix) == 0 {
314-
ret = append(ret, name)
315-
} else {
316-
num, _ := strconv.Atoi(suffix)
317-
if arr, ok := m[prefix]; !ok {
318-
m[prefix] = []int{num}
319-
} else {
320-
m[prefix] = append(arr, num)
321-
}
322-
}
323-
}
324-
325-
// we sort the prefix to get consistent output for tests
326-
for prefix := range m {
327-
keys = append(keys, prefix)
328-
}
329-
sort.Strings(keys)
330-
331-
for _, prefix := range keys {
332-
arr := m[prefix]
333-
sort.Ints(arr)
334-
var start, end int
335-
for i, num := range arr {
336-
if i == 0 {
337-
start = num
338-
end = num
339-
} else if num == end+1 {
340-
end = num
341-
} else if start == end {
342-
ret = append(ret, fmt.Sprintf("%s%d", prefix, start))
343-
start = num
344-
end = num
345-
} else {
346-
ret = append(ret, fmt.Sprintf("%s[%d-%d]", prefix, start, end))
347-
start = num
348-
end = num
349-
}
350-
}
351-
if start == end {
352-
ret = append(ret, fmt.Sprintf("%s%d", prefix, end))
353-
} else {
354-
ret = append(ret, fmt.Sprintf("%s[%d-%d]", prefix, start, end))
355-
}
356-
}
357-
358-
return ret
359-
}
360-
361-
// split divides a string into a prefix and a numerical suffix
362-
func split(input string) (string, string) {
363-
n := len(input)
364-
if n == 0 {
365-
return input, ""
366-
}
367-
368-
// find numerical suffix
369-
i := n - 1
370-
for i >= 0 {
371-
if input[i] >= '0' && input[i] <= '9' {
372-
i--
373-
} else {
374-
break
375-
}
376-
}
377-
i++
378-
379-
// ignore leading zeros
380-
for i < n {
381-
if input[i] == '0' {
382-
i++
383-
} else {
384-
break
385-
}
386-
}
387-
388-
if i == n { // no suffix
389-
return input, ""
390-
}
391-
392-
return input[:i], input[i:]
393-
}
394-
395307
func GetTreeTestSet(testForLongLabelName bool) (*topology.Vertex, map[string]string) {
396308
//
397309
// S1

0 commit comments

Comments
 (0)