#38239·openvino

[Bug]: PyTorch frontend computes `sum((x-y)^p)^(1/p)` for `aten::pairwise_distance` / `aten::cdist` — the `Abs()` is missing, so every `p != 2` is wrong or NaN

Author: Hu-loverCreated Sep 17, 2026Updated Sep 18, 2026
Labelsbugsupport_request

OpenVINO Version

2026.1.0 (2026.1.0-21367-63e31528c62, releases/2026/1) and master (428b67a8eb, 2026-09-17) — src/frontends/pytorch/src/op/distance.cpp is unchanged between them.

Operating System

Ubuntu 20.04 (LTS)

Device used for inference

CPU

Framework

PyTorch

Model used

Minimal torch.nn.Module wrappers around torch.nn.functional.pairwise_distance / torch.cdist, generated by the reproducer script below, with the reference values computed by torch itself.

Issue description

torch.nn.functional.pairwise_distance(x1, x2, p=2.0, eps=1e-6, keepdim=False) is defined as ||x1 - x2 + eps||_p, i.e.

( sum_i |x1_i - x2_i + eps|^p ) ^ (1/p)

torch.cdist(x1, x2, p=2.0) is the same p-norm without eps.

The OpenVINO PyTorch frontend implements both through one shared helper:

cpp
// src/frontends/pytorch/src/op/distance.cpp:26-42
Output<Node> pairwise_distance(const NodeContext& context,
                               Output<Node> x,
                               Output<Node> y,
                               Output<Node> p,
                               Output<Node> eps,
                               bool keepdim) {
    auto one = context.mark_node(v0::Constant::create(element::f32, Shape{}, {1}));
    auto p_plus_eps = context.mark_node(std::make_shared<v1::Add>(p, eps));
    auto inv_p = context.mark_node(std::make_shared<v1::Divide>(one, p_plus_eps));
    auto minus_one = context.mark_node(v0::Constant::create(element::i32, Shape{1}, {-1}));
    align_eltwise_input_types(context, x, y, is_python_scalar_input(context, 0), is_python_scalar_input(context, 1));
    auto x_y_diff = context.mark_node(std::make_shared<v1::Subtract>(x, y));
    auto x_y_diff_in_p_power = context.mark_node(std::make_shared<v1::Power>(x_y_diff, p));
    auto summation = context.mark_node(std::make_shared<v1::ReduceSum>(x_y_diff_in_p_power, minus_one, keepdim));
    auto summation_in_inv_p = context.mark_node(std::make_shared<v1::Power>(summation, inv_p));
    return summation_in_inv_p;
}

translate_pairwise_distance (:69-89) and translate_cdist (:45-67) both call it, so both aten ops are affected. Two deviations from the torch formula live in this helper.

Primary: the Abs() is missing

Power(x_y_diff, p) is applied to the signed difference, so the summand is (x-y)^p instead of |x-y|^p:

  • for odd integer p the negative terms subtract, and the result is simply wrong;
  • for non-integer p the final Power(summation, inv_p) takes a fractional power of a possibly negative summation, which yields NaN.

p is the default argument of both aten ops (p=2.), and for p=2 the two formulas coincide (|d|^2 == d^2), which is why this is not visible unless p is set explicitly. Fixing it is one node: an Abs between :37 and :38, plus #include "openvino/op/abs.hpp" (compare src/frontends/pytorch/src/op/norm.cpp:7,51, which does exactly this).

Secondary, at the same site: eps is applied to the exponent instead of to the difference

p_plus_eps = p + eps and inv_p = 1/(p + eps), so the result is raised to 1/(p + eps) and the + eps that torch adds to the difference is never applied at all. For the default eps = 1e-6 this is a small relative error, but it is observable even for p = 2, and for a user-specified eps the exponent is arbitrarily wrong — torch's own pairwise_distance(..., p=1.0, eps=1.0) returns 8.0, while adding eps to the exponent would return 2.6457.

torch.cdist has no eps parameter at all, yet translate_cdist passes a 1e-06 constant (:64) into this helper, so the same 1/(p + 1e-6) exponent is applied to cdist too.

Measured evidence

x = [[1, 2, 3]], y = [[3, -1, 1]], so x - y = [-2, 3, 2] and

quantity value
sum(x-y) 3
`sum x-y
sum(x-y)^3 -8 + 27 + 8 = 2727^(1/3) = 3.0
`sum x-y
call torch OpenVINO 2026.1.0 verdict
pairwise_distance(p=2.0) (default) 4.123106 4.123103 wrong in the 7th digit — the eps/exponent deviation
pairwise_distance(p=1.0) 7.000001 2.999997 WRONG — returns sum(x-y), i.e. the negative term cancelled
pairwise_distance(p=3.0) 3.503399 2.999999 WRONG — returns 27^(1/3), i.e. sum(x-y)^3
pairwise_distance(p=0.5) 20.797962 nan WRONG — fractional power of a negative sum
cdist(p=2.0) (default) [4.123106, 5.09902, 2.692582, 5.220153] [4.123103, 5.099016, 2.692581, 5.220149] wrong in the 7th digit
cdist(p=1.0) [7.0, 6.0, 3.5, 8.5] [2.999997, 3.999995, nan, nan] WRONG

The default-p rows match OpenVINO's output to the last digit under the 1/(p + eps) exponent, e.g. for p = 2, sum(x-y)^2 = 17 and

17^(1/2)        = 4.123105625617661     <- torch
17^(1/2.000001) = 4.123102705210686     <- OpenVINO returns 4.1231027

so the two deviations are separable and both are confirmed by measurement rather than by reading alone.

The dynamo=True/FX path is not affected on the same input (pairwise_distance(p=1.0) matches torch there), because that path decomposes the aten op differently. The default conversion path is TorchScript, which is the one that hits this helper.

Step-by-step reproduction

python
#!/usr/bin/env python3
"""PyTorch frontend: aten::pairwise_distance / aten::cdist compute sum((x-y)^p)^(1/p)
instead of sum(|x-y|^p)^(1/p) -- the Abs() before the Power() is missing.

Source (master, src/frontends/pytorch/src/op/distance.cpp:37-41):

    auto x_y_diff = context.mark_node(std::make_shared<v1::Subtract>(x, y));
    auto x_y_diff_in_p_power = context.mark_node(std::make_shared<v1::Power>(x_y_diff, p));
    auto summation = context.mark_node(std::make_shared<v1::ReduceSum>(x_y_diff_in_p_power, minus_one, keepdim));
    auto summation_in_inv_p = context.mark_node(std::make_shared<v1::Power>(summation, inv_p));

`pairwise_distance()` (distance.cpp:26-42) is the shared helper for both
`translate_pairwise_distance` (aten::pairwise_distance) and `translate_cdist` (aten::cdist).
`p` is a default argument of both aten ops -- `p=2.` -- and for p=2 the two formulas
coincide (|d|^2 == d^2), which is why this went unnoticed: only an explicit p != 2
exposes it.  For odd p the negative terms cancel, for non-integer p the negative base
of the final fractional power produces NaN.

Run:
    pip install torch onnxruntime openvino
    python repro_pytorch_pairwise_distance_cdist.py
"""
import numpy as np
import openvino as ov
import torch

X = torch.tensor([[1.0, 2.0, 3.0]])
Y = torch.tensor([[3.0, -1.0, 1.0]])
# x - y = [-2, 3, 2];  sum|x-y| = 7;  sum(x-y) = 3;  sum|x-y|^3 = 43;  sum(x-y)^3 = 27
A = torch.tensor([[1.0, 2.0, 3.0], [0.5, -1.0, 2.0]])
B = torch.tensor([[3.0, -1.0, 1.0], [2.0, 2.0, -2.0]])


def check(label, fn, lhs, rhs, p=None):
    class M(torch.nn.Module):
        def forward(self, a, b):
            if p is None:
                return fn(a, b)
            return fn(a, b, p)

    m = M().eval()
    with torch.no_grad():
        ref = m(lhs, rhs).numpy()
    ov_model = ov.convert_model(m, example_input=[lhs, rhs])
    out = np.asarray(list(ov.Core().compile_model(ov_model, "CPU")([lhs.numpy(), rhs.numpy()]).values())[0])
    ok = ref.shape == out.shape and np.allclose(ref, out, atol=1e-5, rtol=1e-4, equal_nan=True)
    print(f"  {label:34s} torch {np.array2string(ref.ravel(), precision=6):12s} "
          f"openvino {np.array2string(out.ravel(), precision=6):12s} {'ok' if ok else '*** WRONG ***'}")


print("aten::pairwise_distance(Tensor x1, Tensor x2, float p=2., float eps=1e-6, bool keepdim=False)")
print("  x = [[1,2,3]]   y = [[3,-1,1]]   x-y = [-2,3,2]")
print(f"  hand check: sum|x-y| = 7 ; sum(x-y) = 3 ; sum|x-y|**3 = 43 ; 43**(1/3) = {43 ** (1 / 3):.6f}")
check("p=2 (the default) -- masked", torch.nn.functional.pairwise_distance, X, Y)
check("p=1", torch.nn.functional.pairwise_distance, X, Y, p=1.0)
check("p=3", torch.nn.functional.pairwise_distance, X, Y, p=3.0)
check("p=0.5", torch.nn.functional.pairwise_distance, X, Y, p=0.5)

print()
print("aten::cdist(Tensor x1, Tensor x2, float p=2., int? compute_mode=None)")
print("  A = [[1,2,3],[0.5,-1,2]]   B = [[3,-1,1],[2,2,-2]]")
check("p=2 (the default) -- masked", torch.cdist, A, B)
check("p=1", torch.cdist, A, B, p=1.0)

Relevant log output

bash
$ python repro_pytorch_pairwise_distance_cdist.py     # OpenVINO 2026.1.0 (2026.1.0-21367-63e31528c62)
aten::pairwise_distance(Tensor x1, Tensor x2, float p=2., float eps=1e-6, bool keepdim=False)
  x = [[1,2,3]]   y = [[3,-1,1]]   x-y = [-2,3,2]
  hand check: sum|x-y| = 7 ; sum(x-y) = 3 ; sum|x-y|**3 = 43 ; 43**(1/3) = 3.503398
  p=2 (the default) -- masked        torch [4.123106]   openvino [4.123103]   ok
  p=1                                torch [7.000001]   openvino [2.999997]   *** WRONG ***
  p=3                                torch [3.503399]   openvino [2.999999]   *** WRONG ***
  p=0.5                              torch [20.797962]  openvino [nan]        *** WRONG ***

aten::cdist(Tensor x1, Tensor x2, float p=2., int? compute_mode=None)
  A = [[1,2,3],[0.5,-1,2]]   B = [[3,-1,1],[2,2,-2]]
  p=2 (the default) -- masked        torch [4.123106 5.09902  2.692582 5.220153] openvino [4.123103 5.099016 2.692581 5.220149] ok
  p=1                                torch [7.  6.  3.5 8.5] openvino [2.999997 3.999995      nan      nan] *** WRONG ***

# NB: the two default-p rows print "ok" only because the script's tolerance is 1e-5;
# relative to torch they differ in the 7th significant digit (the eps/exponent deviation).

Issue submission checklist

  • I'm reporting an issue. It's not a question.
  • I checked the problem with the documentation, FAQ, open issues, Stack Overflow, etc., and have not found a solution.
  • There is reproducer code and related data files such as images, videos, models, etc.

Source: openvinotoolkit/openvino