Skip to content

Commit c85051c

Browse files
committed
fix: bug in visualization construction
1 parent 5d5c580 commit c85051c

File tree

3 files changed

+131
-11
lines changed

3 files changed

+131
-11
lines changed

zhc_ir/src/visualization/placement/annotation.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,17 @@ impl PlacementSolution {
199199
}
200200
}
201201

202+
/// Convert raw layer positions to ranks (0..n-1) based on sorted order.
203+
fn positions_to_ranks(positions: SmallVec<Place>) -> SmallVec<Place> {
204+
let mut indexed = positions.iter().enumerate().map(|(i, p)| (i, *p)).covec();
205+
indexed.sort_by_key(|(_, pos)| *pos);
206+
let mut ranks = vec![Place(0.0); positions.len()];
207+
for (rank, (orig_idx, _)) in indexed.iter().enumerate() {
208+
ranks[*orig_idx] = Place(rank as f64);
209+
}
210+
ranks.into_iter().collect()
211+
}
212+
202213
fn resolve(input: OpMap<PlacementVariable>) -> OpMap<PlacementSolution> {
203214
input.map(|v| match v {
204215
PlacementVariable::NonGroup { op, .. } => PlacementSolution::NonGroup { op: op.get_val() },
@@ -208,12 +219,18 @@ fn resolve(input: OpMap<PlacementVariable>) -> OpMap<PlacementSolution> {
208219
inputs,
209220
outputs,
210221
..
211-
} => PlacementSolution::Group {
212-
op: op.get_val(),
213-
inputs: inputs.into_iter().map(|a| a.get_val()).collect(),
214-
outputs: outputs.into_iter().map(|a| a.get_val()).collect(),
215-
maps: (resolve(maps.0), maps.1.map(|_| ())),
216-
},
222+
} => {
223+
// Convert raw layer positions to ranks, since other ops on the same
224+
// layer can create gaps in the position sequence.
225+
let input_positions = inputs.into_iter().map(|a| a.get_val()).cosvec();
226+
let output_positions = outputs.into_iter().map(|a| a.get_val()).cosvec();
227+
PlacementSolution::Group {
228+
op: op.get_val(),
229+
inputs: positions_to_ranks(input_positions),
230+
outputs: positions_to_ranks(output_positions),
231+
maps: (resolve(maps.0), maps.1.map(|_| ())),
232+
}
233+
}
217234
})
218235
}
219236

zhc_ir/src/visualization/placement/solver.rs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,25 @@ fn place_once_top_down<'ir, 'ann>(ir: AnnIRView<'ir, 'ann, LayoutDialect, Placem
9494

9595
// Last, for the group operations of the layer, we reconcile the rets positions with the
9696
// constraints coming from inside of the group. That is, we reorder the rets
97-
// position following the order of the group outputs
97+
// position following the order of the group outputs.
98+
// Note: we use ranks (relative order among outputs) rather than raw layer positions,
99+
// since other ops on the same layer can create gaps in the position sequence.
98100
for op in layer_ops.iter_mut() {
99101
if let PlacementVariable::Group { outputs, rets, .. } = op.get_annotation() {
100102
let rets_original_places = rets.iter().map(|r| r.get_val()).cosvec();
101-
for (ret, val) in (rets.iter(), outputs.iter()).mzip() {
102-
ret.set_val(rets_original_places[val.get_val().0 as usize]);
103+
// Compute rank of each output based on sorted position order
104+
let mut indexed = outputs
105+
.iter()
106+
.enumerate()
107+
.map(|(i, o)| (i, o.get_val()))
108+
.cosvec();
109+
indexed.sort_by_key(|(_, pos)| *pos);
110+
let mut ranks = vec![0usize; outputs.len()];
111+
for (rank, (orig_idx, _)) in indexed.iter().enumerate() {
112+
ranks[*orig_idx] = rank;
113+
}
114+
for (ret, rank) in (rets.iter(), ranks.iter()).mzip() {
115+
ret.set_val(rets_original_places[*rank]);
103116
}
104117
}
105118
}
@@ -198,11 +211,24 @@ fn place_once_bottom_up<'ir, 'ann>(ir: AnnIRView<'ir, 'ann, LayoutDialect, Place
198211
// Last, for the group operations of the layer, we reconcile the args positions with the
199212
// constraints coming from inside of the group. That is, we reorder the args
200213
// position following the order of the group inputs.
214+
// Note: we use ranks (relative order among inputs) rather than raw layer positions,
215+
// since other ops on the same layer can create gaps in the position sequence.
201216
for op in layer_ops.iter_mut() {
202217
if let PlacementVariable::Group { inputs, args, .. } = op.get_annotation() {
203218
let args_original_places = args.iter().map(|r| r.get_val()).cosvec();
204-
for (arg, val) in (args.iter(), inputs.iter()).mzip() {
205-
arg.set_val(args_original_places[val.get_val().0 as usize]);
219+
// Compute rank of each input based on sorted position order
220+
let mut indexed = inputs
221+
.iter()
222+
.enumerate()
223+
.map(|(i, o)| (i, o.get_val()))
224+
.cosvec();
225+
indexed.sort_by_key(|(_, pos)| *pos);
226+
let mut ranks = vec![0usize; inputs.len()];
227+
for (rank, (orig_idx, _)) in indexed.iter().enumerate() {
228+
ranks[*orig_idx] = rank;
229+
}
230+
for (arg, rank) in (args.iter(), ranks.iter()).mzip() {
231+
arg.set_val(args_original_places[*rank]);
206232
}
207233
}
208234
}

zhc_ir/src/visualization/test.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,3 +1076,80 @@ fn test_cross_group_reorder() {
10761076
op_annotations.insert(r, root.clone());
10771077
draw_ir_html(&ir, op_annotations, "test33.html");
10781078
}
1079+
1080+
/// Bug reproducer: GroupInput shares layer 1 with an IntInput inside the group.
1081+
/// This triggers the indexing bug in place_once_bottom_up (line 203) where
1082+
/// the layer position is used as an index into args_original_places.
1083+
///
1084+
/// Forces crossing: two external inputs A, B go into the group. An internal source C
1085+
/// also exists on layer 1. The median heuristic will reorder based on downstream users,
1086+
/// potentially placing C between the GroupInputs.
1087+
#[test]
1088+
fn test_mixed_first_layer_in_group() {
1089+
// Structure:
1090+
// A, B (root) -> [group: gi_A, gi_B, internal_C (all layer 1) -> use them -> outputs] -> ret
1091+
// With careful wiring, reordering can place internal_C at position 1, pushing gi_B to position
1092+
// 2. args_original_places has 2 elements, so index 2 is out of bounds.
1093+
let mut ir: IR<TestLang> = IR::empty();
1094+
let (op_a, a) = ir.add_op(TestInstructionSet::IntInput { pos: 0 }, svec![]);
1095+
let (op_b, b) = ir.add_op(TestInstructionSet::IntInput { pos: 1 }, svec![]);
1096+
// Inside group_a:
1097+
let (op_c, c) = ir.add_op(TestInstructionSet::IntInput { pos: 2 }, svec![]); // internal source
1098+
// Wire so that C is used first (lower position), then B, then A - forces reorder
1099+
let (op_add1, add1) = ir.add_op(TestInstructionSet::Add, svec![c[0], b[0]]); // uses C and B
1100+
let (op_add2, add2) = ir.add_op(TestInstructionSet::Add, svec![add1[0], a[0]]); // uses result and A
1101+
// Back at root:
1102+
let (op_ret, _) = ir.add_op(TestInstructionSet::Return, svec![add2[0]]);
1103+
1104+
let root = Hierarchy::new();
1105+
let mut group_a = root.clone();
1106+
group_a.push("group_a");
1107+
1108+
let mut op_annotations = ir.empty_opmap();
1109+
op_annotations.insert(op_a, root.clone());
1110+
op_annotations.insert(op_b, root.clone());
1111+
op_annotations.insert(op_c, group_a.clone()); // internal source, layer 1
1112+
op_annotations.insert(op_add1, group_a.clone());
1113+
op_annotations.insert(op_add2, group_a.clone());
1114+
op_annotations.insert(op_ret, root.clone());
1115+
draw_ir_html(&ir, op_annotations, "test34.html");
1116+
}
1117+
1118+
/// Bug reproducer: GroupOutput shares last layer with a Return inside the group.
1119+
/// This triggers the indexing bug in place_once_top_down (line 100) where
1120+
/// the layer position is used as an index into rets_original_places.
1121+
///
1122+
/// Strategy: Single GroupOutput + Return on last layer. If Return ends up at
1123+
/// position 0 and GroupOutput at position 1, indexing with 1 into a 1-element
1124+
/// rets_original_places array causes out of bounds.
1125+
/// We use two inputs where the one feeding the internal Return has lower position.
1126+
#[test]
1127+
fn test_mixed_last_layer_in_group() {
1128+
let mut ir: IR<TestLang> = IR::empty();
1129+
// inp0 at position 0, inp1 at position 1
1130+
let (op_inp0, inp0) = ir.add_op(TestInstructionSet::IntInput { pos: 0 }, svec![]);
1131+
let (op_inp1, inp1) = ir.add_op(TestInstructionSet::IntInput { pos: 1 }, svec![]);
1132+
// Inside group_a:
1133+
// inc1 from inp0 (position 0) -> feeds internal Return (will want position 0 on last layer)
1134+
let (op_inc1, inc1) = ir.add_op(TestInstructionSet::Inc, svec![inp0[0]]);
1135+
// inc2 from inp1 (position 1) -> exits group (will want position 1 on last layer)
1136+
let (op_inc2, inc2) = ir.add_op(TestInstructionSet::Inc, svec![inp1[0]]);
1137+
// Internal Return fed by inc1 - should get position 0 on last layer
1138+
let (op_internal_ret, _) = ir.add_op(TestInstructionSet::Return, svec![inc1[0]]);
1139+
// inc2 exits the group - GroupOutput should get position 1 on last layer
1140+
// But rets_original_places has only 1 element!
1141+
let (op_ret, _) = ir.add_op(TestInstructionSet::Return, svec![inc2[0]]);
1142+
1143+
let root = Hierarchy::new();
1144+
let mut group_a = root.clone();
1145+
group_a.push("group_a");
1146+
1147+
let mut op_annotations = ir.empty_opmap();
1148+
op_annotations.insert(op_inp0, root.clone());
1149+
op_annotations.insert(op_inp1, root.clone());
1150+
op_annotations.insert(op_inc1, group_a.clone());
1151+
op_annotations.insert(op_inc2, group_a.clone());
1152+
op_annotations.insert(op_internal_ret, group_a.clone());
1153+
op_annotations.insert(op_ret, root.clone());
1154+
draw_ir_html(&ir, op_annotations, "test35.html");
1155+
}

0 commit comments

Comments
 (0)