From 6a1a74b2406e5a8c533444378a3998274212c6be Mon Sep 17 00:00:00 2001 From: imbajin Date: Thu, 5 Nov 2026 23:04:27 +0800 Subject: [PATCH 1/2] perf(vermeer): compact triangle adjacency exchange --- vermeer/algorithms/triangle_count.go | 146 +++++++++++++++------------ 1 file changed, 80 insertions(+), 66 deletions(-) diff --git a/vermeer/algorithms/triangle_count.go b/vermeer/algorithms/triangle_count.go index 3dcc91f6c..a465a6982 100644 --- a/vermeer/algorithms/triangle_count.go +++ b/vermeer/algorithms/triangle_count.go @@ -47,96 +47,110 @@ func (tc *TriangleCountMaker) DataNeeded() []string { return []string{compute.UseOutEdge} } +// buildTriangleNeighborState treats incoming and outgoing edges as one +// undirected neighborhood, removes self-loops and duplicate parallel edges, +// and returns only the lower-ID half for cross-worker exchange. For every +// triangle incident to vertex v, exactly one of the other two vertices has the +// larger ID, so intersecting N(v) with each neighbor's lower-ID half counts the +// triangle once for v. +func buildTriangleNeighborState(vertexID uint32, inEdges, + outEdges serialize.SliceUint32) (map[serialize.SUint32]struct{}, serialize.SliceUint32) { + neighbors := make(map[serialize.SUint32]struct{}, len(inEdges)+len(outEdges)) + lower := make(serialize.SliceUint32, 0, (len(inEdges)+len(outEdges))/2) + add := func(neighbor serialize.SUint32) { + if uint32(neighbor) == vertexID { + return + } + if _, exists := neighbors[neighbor]; exists { + return + } + neighbors[neighbor] = struct{}{} + if uint32(neighbor) < vertexID { + lower = append(lower, neighbor) + } + } + for _, neighbor := range inEdges { + add(neighbor) + } + for _, neighbor := range outEdges { + add(neighbor) + } + return neighbors, lower +} + +func countTriangleMembership(neighbors map[serialize.SUint32]struct{}, + lowerNeighbors serialize.SliceUint32) serialize.SUint32 { + var count serialize.SUint32 + for _, candidate := range lowerNeighbors { + if _, exists := neighbors[candidate]; exists { + count++ + } + } + return count +} + type TriangleCountWorker struct { compute.WorkerComputerBase - triangleCount []serialize.SUint32 - totalNeighborSet []serialize.MapUint32Struct - selfNeighborSet []map[serialize.SUint32]struct{} + triangleCount []serialize.SUint32 + lowerNeighbors []serialize.SliceUint32 + localNeighbors []map[serialize.SUint32]struct{} } -func (tcw *TriangleCountWorker) VertexValue(i uint32) serialize.MarshalAble { +func (tcw *TriangleCountWorker) VertexValue(vertexID uint32) serialize.MarshalAble { + if tcw.WContext.Output { + return &tcw.triangleCount[vertexID] + } if tcw.WContext.Step == 1 { - return &tcw.totalNeighborSet[i] + return &tcw.lowerNeighbors[vertexID] } - return &tcw.triangleCount[i] + return &tcw.triangleCount[vertexID] } func (tcw *TriangleCountWorker) Init() error { - tcw.triangleCount = make([]serialize.SUint32, tcw.WContext.GraphData.Vertex.TotalVertexCount()) - tcw.totalNeighborSet = make([]serialize.MapUint32Struct, tcw.WContext.GraphData.Vertex.TotalVertexCount()) - tcw.selfNeighborSet = make([]map[serialize.SUint32]struct{}, tcw.WContext.GraphData.VertexCount) + totalVertices := tcw.WContext.GraphData.Vertex.TotalVertexCount() + localVertices := tcw.WContext.GraphData.VertexCount + tcw.triangleCount = make([]serialize.SUint32, totalVertices) + // Slice headers are substantially cheaper than one Go map per global + // vertex. Only local vertices retain hash sets for membership tests. + tcw.lowerNeighbors = make([]serialize.SliceUint32, totalVertices) + tcw.localNeighbors = make([]map[serialize.SUint32]struct{}, localVertices) return nil } func (tcw *TriangleCountWorker) Compute(vertexID uint32, pID int) { _ = pID - vertID := vertexID - tcw.WContext.GraphData.VertIDStart + localID := vertexID - tcw.WContext.GraphData.VertIDStart switch tcw.WContext.Step { case 1: - //if tcw.WContext.GraphData.BothEdges != nil { - // tcw.selfNeighborSet[vertID] = make(map[serialize.SUint32]struct{}, - // len(tcw.WContext.GraphData.BothEdges[vertID])) - // tcw.totalNeighborSet[vertexID] = make(map[serialize.SUint32]struct{}, - // len(tcw.WContext.GraphData.BothEdges[vertID])/2) - // for _, nID := range tcw.WContext.GraphData.BothEdges[vertID] { - // if uint32(nID) == vertexID { - // continue - // } - // tcw.selfNeighborSet[vertID][nID] = struct{}{} - // if uint32(nID) < vertexID { - // tcw.totalNeighborSet[vertexID][nID] = struct{}{} - // } - // } - //} else { - inEdges := tcw.WContext.GraphData.Edges.GetInEdges(vertID) - outEdges := tcw.WContext.GraphData.Edges.GetOutEdges(vertID) - tcw.selfNeighborSet[vertID] = make(map[serialize.SUint32]struct{}, - len(inEdges)+len(outEdges)) - tcw.totalNeighborSet[vertexID] = make(map[serialize.SUint32]struct{}, - (len(inEdges)+len(outEdges))/2) - for _, nID := range inEdges { - if uint32(nID) == vertexID { - continue - } - tcw.selfNeighborSet[vertID][nID] = struct{}{} - if uint32(nID) < vertexID { - tcw.totalNeighborSet[vertexID][nID] = struct{}{} - } - } - for _, nID := range outEdges { - if uint32(nID) == vertexID { + neighbors, lower := buildTriangleNeighborState(vertexID, + tcw.WContext.GraphData.Edges.GetInEdges(localID), + tcw.WContext.GraphData.Edges.GetOutEdges(localID)) + tcw.localNeighbors[localID] = neighbors + tcw.lowerNeighbors[vertexID] = lower + case 2: + neighbors := tcw.localNeighbors[localID] + var count serialize.SUint32 + for neighborID := range neighbors { + if int(neighborID) >= len(tcw.lowerNeighbors) { continue } - tcw.selfNeighborSet[vertID][nID] = struct{}{} - if uint32(nID) < vertexID { - tcw.totalNeighborSet[vertexID][nID] = struct{}{} - } - } - //} - case 2: - for fID := range tcw.selfNeighborSet[vertID] { - tcw.triangleCount[vertexID] += tcw.findCross(tcw.selfNeighborSet[vertID], tcw.totalNeighborSet[fID]) + count += countTriangleMembership(neighbors, + tcw.lowerNeighbors[neighborID]) } + tcw.triangleCount[vertexID] = count } } -func (tcw *TriangleCountWorker) findCross(a map[serialize.SUint32]struct{}, b map[serialize.SUint32]struct{}) serialize.SUint32 { - var count serialize.SUint32 = 0 - if len(a) > len(b) { - for k := range b { - if _, ok := a[k]; ok { - count++ - } - } - } else { - for k := range a { - if _, ok := b[k]; ok { - count++ - } - } +func (tcw *TriangleCountWorker) AfterStep() { + if tcw.WContext.Step != 2 { + return } - return count + // Output needs only the final count. Release adjacency workspace before the + // writer starts so a large graph is not held twice during result export. + tcw.lowerNeighbors = nil + tcw.localNeighbors = nil } + func (tcw *TriangleCountWorker) OutputValueType() string { return common.HgValueTypeInt } From 794c3f25619d4131dd6eb6a7b33166b866d1a110 Mon Sep 17 00:00:00 2001 From: imbajin Date: Thu, 5 Nov 2026 23:04:53 +0800 Subject: [PATCH 2/2] test(vermeer): cover compact triangle intersections --- vermeer/algorithms/triangle_count_test.go | 157 ++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 vermeer/algorithms/triangle_count_test.go diff --git a/vermeer/algorithms/triangle_count_test.go b/vermeer/algorithms/triangle_count_test.go new file mode 100644 index 000000000..e5ca885ac --- /dev/null +++ b/vermeer/algorithms/triangle_count_test.go @@ -0,0 +1,157 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with this +work for additional information regarding copyright ownership. The ASF +licenses this file to You under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +*/ + +package algorithms + +import ( + "testing" + "vermeer/apps/serialize" +) + +func TestBuildTriangleNeighborStateDeduplicatesAndDropsSelfLoops(t *testing.T) { + neighbors, lower := buildTriangleNeighborState(3, + serialize.SliceUint32{1, 2, 3, 2}, + serialize.SliceUint32{1, 4, 3, 4}) + if len(neighbors) != 3 { + t.Fatalf("expected neighbors {1,2,4}, got %v", neighbors) + } + for _, expected := range []serialize.SUint32{1, 2, 4} { + if _, exists := neighbors[expected]; !exists { + t.Fatalf("missing neighbor %d in %v", expected, neighbors) + } + } + if len(lower) != 2 || lower[0] != 1 || lower[1] != 2 { + t.Fatalf("expected lower neighbors [1 2], got %v", lower) + } +} + +func TestTriangleCountReturnsOneForEveryTriangleVertex(t *testing.T) { + // Every undirected edge is intentionally present in both directions. The + // compact state must deduplicate it before counting. + inEdges := []serialize.SliceUint32{ + {1, 2}, + {0, 2}, + {0, 1}, + } + outEdges := []serialize.SliceUint32{ + {1, 2}, + {0, 2}, + {0, 1}, + } + counts := triangleCountsForTest(inEdges, outEdges) + for vertexID, count := range counts { + if count != 1 { + t.Fatalf("vertex %d expected one incident triangle, got %d", vertexID, count) + } + } +} + +func TestTriangleCountDoesNotCountOpenWedge(t *testing.T) { + inEdges := []serialize.SliceUint32{ + {1}, + {0, 2}, + {1}, + } + outEdges := []serialize.SliceUint32{ + {1}, + {0, 2}, + {1}, + } + counts := triangleCountsForTest(inEdges, outEdges) + for vertexID, count := range counts { + if count != 0 { + t.Fatalf("open wedge vertex %d reported %d triangles", vertexID, count) + } + } +} + +func TestTriangleCountHandlesMixedEdgeDirections(t *testing.T) { + // The same undirected triangle is split across incoming and outgoing + // storage, matching graphs that do not materialize reciprocal edges. + inEdges := []serialize.SliceUint32{ + {2}, + {0}, + {1}, + } + outEdges := []serialize.SliceUint32{ + {1}, + {2}, + {0}, + } + counts := triangleCountsForTest(inEdges, outEdges) + for vertexID, count := range counts { + if count != 1 { + t.Fatalf("mixed-direction vertex %d expected one triangle, got %d", vertexID, count) + } + } +} + +func TestCountTriangleMembershipScansCompactLowerSlice(t *testing.T) { + neighbors := map[serialize.SUint32]struct{}{ + 1: {}, + 3: {}, + 5: {}, + 7: {}, + } + lower := serialize.SliceUint32{0, 1, 2, 3, 4, 5, 6} + if got := countTriangleMembership(neighbors, lower); got != 3 { + t.Fatalf("expected three shared neighbors, got %d", got) + } +} + +func TestTriangleCountIgnoresInvalidRemoteNeighborList(t *testing.T) { + neighbors := []map[serialize.SUint32]struct{}{ + {1: {}, 99: {}}, + {0: {}}, + } + lower := []serialize.SliceUint32{ + {}, + {0}, + } + var count serialize.SUint32 + for neighborID := range neighbors[0] { + if int(neighborID) >= len(lower) { + continue + } + count += countTriangleMembership(neighbors[0], lower[neighborID]) + } + if count != 0 { + t.Fatalf("invalid remote ID changed triangle count: %d", count) + } +} + +func triangleCountsForTest(inEdges, outEdges []serialize.SliceUint32) []serialize.SUint32 { + total := len(inEdges) + neighbors := make([]map[serialize.SUint32]struct{}, total) + lower := make([]serialize.SliceUint32, total) + for vertexID := 0; vertexID < total; vertexID++ { + neighbors[vertexID], lower[vertexID] = buildTriangleNeighborState( + uint32(vertexID), inEdges[vertexID], outEdges[vertexID]) + } + + counts := make([]serialize.SUint32, total) + for vertexID := 0; vertexID < total; vertexID++ { + for neighborID := range neighbors[vertexID] { + if int(neighborID) >= total { + continue + } + counts[vertexID] += countTriangleMembership(neighbors[vertexID], + lower[neighborID]) + } + } + return counts +}