java-topology/defects/pytorch/patch/pytorch-0002-graph-fuser-mergenode-linear-search.md

3.7 KiB
Raw Permalink Blame History

UNDF: UNDF-2026-000000513

pytorch-0002 — graph_fuser.cpp mergeNodeIntoGroup + tryToMoveChunk O(N²)

Location

torch/csrc/jit/passes/graph_fuser.cpp

  • mergeNodeIntoGroup: lines 382391
  • tryToMoveChunk: lines 762780

Defective Code — mergeNodeIntoGroup

// line 382
auto inputs = group->inputs();
for (size_t i = 0; i < n->outputs().size(); ++i) {   // O(O) outer loop
    auto it = std::find(inputs.begin(), inputs.end(), n->outputs()[i]);  // O(I) each
    if (it != inputs.end()) {
        size_t p = it - inputs.begin();
        group->removeInput(p);
        subgraph.inputs()[p]->replaceAllUsesWith(in_graph->outputs()[i]);
        subgraph.eraseInput(p);
    }
}

Defective Code — tryToMoveChunk

// line 762
for (auto input : producer_for_chunk_node->inputs()) {       // O(I) outer loop
    if (!input->type()->isSubtypeOf(*TensorType::get()))
        continue;
    auto bchunk_inputs = bchunk->inputs();
    auto it = std::find(bchunk_inputs.begin(), bchunk_inputs.end(), input);  // O(B) each
    if (it != bchunk_inputs.end()) {
        chunked_inputs.emplace_back();
        auto input_index = std::distance(bchunk_inputs.begin(), it);
        for (const auto chunki : c10::irange(nchunks)) {
            chunked_inputs.back().push_back(
                bchunk->outputs().at(nchunks * input_index + chunki));
        }
        continue;
    }
    bchunk->addInput(input);
    // ...
}

In mergeNodeIntoGroup: O(O × I) where O = outputs of merged node, I = fusion group inputs. In tryToMoveChunk: O(I × B) where I = producer inputs, B = broadcast chunk inputs.

Fixed Code — mergeNodeIntoGroup

// Build reverse index once: O(I)
std::unordered_map<Value*, size_t> input_pos;
const auto inputs_vec = group->inputs().vec();
for (size_t k = 0; k < inputs_vec.size(); ++k) {
    input_pos[inputs_vec[k]] = k;
}
// Erase in reverse to preserve indices
std::vector<size_t> to_erase;
for (size_t i = 0; i < n->outputs().size(); ++i) {
    auto mit = input_pos.find(n->outputs()[i]);       // O(1)
    if (mit != input_pos.end()) {
        subgraph.inputs()[mit->second]->replaceAllUsesWith(in_graph->outputs()[i]);
        to_erase.push_back(mit->second);
    }
}
std::sort(to_erase.rbegin(), to_erase.rend());
for (size_t p : to_erase) {
    group->removeInput(p);
    subgraph.eraseInput(p);
}

Fixed Code — tryToMoveChunk

// Build reverse index once: O(B)
std::unordered_map<Value*, size_t> bchunk_input_pos;
const auto bchunk_inputs_vec = bchunk->inputs().vec();
for (size_t k = 0; k < bchunk_inputs_vec.size(); ++k) {
    bchunk_input_pos[bchunk_inputs_vec[k]] = k;
}

for (auto input : producer_for_chunk_node->inputs()) {
    if (!input->type()->isSubtypeOf(*TensorType::get()))
        continue;
    auto mit = bchunk_input_pos.find(input);           // O(1)
    if (mit != bchunk_input_pos.end()) {
        chunked_inputs.emplace_back();
        auto input_index = mit->second;
        for (const auto chunki : c10::irange(nchunks)) {
            chunked_inputs.back().push_back(
                bchunk->outputs().at(nchunks * input_index + chunki));
        }
        continue;
    }
    bchunk->addInput(input);
    // ...
}

Complexity Analysis

Path Complexity
Slow mergeNodeIntoGroup O(O × I)
Fast mergeNodeIntoGroup O(O + I)
Slow tryToMoveChunk O(I × B)
Fast tryToMoveChunk O(I + B)

At O=16, I=128: 128× speedup in merge. At I=32, B=64: 64× speedup in chunk move.

Severity

HIGH — both functions sit on the hot path of the JIT kernel fusion pass (torch.compile, torch.jit.script). mergeNodeIntoGroup is called once per node during fusion graph construction. tryToMoveChunk is called in the node scan loop for every FusionGroup consumer.