Skip to content

Commit cc531bf

Browse files
authored
Merge pull request #19 from TensorKitchen/join-backend
Reduce join backend allocations and add reconstruct_tucker!
2 parents 3a6e789 + 08b978e commit cc531bf

16 files changed

Lines changed: 603 additions & 247 deletions

File tree

src/api/btd.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ end
5050
return solver == :als ? BTDHOSVDMultistartInit() : :alswarm
5151
end
5252

53-
@inline _btd_solver_symbol(solver::AbstractSolver) =
54-
solver isa ALSSolver ? :als : solver_symbol(solver)
53+
@inline _btd_solver_symbol(::ALSSolver) = :als
54+
@inline _btd_solver_symbol(solver::AbstractSolver) = solver_symbol(solver)
5555

5656
function _btd_effective_init(
5757
solver::Symbol,

src/api/cpd.jl

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,104 @@ function _merge_res_solver_info(res, patch::NamedTuple)
2222
)
2323
end
2424

25+
mutable struct _CPDComponentTraceRecorder{M}
26+
model::M
27+
previous::Any
28+
previous_cost::Float64
29+
iterations::Vector{Int}
30+
cost_history::Vector{Float64}
31+
cost_rel_change_history::Vector{Float64}
32+
max_component_delta_history::Vector{Float64}
33+
component_delta_history::Vector{Vector{Float64}}
34+
end
35+
36+
function _CPDComponentTraceRecorder(model)
37+
return _CPDComponentTraceRecorder(
38+
model,
39+
nothing,
40+
NaN,
41+
Int[],
42+
Float64[],
43+
Float64[],
44+
Float64[],
45+
Vector{Float64}[],
46+
)
47+
end
48+
49+
function _rankone_norm2(λ, U, k::Int)
50+
val = abs2(λ[k])
51+
@inbounds for m = 1:length(U)
52+
val *= sum(abs2, @view U[m][:, k])
53+
end
54+
return Float64(val)
55+
end
56+
57+
function _rankone_inner(λa, Ua, λb, Ub, k::Int)
58+
val = λa[k] * λb[k]
59+
@inbounds for m = 1:length(Ua)
60+
val *= dot(@view(Ua[m][:, k]), @view(Ub[m][:, k]))
61+
end
62+
return Float64(val)
63+
end
64+
65+
function _cpd_component_deltas(prev::CPDPoint, curr::CPDPoint)
66+
λ_prev = lambda(prev)
67+
U_prev = factors(prev)
68+
λ_curr = lambda(curr)
69+
U_curr = factors(curr)
70+
r = length(λ_curr)
71+
deltas = Vector{Float64}(undef, r)
72+
@inbounds for k = 1:r
73+
n_prev = _rankone_norm2(λ_prev, U_prev, k)
74+
n_curr = _rankone_norm2(λ_curr, U_curr, k)
75+
cross = _rankone_inner(λ_prev, U_prev, λ_curr, U_curr, k)
76+
delta = sqrt(max(n_prev + n_curr - 2 * cross, 0.0))
77+
deltas[k] = delta / max(sqrt(max(n_prev, 0.0)), 1.0)
78+
end
79+
return deltas
80+
end
81+
82+
function _record_cpd_component_trace!(rec::_CPDComponentTraceRecorder, p, iter::Int)
83+
q = cpd_point(rec.model, p)
84+
cost_val = Float64(cost(rec.model, p))
85+
if rec.previous !== nothing
86+
deltas = _cpd_component_deltas(rec.previous, q)
87+
rel_change = abs(rec.previous_cost - cost_val) / max(abs(rec.previous_cost), 1.0)
88+
push!(rec.iterations, iter)
89+
push!(rec.cost_history, cost_val)
90+
push!(rec.cost_rel_change_history, rel_change)
91+
push!(rec.max_component_delta_history, maximum(deltas))
92+
push!(rec.component_delta_history, deltas)
93+
end
94+
rec.previous = q
95+
rec.previous_cost = cost_val
96+
return nothing
97+
end
98+
99+
function _cpd_component_trace_callback(rec::_CPDComponentTraceRecorder)
100+
return function (problem, state, k)
101+
p = try
102+
Manopt.get_iterate(state)
103+
catch
104+
return nothing
105+
end
106+
_record_cpd_component_trace!(rec, p, Int(k))
107+
return nothing
108+
end
109+
end
110+
111+
function _cpd_component_trace_info(rec::_CPDComponentTraceRecorder)
112+
return (
113+
component_trace_iterations = rec.iterations,
114+
component_trace_cost_history = rec.cost_history,
115+
component_trace_cost_rel_change_history = rec.cost_rel_change_history,
116+
component_trace_max_delta_history = rec.max_component_delta_history,
117+
component_trace_delta_history = rec.component_delta_history,
118+
component_trace_final_max_delta = isempty(rec.max_component_delta_history) ? NaN :
119+
rec.max_component_delta_history[end],
120+
)
121+
end
122+
25123
function _pack_cpd_explicit_p0(model, p0)
26124
p0 isa CPDPoint && return pack_cpd_point(model, p0)
27125
p0 isa CPDResult && return pack_cpd_point(model, cpd_point(p0))
@@ -159,8 +257,12 @@ function _run_cpd_solver(
159257
verbose::Bool,
160258
vector_transport_method,
161259
pullback_eps,
260+
component_trace,
162261
kwargs...,
163262
)
263+
trace_recorder = component_trace ? _CPDComponentTraceRecorder(model) : nothing
264+
iteration_callbacks =
265+
isnothing(trace_recorder) ? () : (_cpd_component_trace_callback(trace_recorder),)
164266
p_solve = if init_eff isa ALSWarmStartInit && isnothing(p0) && !(solver isa ALSSolver)
165267
_cpd_als_warm_then_pack(
166268
model,
@@ -175,7 +277,7 @@ function _run_cpd_solver(
175277
_pack_cpd_explicit_p0(model, p0)
176278
end
177279

178-
return _solve_model(
280+
result = _solve_model(
179281
model;
180282
init = init_eff,
181283
p0 = p_solve,
@@ -188,8 +290,11 @@ function _run_cpd_solver(
188290
verbose,
189291
refinement_verbose = verbose,
190292
vector_transport_method,
293+
iteration_callbacks,
191294
kwargs...,
192295
)
296+
return isnothing(trace_recorder) ? result :
297+
_merge_res_solver_info(result, _cpd_component_trace_info(trace_recorder))
193298
end
194299

195300
function _cpd_impl(
@@ -212,6 +317,7 @@ function _cpd_impl(
212317
verbose,
213318
vector_transport_method,
214319
pullback_eps = 1e-8,
320+
component_trace::Bool = false,
215321
kwargs...,
216322
) where {T<:AbstractFloat,N}
217323
haskey(kwargs, :softplus_beta) && throw(
@@ -254,6 +360,9 @@ function _cpd_impl(
254360
throw(ArgumentError("geometry=$geometry_eff requires nonnegative=true."))
255361
end
256362
if solver_obj isa ALSSolver
363+
component_trace && throw(
364+
ArgumentError("component_trace=true is only supported for manifold solvers."),
365+
)
257366
geometry_eff == :canonical || throw(
258367
ArgumentError(
259368
"solver=:als does not use manifold geometry. Use geometry=:canonical.",
@@ -292,6 +401,7 @@ function _cpd_impl(
292401
verbose,
293402
vector_transport_method,
294403
pullback_eps = pullback_eps_eff,
404+
component_trace,
295405
nonnegative,
296406
kwargs...,
297407
)
@@ -361,6 +471,9 @@ If `r` is omitted, uses the smallest tensor mode as a heuristic rank.
361471
* `verbose = true`: Enables progress output.
362472
* `nonnegative::Bool = false`: Nonnegative CPD option to be selected by the user. (same as `nncpd`)
363473
* `pullback_eps = 1e-8`: Regularization parameter for pullback-style nonnegative geometries.
474+
* `component_trace = false`: For manifold solvers, records per-iteration movement of
475+
each CP rank-one term in `solver_info`. Use this to diagnose whether a flat cost
476+
means the rank-one terms are also stuck.
364477
365478
## Notes
366479
* `solver = :als` does not use manifold geometry. In that case:
@@ -403,6 +516,7 @@ function cpd(
403516
verbose = true,
404517
vector_transport_method = nothing,
405518
pullback_eps = 1e-8,
519+
component_trace::Bool = false,
406520
kwargs...,
407521
) where {T<:AbstractFloat,N}
408522
if nonnegative
@@ -436,6 +550,7 @@ function cpd(
436550
scale_by_lambda = scale_by_lambda,
437551
lambda_eps = lambda_eps,
438552
pullback_eps = pullback_eps,
553+
component_trace = component_trace,
439554
verbose = verbose,
440555
vector_transport_method = vector_transport_method,
441556
kwargs...,
@@ -459,6 +574,7 @@ function cpd(
459574
lambda_eps = lambda_eps,
460575
nonnegative = false,
461576
pullback_eps = pullback_eps,
577+
component_trace = component_trace,
462578
verbose = verbose,
463579
vector_transport_method = vector_transport_method,
464580
kwargs...,

src/cpd/core/cp_normalization.jl

Lines changed: 75 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -73,52 +73,86 @@ function normalize_components!(
7373
)
7474
end
7575

76-
if policy isa NoNormalization
77-
return factors
78-
elseif policy isa SeparateLambdaNormalization
79-
@inbounds for k = 1:r
80-
scale = lambda[k]
81-
for m = 1:d
82-
col = @view factors[m][:, k]
83-
scale = _normalize_column_into_lambda!(col, scale)
84-
end
85-
lambda[k] = scale
76+
return _normalize_components_policy!(factors, lambda, policy)
77+
end
78+
79+
_normalize_components_policy!(
80+
factors::Vector{Matrix{T}},
81+
lambda::Vector{T},
82+
::NoNormalization,
83+
) where {T<:AbstractFloat} = factors
84+
85+
function _normalize_components_policy!(
86+
factors::Vector{Matrix{T}},
87+
lambda::Vector{T},
88+
::SeparateLambdaNormalization,
89+
) where {T<:AbstractFloat}
90+
r = length(lambda)
91+
d = length(factors)
92+
@inbounds for k = 1:r
93+
scale = lambda[k]
94+
for m = 1:d
95+
col = @view factors[m][:, k]
96+
scale = _normalize_column_into_lambda!(col, scale)
8697
end
87-
return factors
88-
elseif policy isa LastModeNormalization
89-
last_mode = d
90-
@inbounds for k = 1:r
91-
total_scale = lambda[k]
92-
for m = 1:d
93-
col = @view factors[m][:, k]
94-
nu = _safe_column_norm!(col)
95-
col ./= nu
96-
total_scale *= nu
97-
end
98-
mag = abs(total_scale)
99-
factors[last_mode][:, k] .*= mag
100-
lambda[k] = _sign_or_zero(total_scale)
98+
lambda[k] = scale
99+
end
100+
return factors
101+
end
102+
103+
function _normalize_components_policy!(
104+
factors::Vector{Matrix{T}},
105+
lambda::Vector{T},
106+
::LastModeNormalization,
107+
) where {T<:AbstractFloat}
108+
r = length(lambda)
109+
d = length(factors)
110+
last_mode = d
111+
@inbounds for k = 1:r
112+
total_scale = lambda[k]
113+
for m = 1:d
114+
col = @view factors[m][:, k]
115+
nu = _safe_column_norm!(col)
116+
col ./= nu
117+
total_scale *= nu
101118
end
102-
return factors
103-
elseif policy isa EvenDistributionNormalization
104-
@inbounds for k = 1:r
105-
total_scale = lambda[k]
106-
for m = 1:d
107-
col = @view factors[m][:, k]
108-
nu = _safe_column_norm!(col)
109-
col ./= nu
110-
total_scale *= nu
111-
end
112-
mag = abs(total_scale)
113-
scale = mag <= eps(T) ? zero(T) : mag^(inv(T(d)))
114-
for m = 1:d
115-
factors[m][:, k] .*= scale
116-
end
117-
lambda[k] = _sign_or_zero(total_scale)
119+
mag = abs(total_scale)
120+
factors[last_mode][:, k] .*= mag
121+
lambda[k] = _sign_or_zero(total_scale)
122+
end
123+
return factors
124+
end
125+
126+
function _normalize_components_policy!(
127+
factors::Vector{Matrix{T}},
128+
lambda::Vector{T},
129+
::EvenDistributionNormalization,
130+
) where {T<:AbstractFloat}
131+
r = length(lambda)
132+
d = length(factors)
133+
@inbounds for k = 1:r
134+
total_scale = lambda[k]
135+
for m = 1:d
136+
col = @view factors[m][:, k]
137+
nu = _safe_column_norm!(col)
138+
col ./= nu
139+
total_scale *= nu
118140
end
119-
return factors
141+
mag = abs(total_scale)
142+
scale = mag <= eps(T) ? zero(T) : mag^(inv(T(d)))
143+
for m = 1:d
144+
factors[m][:, k] .*= scale
145+
end
146+
lambda[k] = _sign_or_zero(total_scale)
120147
end
148+
return factors
149+
end
121150

151+
function _normalize_components_policy!(
152+
factors::Vector{Matrix{T}},
153+
lambda::Vector{T},
154+
policy::AbstractNormalizationPolicy,
155+
) where {T<:AbstractFloat}
122156
throw(ArgumentError("Unsupported normalization policy $(typeof(policy))."))
123157
end
124158

src/dispatch/approx_routing.jl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@
33
# If every summand is a Manifolds.Segre with the same factor_dims, the call is a
44
# plain rank-r CPD. Route those directly into the cpd() tree so they get the
55
# CPDBackend, CPDResult, and all CPD-specific kwargs (geometry, nonnegative, ...).
6+
_is_segre_manifold(::Manifolds.Segre) = true
7+
_is_segre_manifold(::AbstractManifold) = false
8+
69
function _all_segre_uniform(manifolds)
710
isempty(manifolds) && return false
8-
all(m -> m isa Manifolds.Segre, manifolds) || return false
11+
all(_is_segre_manifold, manifolds) || return false
912
d0 = factor_dims(first(manifolds))
1013
return all(m -> factor_dims(m) == d0, manifolds)
1114
end
1215

16+
_is_tucker_manifold(::Manifolds.Tucker) = true
17+
_is_tucker_manifold(::AbstractManifold) = false
18+
1319
function _all_tucker_uniform(manifolds, target_shape::Tuple)
1420
isempty(manifolds) && return false
15-
all(m -> m isa Manifolds.Tucker, manifolds) || return false
21+
all(_is_tucker_manifold, manifolds) || return false
1622
dims0 = factor_dims(first(manifolds))
1723
ranks0 = multilinear_rank(first(manifolds))
1824
dims0 == target_shape || return false

0 commit comments

Comments
 (0)