java-topology/defects/kubeflow/unit/KubeflowTaskDagAlgorithm.java

261 lines
11 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.*;
/**
* kubeflow-0001 — O(T²×I) pipeline DAG compilation in pipeline_spec_builder.py
*
* Simulates the defective pattern from:
* sdk/python/kfp/compiler/pipeline_spec_builder.py lines 13831540
*
* Defect:
* tasks_in_current_dag is a List[str] REBUILT inside the outer loop over T tasks,
* and then each task performs O(I) input lookups each doing O(T) list `in` check.
* Total: O(T) outer × O(I) inputs × O(T) in-check = O(T² × I).
*
* Fix:
* Build tasks_in_current_dag once as a Set[str] outside the loop.
* Each `in` check becomes O(1).
* Total: O(T × I).
*
* Op-count measures: number of string comparisons performed across all `in` checks.
*/
public class KubeflowTaskDagAlgorithm {
static void check(String desc, boolean cond) {
System.out.println((cond ? "PASS" : "FAIL") + ": " + desc);
if (!cond) throw new AssertionError("FAIL: " + desc);
}
// Simulates a PipelineTask with input channels that may reference other tasks
static class Task {
final String name;
final List<String> inputProducers; // task names that produce each input (may be null)
Task(String name, List<String> inputProducers) {
this.name = name;
this.inputProducers = inputProducers;
}
}
// Simulates build result: which inputs are "task outputs" vs "component inputs"
static class TaskSpec {
final Map<String, String> taskOutputInputs = new LinkedHashMap<>(); // input → producer task
final Map<String, String> componentInputs = new LinkedHashMap<>(); // input → component input
}
// Result carrying op-count
static class Result {
final List<TaskSpec> taskSpecs;
final long inChecks; // total string comparisons
Result(List<TaskSpec> specs, long checks) {
this.taskSpecs = specs;
this.inChecks = checks;
}
}
// -----------------------------------------------------------------------
// DEFECTIVE: List[str] rebuilt inside outer loop, O(T) in-check per input
// -----------------------------------------------------------------------
static long defectiveChecks;
static boolean listContains(List<String> list, String item) {
for (String s : list) {
defectiveChecks++;
if (s.equals(item)) return true;
}
return false;
}
static TaskSpec buildTaskSpecDefective(Task task, List<String> tasksInCurrentDag) {
TaskSpec spec = new TaskSpec();
for (String producer : task.inputProducers) {
if (producer == null) {
spec.componentInputs.put(task.name + "_null", "pipeline_input");
} else if (listContains(tasksInCurrentDag, producer)) {
spec.taskOutputInputs.put(task.name + "_from_" + producer, producer);
} else {
spec.componentInputs.put(task.name + "_from_" + producer, "outer_" + producer);
}
}
return spec;
}
static Result runDefective(List<Task> tasks) {
defectiveChecks = 0;
List<TaskSpec> specs = new ArrayList<>();
for (Task task : tasks) {
// DEFECTIVE: rebuilt on every iteration of the outer loop
List<String> tasksInCurrentDag = new ArrayList<>();
for (Task t : tasks) {
tasksInCurrentDag.add(t.name);
}
specs.add(buildTaskSpecDefective(task, tasksInCurrentDag));
}
return new Result(specs, defectiveChecks);
}
// -----------------------------------------------------------------------
// FIXED: Set[str] built once outside the loop, O(1) in-check per input
// -----------------------------------------------------------------------
static long fixedChecks;
static boolean setContains(Set<String> set, String item) {
fixedChecks++; // O(1) hash lookup counted as 1
return set.contains(item);
}
static TaskSpec buildTaskSpecFixed(Task task, Set<String> tasksInCurrentDag) {
TaskSpec spec = new TaskSpec();
for (String producer : task.inputProducers) {
if (producer == null) {
spec.componentInputs.put(task.name + "_null", "pipeline_input");
} else if (setContains(tasksInCurrentDag, producer)) {
spec.taskOutputInputs.put(task.name + "_from_" + producer, producer);
} else {
spec.componentInputs.put(task.name + "_from_" + producer, "outer_" + producer);
}
}
return spec;
}
static Result runFixed(List<Task> tasks) {
fixedChecks = 0;
List<TaskSpec> specs = new ArrayList<>();
// FIXED: built once outside the loop
Set<String> tasksInCurrentDag = new LinkedHashSet<>();
for (Task t : tasks) {
tasksInCurrentDag.add(t.name);
}
for (Task task : tasks) {
specs.add(buildTaskSpecFixed(task, tasksInCurrentDag));
}
return new Result(specs, fixedChecks);
}
// -----------------------------------------------------------------------
// Build test pipeline: T tasks, each with I inputs referencing earlier tasks
// -----------------------------------------------------------------------
static List<Task> buildPipeline(int T, int I) {
List<Task> tasks = new ArrayList<>();
for (int t = 0; t < T; t++) {
List<String> inputs = new ArrayList<>();
for (int i = 0; i < I; i++) {
if (t > 0) {
// reference a task within the same DAG
inputs.add("task" + (t - 1));
} else {
inputs.add(null); // pipeline-level input
}
}
tasks.add(new Task("task" + t, inputs));
}
return tasks;
}
// -----------------------------------------------------------------------
// Verify both produce identical specs
// -----------------------------------------------------------------------
static boolean specsEqual(List<TaskSpec> a, List<TaskSpec> b) {
if (a.size() != b.size()) return false;
for (int i = 0; i < a.size(); i++) {
if (!a.get(i).taskOutputInputs.equals(b.get(i).taskOutputInputs)) return false;
if (!a.get(i).componentInputs.equals(b.get(i).componentInputs)) return false;
}
return true;
}
public static void main(String[] args) {
System.out.println("=== kubeflow-0001: O(T²×I) tasks_in_current_dag List in pipeline compiler ===");
// ------ Test 1: correctness on small pipeline ------
{
List<Task> tasks = buildPipeline(5, 3);
Result def = runDefective(tasks);
Result fix = runFixed(tasks);
check("pipeline-5x3: same task specs (defective == fixed)", specsEqual(def.taskSpecs, fix.taskSpecs));
check("pipeline-5x3: fixed uses fewer checks", fix.inChecks <= def.inChecks);
}
// ------ Test 2: O(T²×I) vs O(T×I) scaling ------
{
int I = 5;
int T = 200;
List<Task> tasks = buildPipeline(T, I);
Result def = runDefective(tasks);
Result fix = runFixed(tasks);
long ratio = def.inChecks / Math.max(fix.inChecks, 1);
System.out.printf(" T=%d I=%d: defective checks=%d, fixed checks=%d, ratio=%dx%n",
T, I, def.inChecks, fix.inChecks, ratio);
// Defective: each of T tasks does I × O(T) checks = T² × I total
// Expected ~200² × 5 / 2 avg = ~100,000
check("T=200,I=5: defective checks > T*I*5 (quadratic evidence)",
def.inChecks > (long) T * I * 5);
// Fixed: T × I × 1 = 1000
// task0 has null producers so its I inputs are skipped before the set check;
// remaining T-1 tasks each do I checks → (T-1)*I total (bounded by T*I)
check("T=200,I=5: fixed checks <= T*I (linear)",
fix.inChecks <= (long) T * I);
check("T=200,I=5: ratio >= 10x",
ratio >= 10);
check("T=200,I=5: same specs", specsEqual(def.taskSpecs, fix.taskSpecs));
}
// ------ Test 3: quadratic growth is clear ------
{
int I = 3;
List<Task> tasks100 = buildPipeline(100, I);
List<Task> tasks400 = buildPipeline(400, I);
Result def100 = runDefective(tasks100);
Result def400 = runDefective(tasks400);
double growthRatio = (double) def400.inChecks / Math.max(def100.inChecks, 1);
System.out.printf(" Defective: T=100 checks=%d, T=400 checks=%d, growth=%.1fx%n",
def100.inChecks, def400.inChecks, growthRatio);
check("quadratic growth: def400 > 4x def100 (O(T²) evidence)", growthRatio > 4.0);
Result fix100 = runFixed(tasks100);
Result fix400 = runFixed(tasks400);
double fixGrowth = (double) fix400.inChecks / Math.max(fix100.inChecks, 1);
System.out.printf(" Fixed: T=100 checks=%d, T=400 checks=%d, growth=%.1fx%n",
fix100.inChecks, fix400.inChecks, fixGrowth);
check("linear growth: fix400 ≈ 4x fix100 (O(T) evidence)",
fixGrowth >= 3.5 && fixGrowth <= 4.5);
}
// ------ Test 4: mixed producers (some inside, some outside DAG) ------
{
// Task t2 references task t0 (inside), task "external_task" (outside)
List<String> t0inputs = Arrays.asList((String)null);
List<String> t1inputs = Arrays.asList("task0");
List<String> t2inputs = Arrays.asList("task0", "task1", "external_task");
List<Task> tasks = Arrays.asList(
new Task("task0", t0inputs),
new Task("task1", t1inputs),
new Task("task2", t2inputs)
);
Result def = runDefective(tasks);
Result fix = runFixed(tasks);
check("mixed: same specs", specsEqual(def.taskSpecs, fix.taskSpecs));
// task2's "external_task" ref should be in componentInputs (not in DAG)
TaskSpec t2defSpec = def.taskSpecs.get(2);
check("mixed: external_task classified as component_input (defective)",
t2defSpec.componentInputs.containsKey("task2_from_external_task"));
check("mixed: external_task classified as component_input (fixed)",
fix.taskSpecs.get(2).componentInputs.containsKey("task2_from_external_task"));
}
System.out.println("All tests PASS.");
}
}