[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
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:
// 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
pthe negative terms subtract, and the result is simply wrong; - for non-integer
pthe finalPower(summation, inv_p)takes a fractional power of a possibly negativesummation, which yieldsNaN.
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 = 27 → 27^(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.1231027so 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
#!/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
$ 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