Repository navigation
Conversation
|
Hi @helske -- it looks like the actual changes to the source may be missing from your branch, only the test changes seem to have been checked in |
|
Yes I run out of time earlier today. Here's the actual changes. Note: I did not change the current two-scalar versions of If |
andrjohns
left a comment
There was a problem hiding this comment.
Thanks for the contribution! Looking pretty good, just some extra simplifications you can take advantage of and a couple of performance improvements
| const auto probs = softmax(vals); | ||
| return fvar<T_fvar_inner>(log_sum_exp(vals), | ||
| v.d().cwiseProduct(probs).sum()); |
There was a problem hiding this comment.
| const auto probs = softmax(vals); | |
| return fvar<T_fvar_inner>(log_sum_exp(vals), | |
| v.d().cwiseProduct(probs).sum()); | |
| return fvar<T_fvar_inner>(log_sum_exp(vals), | |
| v.d().cwiseProduct(softmax(vals)).sum()); |
| /* | ||
| * log_softmax(x) = x - log_sum_exp(x). | ||
| * | ||
| * exp(-1000) underflows to zero in double precision. Therefore: | ||
| * | ||
| * log(softmax(x)[2]) | ||
| * | ||
| * gives -inf while | ||
| * | ||
| * x[2] - log_sum_exp(x) | ||
| * | ||
| * correctly retains the log-probability. | ||
| */ |
There was a problem hiding this comment.
| /* | |
| * log_softmax(x) = x - log_sum_exp(x). | |
| * | |
| * exp(-1000) underflows to zero in double precision. Therefore: | |
| * | |
| * log(softmax(x)[2]) | |
| * | |
| * gives -inf while | |
| * | |
| * x[2] - log_sum_exp(x) | |
| * | |
| * correctly retains the log-probability. | |
| */ |
Comment isn't really necessary, since the test title already specifies that it's testing a large range
| EXPECT_TRUE(std::isfinite(y.val()(0))); | ||
| EXPECT_TRUE(std::isfinite(y.val()(1))); | ||
| EXPECT_TRUE(std::isfinite(y.val()(2))); |
There was a problem hiding this comment.
| EXPECT_TRUE(std::isfinite(y.val()(0))); | |
| EXPECT_TRUE(std::isfinite(y.val()(1))); | |
| EXPECT_TRUE(std::isfinite(y.val()(2))); |
Will be covered by the value tests below
|
@helske apologies for the delay in wrapping this up! Can you fix up the |
|
No worries, I got occupied with other stuff but I'm aiming to finish this by the end of the week. |
|
The current PR is 1.1 to 1.7 times slower than develop. Time can be improved to 0.5 to 0.9 compared to develop with two changes:
Also, it would be good to fix OpenCL part at the same time, as the OpenCL reverse mode has the same develop defect. I can also make commits for these if needed. |
|
I made further changes, adding the helper function for softmax and log-sum-exp etc. While trying to commit the changes I ended up having some conflicts and diverging branches which I tried to fix with LMM, but it ended up messing things further, so I'm not sure if something got broken in the process. Please check especially the I didn't touch the OpenCL files, not sure about the best way to modify those as I'm familiar with the syntax used on those. |
|
@helske , after you have CPU side finished, you can ask Claude (e.g. Opus 5.5) to implement the same in OpenCL as then it's just porting the algorithms and can re-use values from tests compare to CPU resutls, so it will get it correct |
|
@helske, the new version is better. The history rewrite did not break anything that I could find. On the new head the tests pass and accuracy good. I have two small suggestions for the CPU side speed
using val_t = plain_type_t<decltype(x_arena.val())>;
using return_t = return_var_matrix_t<val_t, T>;
const auto& x_val = to_ref(x_arena.val());
arena_t<val_t> arena_softmax;
const double lse = internal::softmax_log_sum_exp(x_val, arena_softmax);
arena_t<return_t> res = (x_val.array() - lse).matrix();
reverse_pass_callback([x_arena, res, arena_softmax]() mutable {
const auto& res_adj = to_ref(res.adj());
x_arena.adj().array()
+= res_adj.array() - res_adj.sum() * arena_softmax.array();
});
return res;
template <typename T, typename Out,
require_eigen_vt<std::is_arithmetic, T>* = nullptr>
inline double softmax_log_sum_exp(const T& x, Out& softmax_x) {
if (x.size() == 0) {
softmax_x = x;
return NEGATIVE_INFTY;
}
const auto& x_ref = to_ref(x);
const double max = x_ref.maxCoeff();
softmax_x = (x_ref.array() - max).exp().matrix();
const double theta_sum = softmax_x.sum();
softmax_x /= theta_sum;
return std::isfinite(max) ? max + std::log(theta_sum) : max;
}
template <typename T, require_eigen_vt<std::is_arithmetic, T>* = nullptr>
inline std::pair<plain_type_t<T>, double> softmax_log_sum_exp(const T& x) {
plain_type_t<T> softmax_x;
const double lse = softmax_log_sum_exp(x, softmax_x);
return {std::move(softmax_x), lse};
}Rev With both additions, every reverse-mode path is faster than develop for n = 10 to 1000. Rev
A small one: the |
Summary
This PR concerns #2802, I already started to work on this on Stancon hackathon with @SteveBronder.
The current primitives of
softmax,log_softmaxandlog_sum_expare numerically stable according to this paper. However, thefwdandrevspecializations are numerically less stable, and their handling of non-finite values is not consistent with the primitives.The new
fwdandrevspecializations of these functions rely on the stablesoftmaxandlog_sum_expprimitives rather than independently reimplementing them. To make this possible, I relaxed therequire_*constraints on thesoftmaxandlog_softmaxprimitives so that they support also also matrix input (operating over all elements of the matrix). If this is not a good idea, alternativelyfwdandrevcould use reshaping/flattening or reimplement the stable softmax (see also below).There is still potential room for improvement:
fwd::log_softmax,fwd::log_sum_exp, andrev::log_sum_expnow compute bothsoftmax(x)andlog_sum_exp(x)which have overlap in computations which could be avoided by computing both in a single pass.max + std::log((v_ref.array() - max).exp().sum());is safe, their suggested algorithm 4.1 excludes the maximum from the sum and useslog1p, which, based on some standalone tests I made could make a difference in some cases. Basically we could do something likeTests
The new tests covers numerical stability and non-finite-value handling in the
fwdandrevspecializations oflog_softmaxandlog_sum_exp. The tests cover inputs with large dynamic ranges, many small probabilities, and infinite values. These tests fail with the current implementations and pass with the proposed changes. At least on my setup, few of the tests have tolerances of 1e-12 which I'm not sure are actually portable. For writing the tests, I used help from LLM.Side Effects
I guess this now exposes support for matrices in
softmaxandlog_softmax?Release notes
Improved numerical accuracy of
log_sum_exp,softmaxandlog_softmaxfunctions.Checklist
Copyright holder: Jouni Helske
The copyright holder is typically you or your assignee, such as a university or company. By submitting this pull request, the copyright holder is agreeing to the license the submitted work under the following licenses:
- Code: BSD 3-clause (https://opensource.org/licenses/BSD-3-Clause)
- Documentation: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
the basic tests are passing
./runTests.py test/unit)make test-headers)make test-math-dependencies)make doxygen)make cpplint)the code is written in idiomatic C++ and changes are documented in the doxygen
the new changes are tested