Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2bf1a0f
linear ot implemented
Jul 23, 2025
b24c275
improve stopping criterion and assymetric case
Jul 24, 2025
1267322
Add recompute_const and simplify the pipeline for the symmetric = False
Aug 22, 2025
acb6233
add tests
Aug 23, 2025
8645bb8
update the examples and rename to follow the "ot.solve" naming conven…
Aug 25, 2025
ba09929
update realeases.md
Aug 25, 2025
a3344cb
idem
Aug 25, 2025
f5dfe15
Merge branch 'master' into ot-batch
rflamary Aug 25, 2025
022e398
move set_grad_enabled to backend
Aug 25, 2025
aee7eb4
set_grad_enabled for quadratric solver
Aug 25, 2025
fcff735
update doc
Aug 25, 2025
9ccb357
remove useless importation in doc
Aug 25, 2025
e04d4e0
Update references
Aug 25, 2025
68117dd
Merge branch 'master' into ot-batch
cedricvincentcuaz Aug 27, 2025
ed8c842
Merge branch 'master' into ot-batch
rflamary Sep 4, 2025
fe37b7e
Merge branch 'master' into ot-batch
rflamary Sep 8, 2025
ae478fe
update example
Sep 10, 2025
a8e2319
Remove classes in quadratic, move examples to backend, add potentials…
Sep 10, 2025
395e4cc
updat tests
Sep 10, 2025
0d6fbbc
Massive improvement of the documentation for ot.batch
Sep 10, 2025
da7639e
cover (almost) all ot.batch with tests
Sep 11, 2025
62a6f9d
bug in the tests
Sep 11, 2025
17fa680
update docstring
Sep 11, 2025
6f6109d
highlight that ot.batch is solving the entropic version
Sep 11, 2025
5524efb
removing yet another error in the docstring
Sep 11, 2025
bb37cf2
Add missing parameter recompute_const
Sep 11, 2025
f6ed5f6
Remove png, add all backends and gradient mode to tests
Sep 15, 2025
cde102b
add the missing pytest
Sep 15, 2025
f06b99f
change .sum() into nx.sum
Sep 15, 2025
dd6eda3
add missing backend
Sep 15, 2025
3fa7c1b
yet another missing nx
Sep 15, 2025
a77d034
remove useless squeeze and add test for non-log bregman
Sep 15, 2025
1528f95
remove last_step from quadratic tests
Sep 15, 2025
69f59a1
add missing tests and improve documentation
Sep 16, 2025
27cea5e
proper unsqueeze test
Sep 16, 2025
3264b5c
add unsqueeze to tensorflow
Sep 16, 2025
e70ecd1
solve double backprop issue in test_gradients_torch
Sep 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- Fix jax version for auto-grad (PR #732)
- Added to each example in the examples gallery the information about the release version in which it was introduced (PR #743)
- Removed release information from quickstart guide (PR #744)
- Implement batch parallel solvers in ot.batch (PR #745)

#### Closed issues
- Fixed `ot.mapping` solvers which depended on deprecated `cvxpy` `ECOS` solver (PR #692, Issue #668)
Expand Down
4 changes: 4 additions & 0 deletions examples/batch/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@


Batch parrallel optimal transport
Comment thread
KrzakalaPaul marked this conversation as resolved.
Outdated
-----------------------------------
114 changes: 114 additions & 0 deletions examples/batch/demo_efficiency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
Comment thread
KrzakalaPaul marked this conversation as resolved.
Outdated
"""
==================
Batch parallel OT
==================

Shows the efficiency of using parallel OT solvers for optimal transport.

"""

# Author: Paul Krzakala <paul.krzakala@gmail.com>
# License: MIT License

from ot.batch._linear import *
from ot.batch._quadratic import *
import numpy as np
import matplotlib.pyplot as plt
from time import perf_counter
import torch

b = 256
n = 40
d_nodes = 16
d_edges = 16
epsilon = 0.1
max_iter = 10
tol = 1e-5
max_iter_inner = 10
tol_inner = 1e-5
alpha = 0.5

X1 = torch.randn(b, n, d_nodes, dtype=torch.float32)
C1 = torch.randn(b, n, n, d_edges, dtype=torch.float32)

X2 = X1[:, torch.randperm(n), :]
perm = torch.randperm(n)
C2 = C1[:, perm, :, :][:, :, perm, :]

M = cost_matrix_l2_batch(X1, X2)

# Compute using for loops and batches of size 1
start = perf_counter()
average_cost = 0
for i in range(b):
res = solve_gromov_batch(
alpha=alpha,
epsilon=epsilon,
M=M[i : i + 1],
C1=C1[i : i + 1],
C2=C2[i : i + 1],
max_iter=max_iter,
tol=tol,
max_iter_inner=max_iter_inner,
tol_inner=tol_inner,
)
average_cost += res.value.item()
average_cost /= b

end = perf_counter()
print(f"Quadratic solver naive (CPU): {end - start:.4f} seconds")
print(f"Average cost (CPU): {average_cost:.4f}")
print("")

start = perf_counter()
res = solve_gromov_batch(
alpha=alpha,
epsilon=epsilon,
M=M,
C1=C1,
C2=C2,
max_iter=max_iter,
tol=tol,
max_iter_inner=max_iter_inner,
tol_inner=tol_inner,
)
average_cost = res.value.mean().item()
end = perf_counter()
print(f"Quadratic solver batch (CPU): {end - start:.4f} seconds")
print(f"Average cost (CPU): {average_cost:.4f}")
print("")

if torch.cuda.is_available():
M, C1, C2 = M.cuda(), C1.cuda(), C2.cuda()
# GPU Warmup
print("Warming up GPU...")
for _ in range(100):
solve_gromov_batch(
alpha=alpha,
epsilon=epsilon,
M=M,
C1=C1,
C2=C2,
max_iter=max_iter,
tol=tol,
max_iter_inner=max_iter_inner,
tol_inner=tol_inner,
)
print("Done.")
start = perf_counter()
out = solve_gromov_batch(
alpha=alpha,
epsilon=epsilon,
M=M,
C1=C1,
C2=C2,
max_iter=max_iter,
tol=tol,
max_iter_inner=max_iter_inner,
tol_inner=tol_inner,
)
average_cost = out["cost"].mean().item()
end = perf_counter()
print(f"Quadratic solver batch (GPU): {end - start:.4f} seconds")
print(f"Average cost (GPU): {average_cost:.4f}")
136 changes: 136 additions & 0 deletions examples/batch/demo_pitfalls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
"""
==================
Batch parallel linear OT
==================

Showcase the importance of correctly setting the "symmetric" boolean.

"""

from ot.batch._linear import *
from ot.batch._quadratic import *
import numpy as np
import torch
from time import perf_counter

np.random.seed(0)


def demo_symmetric():
print("")
print("Generate random data with C1 and C2 NOT symmetric...")
b = 256
n = 40
epsilon = 0.1
max_iter = 10
tol = 1e-5
max_iter_inner = 10
tol_inner = 1e-5

C1 = torch.randn(b, n, n, dtype=torch.float32)

perm = torch.randperm(n)
C2 = C1[:, perm, :][:, :, perm]
C2 = torch.randn(b, n, n, dtype=torch.float32)

print("")

print("Solving gromov with symmetric=True...")
start = perf_counter()
res = solve_gromov_batch(
epsilon=epsilon,
C1=C1,
C2=C2,
max_iter=max_iter,
tol=tol,
max_iter_inner=max_iter_inner,
tol_inner=tol_inner,
symmetric=True,
)
end = perf_counter()
n_iter = res.log["n_iter"]
print(f"... solver took: {n_iter} iterations.")
print(f"... solver took: {end - start:.4f} seconds")
print(f"... final value: {res.value.mean().item():.4f}")

print("")

print("Solving gromov with symmetric=False...")
start = perf_counter()
res = solve_gromov_batch(
epsilon=epsilon,
C1=C1,
C2=C2,
max_iter=max_iter,
tol=tol,
max_iter_inner=max_iter_inner,
tol_inner=tol_inner,
symmetric=False,
)
end = perf_counter()
n_iter = res.log["n_iter"]
print(f"... solver took: {n_iter} iterations.")
print(f"... solver took: {end - start:.4f} seconds")
print(f"... final value: {res.value.mean().item():.4f}")

print("")
print(
"Conclusion: symmetric=False is slightly faster but leads to suboptimal values because some incorrect approximations are made."
)


def demo_recompute_const():
Comment thread
KrzakalaPaul marked this conversation as resolved.
Outdated
print("")
print("Generate random data...")
b = 64
n = 64
d_nodes = 4
d_edges = 2

C1 = np.random.randn(b, n, n, d_edges).astype("float32")
C2 = np.random.randn(b, n, n, d_edges).astype("float32")

p = np.ones((b, n), dtype=np.float32) / n
q = np.ones((b, n), dtype=np.float32) / n

def compute_loss(T, recompute_const=False):
metric = QuadraticEuclidean()
L = metric.cost_tensor(p, q, C1, C2, symmetric=True)
LT = tensor_product(L, T, recompute_const=recompute_const, symmetric=True)
loss = (LT * T).sum((1, 2))
return loss

print("")
print("Generate random coupling matrix T that does not satisfy marginals...")
T = np.random.rand(b, n, n).astype("float32")

print("")
print("Compute the gromov loss - recompute_const=False")
loss = compute_loss(T, recompute_const=False)
is_pos = (loss >= -1e-6).all()
print("All losses are positive: ", is_pos)

print("")
print("Compute the gromov loss - recompute_const=True")
T = np.random.rand(b, n, n).astype("float32")
loss = compute_loss(T, recompute_const=True)
is_pos = (loss >= -1e-6).all()
print("All losses are positive: ", is_pos)

print("")
print(
"Conclusion: when T does not satisfy marginals, it is important to set recompute_const=True to ensure that the loss computation is correct."
)


if __name__ == "__main__":
Comment thread
KrzakalaPaul marked this conversation as resolved.
Outdated
print(
"----------------- Setting symmetric Flag correctly in ot.solve_gromov -----------------"
)
demo_symmetric()
print("")
print(
"----------------- Testing recompute_const option in tensor_product -----------------"
)
demo_recompute_const()
61 changes: 61 additions & 0 deletions examples/batch/demo_solve_batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
"""
==================
Batch parallel linear OT
==================

Shows how to use the batch parallel linear OT solvers.

"""

# Author: Paul Krzakala <paul.krzakala@gmail.com>
# License: MIT License

from ot.batch._linear import *
from ot.batch._quadratic import *
import numpy as np
import matplotlib.pyplot as plt

# Set parameters
b = 4
n = 20
d = 4
noise = 0.1
max_iter = 10000
tol = 1e-5

# Generate random data
X = np.random.randn(b, n, d).astype("float32")
Y = np.random.randn(b, n, d).astype("float32")
M = cost_matrix_l2_batch(X, Y)

# Define grid search
epsilons = np.logspace(-2, 1, num=10)

plt.figure(figsize=(10, 6))

cost_list = []
n_iter_list = []
for epsilon in epsilons:
res = solve_batch(epsilon=epsilon, M=M, max_iter=max_iter, tol=tol, log_dual=True)
n_iter = res.log["n_iter"]
cost = res.value_linear.mean()
cost_list.append(cost)
n_iter_list.append(n_iter)

ax1 = plt.gca()
ax2 = ax1.twinx()

ax1.plot(epsilons, n_iter_list, "r-o", label="Iterations")
ax2.plot(epsilons, cost_list, "b-s", label="Cost")

ax1.set_xlabel("Epsilon")
ax1.set_ylabel("Iterations", color="r")
ax2.set_ylabel("Cost", color="b")
ax1.set_xscale("log")

ax1.tick_params(axis="y", labelcolor="r")
ax2.tick_params(axis="y", labelcolor="b")

plt.title("Epsilon vs Iters and Cost")
plt.show()
64 changes: 64 additions & 0 deletions examples/batch/demo_solve_gromov_batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""
==================
Batch parallel linear OT
==================

Shows how to use the batch parallel quadratic OT solvers.

"""

from ot.batch._linear import *
from ot.batch._quadratic import *
import numpy as np
import matplotlib.pyplot as plt

b = 32
n = 20
d_nodes = 4
d_edges = 2
epsilon = 0.1
max_iter = 10
tol = 1e-5
max_iter_inner = 10
tol_inner = 1e-5

X1 = np.random.randn(b, n, d_nodes).astype("float32")
C1 = np.random.randn(b, n, n, d_edges).astype("float32")

X2 = X1[:, np.random.permutation(n), :]
permutation = np.random.permutation(n)
C2 = C1[:, permutation, :, :][:, :, permutation, :]

M = cost_matrix_l2_batch(X1, X2)

cost_linear_list = []
cost_quadratic_list = []
alpha_list = np.linspace(0.001, 0.999, 20)

for alpha in alpha_list:
res = solve_gromov_batch(
alpha=alpha,
epsilon=epsilon,
M=M,
C1=C1,
C2=C2,
max_iter=max_iter,
tol=tol,
max_iter_inner=max_iter_inner,
tol_inner=tol_inner,
)

cost_linear = res.value_linear.mean() / (1 - alpha)
cost_quadratic = res.value_quad.mean() / (alpha)
cost_linear_list.append(cost_linear)
cost_quadratic_list.append(cost_quadratic)

plt.figure(figsize=(10, 6))
plt.plot(alpha_list, cost_linear_list, marker="o", label="Cost Linear")
plt.plot(alpha_list, cost_quadratic_list, marker="o", label="Cost Quadratic")
plt.xlabel("Alpha")
plt.ylabel("Average Cost")
plt.legend()
plt.title("Cost vs Alpha")
plt.show()
Loading
Loading