Skip to content

[executorch] Make operator<<() wrap long EValue lists #480

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 46 additions & 3 deletions extension/evalue_util/print_evalue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include <executorch/runtime/core/exec_aten/util/scalar_type_util.h>

#include <algorithm>
#include <cmath>
#include <iomanip>
#include <ostream>
Expand All @@ -20,6 +21,9 @@ namespace executor {

namespace {

/// Number of list items on a line before wrapping.
constexpr size_t kItemsPerLine = 10;

/// The default number of first/last list items to print before eliding.
constexpr size_t kDefaultEdgeItems = 3;

Expand Down Expand Up @@ -77,18 +81,57 @@ void print_scalar_list(
if (print_length) {
os << "(len=" << list.size() << ")";
}
// TODO(T159700776): Wrap at a specified number of columns.

// See if we'll be printing enough elements to cause us to wrap.
bool wrapping = false;
{
long num_printed_items;
if (elide_inner_items) {
num_printed_items =
std::min(static_cast<long>(list.size()), edge_items * 2);
} else {
num_printed_items = static_cast<long>(list.size());
}
wrapping = num_printed_items > kItemsPerLine;
}

os << "[";
size_t num_printed = 0;
for (size_t i = 0; i < list.size(); ++i) {
if (wrapping && num_printed % kItemsPerLine == 0) {
// We've printed a full line, so wrap and begin a new one.
os << "\n ";
}
os << EValue(exec_aten::Scalar(list[i]));
if (i < list.size() - 1) {
if (wrapping || i < list.size() - 1) {
// No trailing comma when not wrapping. Always a trailing comma when
// wrapping. This will leave a trailing space at the end of every wrapped
// line, but it simplifies the logic here.
os << ", ";
}
++num_printed;
if (i + 1 == edge_items && i + edge_items + 1 < list.size()) {
os << "..., ";
if (wrapping) {
os << "\n ...,";
// Make the first line after the elision be the ragged line, letting us
// always end on a full line.
num_printed = kItemsPerLine - edge_items % kItemsPerLine;
if (num_printed % kItemsPerLine != 0) {
// If the line ended exactly when the elision happened, the next
// iteration of the loop will add this line break.
os << "\n ";
}
} else {
// Non-wrapping elision.
os << "..., ";
}
i = list.size() - edge_items - 1;
}
}
if (wrapping) {
// End the current line.
os << "\n";
}
os << "]";
}

Expand Down
240 changes: 240 additions & 0 deletions extension/evalue_util/test/print_evalue_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -729,3 +729,243 @@ TEST(PrintEvalueTest, EdgeItemsAffectsTensorData) {
// in full.
"tensor(sizes=[5, 1, 1, 2], [1., ..., 10.1])\n");
}

//
// Long list wrapping.
//
// Use double as a proxy for testing the wrapping logic; the other scalar
// types use the same underlying code, so they don't need to test this again.
//

// Duplicates the internal value in the cpp file under test.
constexpr size_t kItemsPerLine = 10;

TEST(PrintEvalueTest, ListWrapping) {
// A large list of scalars.
std::array<double, 100> list;
for (int i = 0; i < list.size(); ++i) {
list[i] = static_cast<double>(i);
}

{
// Should elide by default and print on a single line.
EValue value(ArrayRef<double>(list.data(), list.size()));

std::ostringstream os;
os << value;
EXPECT_STREQ(os.str().c_str(), "(len=100)[0., 1., 2., ..., 97., 98., 99.]");
}
{
// Exactly the per-line length should not wrap when increasing the number of
// edge items to disable elision.
EValue value(ArrayRef<double>(list.data(), kItemsPerLine));

std::ostringstream os;
os << torch::executor::util::evalue_edge_items(1000) << value;
EXPECT_STREQ(
os.str().c_str(), "(len=10)[0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]");
}
{
// One more than the per-line length should wrap; no elision.
EValue value(ArrayRef<double>(list.data(), kItemsPerLine + 1));

std::ostringstream os;
os << torch::executor::util::evalue_edge_items(1000) << value;
EXPECT_STREQ(
os.str().c_str(),
"(len=11)[\n"
" 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., \n"
" 10., \n"
"]");
}
{
// Exactly twice the per-line length, without elision.
EValue value(ArrayRef<double>(list.data(), kItemsPerLine * 2));

std::ostringstream os;
os << torch::executor::util::evalue_edge_items(1000) << value;
EXPECT_STREQ(
os.str().c_str(),
"(len=20)[\n"
" 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., \n"
" 10., 11., 12., 13., 14., 15., 16., 17., 18., 19., \n"
// Make sure there is no extra newline here.
"]");
}
{
// Exactly one whole line, with elision.
EValue value(ArrayRef<double>(list.data(), kItemsPerLine * 3));

std::ostringstream os;
os << torch::executor::util::evalue_edge_items(kItemsPerLine) << value;
EXPECT_STREQ(
os.str().c_str(),
"(len=30)[\n"
" 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., \n"
// Elision always on its own line when wrapping.
" ...,\n"
" 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., \n"
"]");
}
{
// Edge item count slightly larger than per-line length, with elision.
EValue value(ArrayRef<double>(list.data(), kItemsPerLine * 3));

std::ostringstream os;
os << torch::executor::util::evalue_edge_items(kItemsPerLine + 1) << value;
EXPECT_STREQ(
os.str().c_str(),
"(len=30)[\n"
" 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., \n"
" 10., \n"
// Elision always on its own line when wrapping.
" ...,\n"
// The ragged line always comes just after the elision so that
// we will end on a full line.
" 19., \n"
" 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., \n"
"]");
}
{
// Large wrapped, ragged, elided example.
EValue value(ArrayRef<double>(list.data(), list.size()));

std::ostringstream os;
os << torch::executor::util::evalue_edge_items(33) << value;
EXPECT_STREQ(
os.str().c_str(),
"(len=100)[\n"
" 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., \n"
" 10., 11., 12., 13., 14., 15., 16., 17., 18., 19., \n"
" 20., 21., 22., 23., 24., 25., 26., 27., 28., 29., \n"
" 30., 31., 32., \n"
" ...,\n"
" 67., 68., 69., \n"
" 70., 71., 72., 73., 74., 75., 76., 77., 78., 79., \n"
" 80., 81., 82., 83., 84., 85., 86., 87., 88., 89., \n"
" 90., 91., 92., 93., 94., 95., 96., 97., 98., 99., \n"
"]");
}
}

TEST(PrintEvalueTest, WrappedTensorData) {
TensorFactory<ScalarType::Double> tf;
// A tensor with a large number of elements.
EValue value(tf.ones({10, 10}));

std::ostringstream os;
os << torch::executor::util::evalue_edge_items(33) << value;
EXPECT_STREQ(
os.str().c_str(),
"tensor(sizes=[10, 10], [\n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
" 1., 1., 1., \n"
" ...,\n"
" 1., 1., 1., \n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
"])");
}

TEST(PrintEvalueTest, WrappedTensorSizes) {
TensorFactory<ScalarType::Double> tf;

{
// A tensor with enough dimensions that the sizes list is wrapped, but
// the data is not.
std::vector<int32_t> sizes(kItemsPerLine + 1, 1);
sizes[0] = 5;
EValue value(tf.ones(sizes));

std::ostringstream os;
os << value;
EXPECT_STREQ(
os.str().c_str(),
"tensor(sizes=[\n"
" 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, \n"
" 1, \n"
"], [1., 1., 1., 1., 1.])");
}
{
// Both sizes and data are wrapped.
std::vector<int32_t> sizes(kItemsPerLine + 1, 1);
sizes[0] = 100;
EValue value(tf.ones(sizes));

std::ostringstream os;
os << torch::executor::util::evalue_edge_items(15) << value;
EXPECT_STREQ(
os.str().c_str(),
"tensor(sizes=[\n"
" 100, 1, 1, 1, 1, 1, 1, 1, 1, 1, \n"
" 1, \n"
// TODO(T159700776): Indent this further to look more like python.
"], [\n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
" 1., 1., 1., 1., 1., \n"
" ...,\n"
" 1., 1., 1., 1., 1., \n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
"])");
}
}

TEST(PrintEvalueTest, WrappedTensorLists) {
TensorFactory<ScalarType::Float> tf;

std::array<EValue, 2> values = {
// Tensors that are large enough for their data to wrap.
tf.ones({10, 10}),
tf.ones({11, 11}),
};
std::array<EValue*, values.size()> wrapped_values = {
&values[0],
&values[1],
};
// Memory that BoxedEvalueList will use to assemble a contiguous array of
// Tensor entries. It's important not to destroy these entries, because the
// values list will own the underlying Tensors.
auto unwrapped_values_memory = std::make_unique<uint8_t[]>(
sizeof(exec_aten::Tensor) * wrapped_values.size());
exec_aten::Tensor* unwrapped_values =
reinterpret_cast<exec_aten::Tensor*>(unwrapped_values_memory.get());
#if USE_ATEN_LIB
// Must be initialized because BoxedEvalueList will use operator=() on each
// entry. But we can't do this in non-ATen mode because
// torch::executor::Tensor doesn't have a default constructor.
for (int i = 0; i < wrapped_values.size(); ++i) {
new (&unwrapped_values[i]) at::Tensor();
}
#endif

// Demonstrate the formatting when printing a list with multiple tensors.
BoxedEvalueList<exec_aten::Tensor> list(
wrapped_values.data(), unwrapped_values, wrapped_values.size());
EValue value(list);

std::ostringstream os;
os << torch::executor::util::evalue_edge_items(15) << value;
EXPECT_STREQ(
os.str().c_str(),
"(len=2)[\n"
" [0]: tensor(sizes=[10, 10], [\n"
// TODO(T159700776): Indent these entries further to look more like
// python.
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
" 1., 1., 1., 1., 1., \n"
" ...,\n"
" 1., 1., 1., 1., 1., \n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
"]),\n"
" [1]: tensor(sizes=[11, 11], [\n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
" 1., 1., 1., 1., 1., \n"
" ...,\n"
" 1., 1., 1., 1., 1., \n"
" 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., \n"
"]),\n"
"]");
}