diff options
author | Håvard Pettersen <havardpe@oath.com> | 2019-11-26 13:11:15 +0000 |
---|---|---|
committer | Håvard Pettersen <havardpe@oath.com> | 2019-12-02 12:25:12 +0000 |
commit | da8f15347142a826aaea75a6e6d300f73ed4607b (patch) | |
tree | 0161c1832ade10d569aafd013e96662367172380 /eval | |
parent | 27340646405f5aed96d8816ff3df3f787d9edbeb (diff) |
tensor peek
Diffstat (limited to 'eval')
24 files changed, 536 insertions, 68 deletions
diff --git a/eval/src/tests/eval/compiled_function/compiled_function_test.cpp b/eval/src/tests/eval/compiled_function/compiled_function_test.cpp index 151a4cf5dd5..1d5a6929083 100644 --- a/eval/src/tests/eval/compiled_function/compiled_function_test.cpp +++ b/eval/src/tests/eval/compiled_function/compiled_function_test.cpp @@ -58,6 +58,9 @@ std::vector<vespalib::string> unsupported = { }; bool is_unsupported(const vespalib::string &expression) { + if (expression.find("{") != vespalib::string::npos) { + return true; + } for (const auto &prefix: unsupported) { if (starts_with(expression, prefix)) { return true; diff --git a/eval/src/tests/eval/function/function_test.cpp b/eval/src/tests/eval/function/function_test.cpp index 0e3100ae425..d93b57ab5b2 100644 --- a/eval/src/tests/eval/function/function_test.cpp +++ b/eval/src/tests/eval/function/function_test.cpp @@ -81,6 +81,12 @@ void verify_error(const vespalib::string &expr, const vespalib::string &expected EXPECT_EQUAL(expected_error, function.get_error()); } +void verify_parse(const vespalib::string &expr, const vespalib::string &expect) { + Function function = Function::parse(expr); + EXPECT_TRUE(!function.has_error()); + EXPECT_EQUAL(function.dump_as_lambda(), expect); +} + TEST("require that scientific numbers can be parsed") { EXPECT_EQUAL(1.0, as_number(Function::parse(params, "1"))); EXPECT_EQUAL(2.5, as_number(Function::parse(params, "2.5"))); @@ -933,6 +939,28 @@ TEST("require that convenient tensor create detects under-specified cells") { //----------------------------------------------------------------------------- +TEST("require that tensor peek can be parsed") { + TEST_DO(verify_parse("t{x:1,y:foo}", "f(t)(t{x:1,y:foo})")); +} + +TEST("require that tensor peek can contain expressions") { + TEST_DO(verify_parse("t{x:1+2,y:foo}", "f(t)(t{x:(1+2),y:foo})")); + TEST_DO(verify_parse("t{x:1+bar,y:foo}", "f(t,bar)(t{x:(1+bar),y:foo})")); + TEST_DO(verify_parse("t{x:1,y:foo+2}", "f(t,foo)(t{x:1,y:(foo+2)})")); + TEST_DO(verify_parse("t{x:1,y:(foo)}", "f(t,foo)(t{x:1,y:(foo)})")); +} + +TEST("require that tensor peek can contain extra whitespace") { + TEST_DO(verify_parse(" t { x : 1 + bar , y : foo + 2 } ", + "f(t,bar,foo)(t{x:(1+bar),y:(foo+2)})")); +} + +TEST("require that empty tensor peek is not allowed") { + TEST_DO(verify_error("x{}", "[x{}]...[empty peek spec]...[]")); +} + +//----------------------------------------------------------------------------- + TEST("require that tensor concat can be parsed") { EXPECT_EQUAL("concat(a,b,d)", Function::parse({"a", "b"}, "concat(a,b,d)").dump()); EXPECT_EQUAL("concat(a,b,d)", Function::parse({"a", "b"}, " concat ( a , b , d ) ").dump()); diff --git a/eval/src/tests/eval/node_types/node_types_test.cpp b/eval/src/tests/eval/node_types/node_types_test.cpp index b2ad107f2aa..97a80d9b4b3 100644 --- a/eval/src/tests/eval/node_types/node_types_test.cpp +++ b/eval/src/tests/eval/node_types/node_types_test.cpp @@ -214,6 +214,31 @@ TEST("require that tensor create resolves correct type") { TEST_DO(verify("tensor(x[3]):{{x:0}:double,{x:1}:error,{x:2}:double}", "error")); } +TEST("require that tensor peek resolves correct type") { + TEST_DO(verify("tensor(x[3]){x:1}", "double")); + TEST_DO(verify("tensor(x[3]){x:double}", "error")); + TEST_DO(verify("tensor(x[3]){x:(double)}", "double")); + TEST_DO(verify("tensor(x[3]){x:3}", "error")); + TEST_DO(verify("tensor(x{}){x:1}", "double")); + TEST_DO(verify("tensor(x{}){x:foo}", "double")); + TEST_DO(verify("tensor(x{}){x:(double)}", "double")); + TEST_DO(verify("tensor(x{}){x:tensor(x[3])}", "error")); + TEST_DO(verify("tensor(x{},y[3]){x:foo,y:2}", "double")); + TEST_DO(verify("tensor(x{},y[3]){x:foo}", "tensor(y[3])")); + TEST_DO(verify("tensor(x{},y[3]){y:2}", "tensor(x{})")); + TEST_DO(verify("tensor<float>(x[3]){x:1}", "double")); + TEST_DO(verify("tensor<float>(x[3]){x:double}", "error")); + TEST_DO(verify("tensor<float>(x[3]){x:(double)}", "double")); + TEST_DO(verify("tensor<float>(x[3]){x:3}", "error")); + TEST_DO(verify("tensor<float>(x{}){x:1}", "double")); + TEST_DO(verify("tensor<float>(x{}){x:foo}", "double")); + TEST_DO(verify("tensor<float>(x{}){x:(double)}", "double")); + TEST_DO(verify("tensor<float>(x{}){x:tensor(x[3])}", "error")); + TEST_DO(verify("tensor<float>(x{},y[3]){x:foo,y:2}", "double")); + TEST_DO(verify("tensor<float>(x{},y[3]){x:foo}", "tensor<float>(y[3])")); + TEST_DO(verify("tensor<float>(x{},y[3]){y:2}", "tensor<float>(x{})")); +} + TEST("require that tensor concat resolves correct type") { TEST_DO(verify("concat(double,double,x)", "tensor(x[2])")); TEST_DO(verify("concat(tensor(x[2]),tensor(x[3]),x)", "tensor(x[5])")); diff --git a/eval/src/tests/eval/tensor_function/tensor_function_test.cpp b/eval/src/tests/eval/tensor_function/tensor_function_test.cpp index c30438b551d..1e59b35a01f 100644 --- a/eval/src/tests/eval/tensor_function/tensor_function_test.cpp +++ b/eval/src/tests/eval/tensor_function/tensor_function_test.cpp @@ -6,6 +6,7 @@ #include <vespa/eval/eval/tensor_function.h> #include <vespa/eval/eval/value_type.h> #include <vespa/vespalib/util/stash.h> +#include <vespa/vespalib/util/stringfmt.h> #include <map> using namespace vespalib; @@ -49,12 +50,27 @@ struct EvalCtx { Value::UP make_false() { return engine.from_spec(TensorSpec("double").add({}, 0.0)); } - Value::UP make_simple_vector() { + Value::UP make_vector(std::initializer_list<double> cells, vespalib::string dim = "x", bool mapped = false) { + vespalib::string type_spec = mapped + ? make_string("tensor(%s{})", dim.c_str()) + : make_string("tensor(%s[%zu])", dim.c_str(), cells.size()); + TensorSpec spec(type_spec); + size_t idx = 0; + for (double cell_value: cells) { + TensorSpec::Label label = mapped + ? TensorSpec::Label(make_string("%zu", idx++)) + : TensorSpec::Label(idx++); + spec.add({{dim, label}}, cell_value); + } + return engine.from_spec(spec); + } + Value::UP make_mixed_tensor(double a, double b, double c, double d) { return engine.from_spec( - TensorSpec("tensor(x[3])") - .add({{"x",0}}, 1) - .add({{"x",1}}, 2) - .add({{"x",2}}, 3)); + TensorSpec("tensor(x{},y[2])") + .add({{"x", "foo"}, {"y", 0}}, a) + .add({{"x", "foo"}, {"y", 1}}, b) + .add({{"x", "bar"}, {"y", 0}}, c) + .add({{"x", "bar"}, {"y", 1}}, d)); } Value::UP make_tensor_matrix_first_half() { return engine.from_spec( @@ -240,7 +256,7 @@ TEST("require that tensor create works") { size_t a_id = ctx.add_tensor(ctx.make_double(1.0)); size_t b_id = ctx.add_tensor(ctx.make_double(2.0)); Value::UP my_const = ctx.make_double(3.0); - Value::UP expect = ctx.make_simple_vector(); + Value::UP expect = ctx.make_vector({1.0, 2.0, 3.0}); const auto &a = inject(ValueType::from_spec("double"), a_id, ctx.stash); const auto &b = inject(ValueType::from_spec("double"), b_id, ctx.stash); const auto &c = const_value(*my_const, ctx.stash); @@ -257,6 +273,56 @@ TEST("require that tensor create works") { TEST_DO(verify_equal(*expect, ctx.eval(prog))); } +TEST("require that single value tensor peek works") { + EvalCtx ctx(SimpleTensorEngine::ref()); + size_t a_id = ctx.add_tensor(ctx.make_double(1.0)); + Value::UP my_const = ctx.make_mixed_tensor(1.0, 2.0, 3.0, 4.0); + Value::UP expect = ctx.make_vector({2.0, 3.0, 0.0}); + const auto &a = inject(ValueType::from_spec("double"), a_id, ctx.stash); + const auto &t = const_value(*my_const, ctx.stash); + const auto &peek1 = peek(t, {{"x", "foo"}, {"y", a}}, ctx.stash); + const auto &peek2 = peek(t, {{"x", "bar"}, {"y", size_t(0)}}, ctx.stash); + const auto &peek3 = peek(t, {{"x", "bar"}, {"y", size_t(1000)}}, ctx.stash); + const auto &fun = create(ValueType::from_spec("tensor(x[3])"), + { + {{{"x", 0}}, peek1}, + {{{"x", 1}}, peek2}, + {{{"x", 2}}, peek3} + }, + ctx.stash); + EXPECT_TRUE(fun.result_is_mutable()); + EXPECT_EQUAL(expect->type(), fun.result_type()); + const auto &prog = ctx.compile(fun); + TEST_DO(verify_equal(*expect, ctx.eval(prog))); +} + +TEST("require that tensor subspace tensor peek works") { + EvalCtx ctx(SimpleTensorEngine::ref()); + Value::UP my_const = ctx.make_mixed_tensor(1.0, 2.0, 3.0, 4.0); + Value::UP expect = ctx.make_vector({3.0, 4.0}, "y"); + const auto &t = const_value(*my_const, ctx.stash); + const auto &fun = peek(t, {{"x", "bar"}}, ctx.stash); + EXPECT_TRUE(fun.result_is_mutable()); + EXPECT_EQUAL(expect->type(), fun.result_type()); + const auto &prog = ctx.compile(fun); + TEST_DO(verify_equal(*expect, ctx.eval(prog))); +} + +TEST("require that automatic string conversion tensor peek works") { + EvalCtx ctx(SimpleTensorEngine::ref()); + size_t a_id = ctx.add_tensor(ctx.make_double(1.0)); + Value::UP my_const = ctx.make_vector({1.0, 2.0, 3.0}, "x", true); + const auto &a = inject(ValueType::from_spec("double"), a_id, ctx.stash); + const auto &t = const_value(*my_const, ctx.stash); + const auto &fun = peek(t, {{"x", a}}, ctx.stash); + EXPECT_TRUE(fun.result_is_mutable()); + EXPECT_TRUE(fun.result_type().is_double()); + const auto &prog = ctx.compile(fun); + const Value &result = ctx.eval(prog); + EXPECT_TRUE(result.is_double()); + EXPECT_EQUAL(2.0, result.as_double()); +} + TEST("require that tensor rename works") { EvalCtx ctx(SimpleTensorEngine::ref()); size_t a_id = ctx.add_tensor(ctx.make_tensor_matrix()); @@ -365,20 +431,20 @@ TEST("require that push_children works") { TEST("require that tensor function can be dumped for debugging") { Stash stash; - auto my_value_1 = stash.create<DoubleValue>(5.0); - auto my_value_2 = stash.create<DoubleValue>(1.0); + auto my_value_1 = stash.create<DoubleValue>(1.0); + auto my_value_2 = stash.create<DoubleValue>(2.0); //------------------------------------------------------------------------- const auto &x5 = inject(ValueType::from_spec("tensor(x[5])"), 0, stash); const auto &mapped_x5 = map(x5, operation::Relu::f, stash); const auto &const_1 = const_value(my_value_1, stash); const auto &joined_x5 = join(mapped_x5, const_1, operation::Mul::f, stash); //------------------------------------------------------------------------- - const auto &x2_0 = const_value(my_value_1, stash); - const auto &x2_1 = const_value(my_value_2, stash); + const auto &peek1 = peek(x5, {{"x", const_1}}, stash); + const auto &peek2 = peek(x5, {{"x", size_t(2)}}, stash); const auto &x2 = create(ValueType::from_spec("tensor(x[2])"), { - {{{"x", 0}}, x2_0}, - {{{"x", 1}}, x2_1} + {{{"x", 0}}, peek1}, + {{{"x", 1}}, peek2} }, stash); const auto &a3y10 = inject(ValueType::from_spec("tensor(a[3],y[10])"), 2, stash); const auto &a3 = reduce(a3y10, Aggr::SUM, {"y"}, stash); diff --git a/eval/src/vespa/eval/eval/function.cpp b/eval/src/vespa/eval/eval/function.cpp index 44bb61abe17..56899debc26 100644 --- a/eval/src/vespa/eval/eval/function.cpp +++ b/eval/src/vespa/eval/eval/function.cpp @@ -734,6 +734,37 @@ bool maybe_parse_tensor_generator(ParseContext &ctx) { return true; } +// tensor_value <-(bind)- '{d1:1,d2:foo,d3:(a+b)}' +void parse_tensor_peek(ParseContext &ctx) { + ctx.skip_spaces(); + ctx.eat('{'); + nodes::TensorPeek::Spec peek_spec; + CommaTracker list; + while (!ctx.find_list_end()) { + list.maybe_eat_comma(ctx); + auto dim_name = get_ident(ctx, false); + ctx.skip_spaces(); + ctx.eat(':'); + if (!ctx.failed()) { + ParseContext::InputMark before_label = ctx.get_input_mark(); + auto label = get_ident(ctx, false); + bool verbatim = !ctx.failed() && ctx.find_expression_end(); + if (verbatim) { + peek_spec.emplace(dim_name, label); + } else { + ctx.restore_input_mark(before_label); + peek_spec.emplace(dim_name, get_expression(ctx)); + } + } + } + ctx.eat('}'); + if (peek_spec.empty()) { + return ctx.fail("empty peek spec"); + } + auto peek = std::make_unique<nodes::TensorPeek>(ctx.pop_expression(), std::move(peek_spec)); + ctx.push_expression(std::move(peek)); +} + void parse_tensor_concat(ParseContext &ctx) { Node_UP lhs = get_expression(ctx); ctx.eat(','); @@ -858,6 +889,9 @@ bool parse_operator(ParseContext &ctx) { if (op.get() != nullptr) { ctx.push_operator(std::move(op)); ctx.skip(str.size()); + } else if (ctx.get() == '{') { + parse_tensor_peek(ctx); + expect_value = false; } else { vespalib::string ident = get_ident(ctx, true); if (ident == "in") { diff --git a/eval/src/vespa/eval/eval/interpreted_function.cpp b/eval/src/vespa/eval/eval/interpreted_function.cpp index 208b2db4c3a..5575f4ecb1c 100644 --- a/eval/src/vespa/eval/eval/interpreted_function.cpp +++ b/eval/src/vespa/eval/eval/interpreted_function.cpp @@ -6,6 +6,7 @@ #include "check_type.h" #include "tensor_spec.h" #include "operation.h" +#include "tensor_nodes.h" #include "tensor_engine.h" #include <vespa/vespalib/util/classname.h> #include <vespa/eval/eval/llvm/compile_cache.h> @@ -21,13 +22,13 @@ namespace eval { namespace { const Function *get_lambda(const nodes::Node &node) { - if (auto ptr = as<nodes::TensorMap>(node)) { + if (auto ptr = nodes::as<nodes::TensorMap>(node)) { return &ptr->lambda(); } - if (auto ptr = as<nodes::TensorJoin>(node)) { + if (auto ptr = nodes::as<nodes::TensorJoin>(node)) { return &ptr->lambda(); } - if (auto ptr = as<nodes::TensorLambda>(node)) { + if (auto ptr = nodes::as<nodes::TensorLambda>(node)) { return &ptr->lambda(); } return nullptr; diff --git a/eval/src/vespa/eval/eval/key_gen.cpp b/eval/src/vespa/eval/eval/key_gen.cpp index 63cd39b5856..f107ba9e59b 100644 --- a/eval/src/vespa/eval/eval/key_gen.cpp +++ b/eval/src/vespa/eval/eval/key_gen.cpp @@ -42,6 +42,7 @@ struct KeyGen : public NodeVisitor, public NodeTraverser { void visit(const TensorLambda &) override { add_byte(15); } // type/lambda should be part of key void visit(const TensorConcat &) override { add_byte(16); } // dimension should be part of key void visit(const TensorCreate &) override { add_byte(17); } // type should be part of key + void visit(const TensorPeek &) override { add_byte(18); } // addr should be part of key void visit(const Add &) override { add_byte(20); } void visit(const Sub &) override { add_byte(21); } void visit(const Mul &) override { add_byte(22); } diff --git a/eval/src/vespa/eval/eval/llvm/compiled_function.cpp b/eval/src/vespa/eval/eval/llvm/compiled_function.cpp index 1ebf64ad269..ba5b8e99254 100644 --- a/eval/src/vespa/eval/eval/llvm/compiled_function.cpp +++ b/eval/src/vespa/eval/eval/llvm/compiled_function.cpp @@ -132,7 +132,8 @@ CompiledFunction::detect_issues(const Function &function) nodes::TensorRename, nodes::TensorLambda, nodes::TensorConcat, - nodes::TensorCreate>(node)) + nodes::TensorCreate, + nodes::TensorPeek>(node)) { issues.push_back(make_string("unsupported node type: %s", getClassName(node).c_str())); diff --git a/eval/src/vespa/eval/eval/llvm/llvm_wrapper.cpp b/eval/src/vespa/eval/eval/llvm/llvm_wrapper.cpp index 083e5d8593c..f62a7aeb7b1 100644 --- a/eval/src/vespa/eval/eval/llvm/llvm_wrapper.cpp +++ b/eval/src/vespa/eval/eval/llvm/llvm_wrapper.cpp @@ -482,6 +482,9 @@ struct FunctionBuilder : public NodeVisitor, public NodeTraverser { void visit(const TensorCreate &node) override { make_error(node.num_children()); } + void visit(const TensorPeek &node) override { + make_error(node.num_children()); + } // operator nodes diff --git a/eval/src/vespa/eval/eval/make_tensor_function.cpp b/eval/src/vespa/eval/eval/make_tensor_function.cpp index f0afb34376d..3b74a7a0e23 100644 --- a/eval/src/vespa/eval/eval/make_tensor_function.cpp +++ b/eval/src/vespa/eval/eval/make_tensor_function.cpp @@ -107,6 +107,28 @@ struct TensorFunctionBuilder : public NodeVisitor, public NodeTraverser { stack.push_back(tensor_function::create(node.type(), spec, stash)); } + void make_peek(const TensorPeek &node) { + assert(stack.size() >= node.num_children()); + const tensor_function::Node ¶m = stack[stack.size()-node.num_children()]; + std::map<vespalib::string, std::variant<TensorSpec::Label, tensor_function::Node::CREF>> spec; + for (auto pos = node.dim_list().rbegin(); pos != node.dim_list().rend(); ++pos) { + if (pos->second.is_expr()) { + spec.emplace(pos->first, stack.back()); + stack.pop_back(); + } else { + size_t dim_idx = param.result_type().dimension_index(pos->first); + assert(dim_idx != ValueType::Dimension::npos); + const auto ¶m_dim = param.result_type().dimensions()[dim_idx]; + if (param_dim.is_mapped()) { + spec.emplace(pos->first, pos->second.label); + } else { + spec.emplace(pos->first, as_number(pos->second.label)); + } + } + } + stack.back() = tensor_function::peek(param, spec, stash); + } + void make_rename(const Node &, const std::vector<vespalib::string> &from, const std::vector<vespalib::string> &to) { assert(stack.size() >= 1); const auto &a = stack.back(); @@ -197,6 +219,9 @@ struct TensorFunctionBuilder : public NodeVisitor, public NodeTraverser { void visit(const TensorCreate &node) override { make_create(node); } + void visit(const TensorPeek &node) override { + make_peek(node); + } void visit(const Add &node) override { make_join(node, operation::Add::f); } diff --git a/eval/src/vespa/eval/eval/node_types.cpp b/eval/src/vespa/eval/eval/node_types.cpp index f802a9ab03f..3e8f06c0941 100644 --- a/eval/src/vespa/eval/eval/node_types.cpp +++ b/eval/src/vespa/eval/eval/node_types.cpp @@ -13,32 +13,25 @@ class State private: const std::vector<ValueType> &_params; std::map<const Node *, ValueType> &_type_map; - std::vector<ValueType> _types; public: State(const std::vector<ValueType> ¶ms, std::map<const Node *, ValueType> &type_map) - : _params(params), _type_map(type_map), _types() {} + : _params(params), _type_map(type_map) {} const ValueType ¶m_type(size_t idx) { assert(idx < _params.size()); return _params[idx]; } - const ValueType &peek(size_t ridx) const { - assert(_types.size() > ridx); - return _types[_types.size() - 1 - ridx]; - } - void bind(size_t prune_cnt, const ValueType &type_ref, const Node &node) { - ValueType type = type_ref; // need copy since type_ref might be inside _types - assert(_types.size() >= prune_cnt); - for (size_t i = 0; i < prune_cnt; ++i) { - _types.pop_back(); - } - _types.push_back(type); + void bind(const ValueType &type, const Node &node) { + auto pos = _type_map.find(&node); + assert(pos == _type_map.end()); _type_map.emplace(&node, type); } - void assert_valid_end_state() const { - assert(_types.size() == 1); + const ValueType &type(const Node &node) { + auto pos = _type_map.find(&node); + assert(pos != _type_map.end()); + return pos->second; } }; @@ -48,22 +41,24 @@ struct TypeResolver : public NodeVisitor, public NodeTraverser { std::map<const Node *, ValueType> &type_map_out); ~TypeResolver(); - //------------------------------------------------------------------------- - - void assert_valid_end_state() const { - state.assert_valid_end_state(); + const ValueType ¶m_type(size_t idx) { + return state.param_type(idx); } - //------------------------------------------------------------------------- + void bind(const ValueType &type, const Node &node) { + state.bind(type, node); + } - void bind_type(const ValueType &type, const Node &node) { - state.bind(node.num_children(), type, node); + const ValueType &type(const Node &node) { + return state.type(node); } + //------------------------------------------------------------------------- + bool check_error(const Node &node) { for (size_t i = 0; i < node.num_children(); ++i) { - if (state.peek(i).is_error()) { - bind_type(ValueType::error_type(), node); + if (type(node.get_child(i)).is_error()) { + bind(ValueType::error_type(), node); return true; } } @@ -71,58 +66,87 @@ struct TypeResolver : public NodeVisitor, public NodeTraverser { } void resolve_op1(const Node &node) { - bind_type(state.peek(0), node); + bind(type(node.get_child(0)), node); } void resolve_op2(const Node &node) { - bind_type(ValueType::join(state.peek(1), state.peek(0)), node); + bind(ValueType::join(type(node.get_child(0)), + type(node.get_child(1))), node); } //------------------------------------------------------------------------- void visit(const Number &node) override { - bind_type(ValueType::double_type(), node); + bind(ValueType::double_type(), node); } void visit(const Symbol &node) override { - bind_type(state.param_type(node.id()), node); + bind(param_type(node.id()), node); } void visit(const String &node) override { - bind_type(ValueType::double_type(), node); + bind(ValueType::double_type(), node); } void visit(const In &node) override { resolve_op1(node); } void visit(const Neg &node) override { resolve_op1(node); } void visit(const Not &node) override { resolve_op1(node); } void visit(const If &node) override { - bind_type(ValueType::either(state.peek(1), state.peek(0)), node); + bind(ValueType::either(type(node.true_expr()), + type(node.false_expr())), node); } void visit(const Error &node) override { - bind_type(ValueType::error_type(), node); + bind(ValueType::error_type(), node); } void visit(const TensorMap &node) override { resolve_op1(node); } void visit(const TensorJoin &node) override { resolve_op2(node); } void visit(const TensorReduce &node) override { - const ValueType &child = state.peek(0); - bind_type(child.reduce(node.dimensions()), node); + const ValueType &child = type(node.get_child(0)); + bind(child.reduce(node.dimensions()), node); } void visit(const TensorRename &node) override { - const ValueType &child = state.peek(0); - bind_type(child.rename(node.from(), node.to()), node); + const ValueType &child = type(node.get_child(0)); + bind(child.rename(node.from(), node.to()), node); } void visit(const TensorLambda &node) override { - bind_type(node.type(), node); + bind(node.type(), node); } void visit(const TensorConcat &node) override { - bind_type(ValueType::concat(state.peek(1), state.peek(0), node.dimension()), node); + bind(ValueType::concat(type(node.get_child(0)), + type(node.get_child(1)), node.dimension()), node); } void visit(const TensorCreate &node) override { for (size_t i = 0; i < node.num_children(); ++i) { - if (!state.peek(i).is_double()) { - return bind_type(ValueType::error_type(), node); + if (!type(node.get_child(i)).is_double()) { + return bind(ValueType::error_type(), node); } } - bind_type(node.type(), node); + bind(node.type(), node); + } + void visit(const TensorPeek &node) override { + const ValueType ¶m_type = type(node.param()); + std::vector<vespalib::string> dimensions; + for (const auto &dim: node.dim_list()) { + dimensions.push_back(dim.first); + if (dim.second.is_expr()) { + if (!type(*dim.second.expr).is_double()) { + return bind(ValueType::error_type(), node); + } + } else { + size_t dim_idx = param_type.dimension_index(dim.first); + if (dim_idx == ValueType::Dimension::npos) { + return bind(ValueType::error_type(), node); + } + const auto ¶m_dim = param_type.dimensions()[dim_idx]; + if (param_dim.is_indexed()) { + if (!is_number(dim.second.label)) { + return bind(ValueType::error_type(), node); + } + if (as_number(dim.second.label) >= param_dim.size) { + return bind(ValueType::error_type(), node); + } + } + } + } + bind(param_type.reduce(dimensions), node); } - void visit(const Add &node) override { resolve_op2(node); } void visit(const Sub &node) override { resolve_op2(node); } void visit(const Mul &node) override { resolve_op2(node); } @@ -202,7 +226,6 @@ NodeTypes::NodeTypes(const Function &function, const std::vector<ValueType> &inp assert(input_types.size() == function.num_params()); nodes::TypeResolver resolver(input_types, _type_map); function.root().traverse(resolver); - resolver.assert_valid_end_state(); } const ValueType & diff --git a/eval/src/vespa/eval/eval/node_visitor.h b/eval/src/vespa/eval/eval/node_visitor.h index d0996f370c2..bca4e00d72e 100644 --- a/eval/src/vespa/eval/eval/node_visitor.h +++ b/eval/src/vespa/eval/eval/node_visitor.h @@ -36,6 +36,7 @@ struct NodeVisitor { virtual void visit(const nodes::TensorLambda &) = 0; virtual void visit(const nodes::TensorConcat &) = 0; virtual void visit(const nodes::TensorCreate &) = 0; + virtual void visit(const nodes::TensorPeek &) = 0; // operator nodes virtual void visit(const nodes::Add &) = 0; @@ -105,6 +106,7 @@ struct EmptyNodeVisitor : NodeVisitor { void visit(const nodes::TensorLambda &) override {} void visit(const nodes::TensorConcat &) override {} void visit(const nodes::TensorCreate &) override {} + void visit(const nodes::TensorPeek &) override {} void visit(const nodes::Add &) override {} void visit(const nodes::Sub &) override {} void visit(const nodes::Mul &) override {} diff --git a/eval/src/vespa/eval/eval/simple_tensor_engine.h b/eval/src/vespa/eval/eval/simple_tensor_engine.h index 4cfd389dfa9..645ec4c4be7 100644 --- a/eval/src/vespa/eval/eval/simple_tensor_engine.h +++ b/eval/src/vespa/eval/eval/simple_tensor_engine.h @@ -20,10 +20,10 @@ public: static const TensorEngine &ref() { return _engine; }; TensorSpec to_spec(const Value &value) const override; - Value::UP from_spec(const TensorSpec &spec) const override; + std::unique_ptr<Value> from_spec(const TensorSpec &spec) const override; void encode(const Value &value, nbostream &output) const override; - Value::UP decode(nbostream &input) const override; + std::unique_ptr<Value> decode(nbostream &input) const override; const Value &map(const Value &a, map_fun_t function, Stash &stash) const override; const Value &join(const Value &a, const Value &b, join_fun_t function, Stash &stash) const override; diff --git a/eval/src/vespa/eval/eval/string_stuff.cpp b/eval/src/vespa/eval/eval/string_stuff.cpp index 782aacb5cea..1fd8649bb57 100644 --- a/eval/src/vespa/eval/eval/string_stuff.cpp +++ b/eval/src/vespa/eval/eval/string_stuff.cpp @@ -5,6 +5,19 @@ namespace vespalib::eval { +bool is_number(const vespalib::string &str) { + for (char c: str) { + if (!isdigit(c)) { + return false; + } + } + return true; +} + +size_t as_number(const vespalib::string &str) { + return atoi(str.c_str()); +} + vespalib::string as_string(const TensorSpec::Address &address) { CommaTracker label_list; vespalib::string str = "{"; diff --git a/eval/src/vespa/eval/eval/string_stuff.h b/eval/src/vespa/eval/eval/string_stuff.h index 820196fe959..68b45b0aa38 100644 --- a/eval/src/vespa/eval/eval/string_stuff.h +++ b/eval/src/vespa/eval/eval/string_stuff.h @@ -38,6 +38,16 @@ struct CommaTracker { }; /** + * Is this string a positive integer (dimension index) + **/ +bool is_number(const vespalib::string &str); + +/** + * Convert this string to a positive integer (dimension index) + **/ +size_t as_number(const vespalib::string &str); + +/** * Convert a tensor spec address into a string on the form: * '{dim1:label,dim2:index, ...}' **/ diff --git a/eval/src/vespa/eval/eval/tensor_engine.h b/eval/src/vespa/eval/eval/tensor_engine.h index d9f6d35609a..b2b9faedadb 100644 --- a/eval/src/vespa/eval/eval/tensor_engine.h +++ b/eval/src/vespa/eval/eval/tensor_engine.h @@ -2,8 +2,6 @@ #pragma once -#include "value_type.h" -#include "tensor_function.h" #include "aggr.h" #include <vespa/vespalib/stllike/string.h> #include <memory> @@ -18,8 +16,9 @@ class nbostream; namespace eval { struct Value; -class Tensor; +class ValueType; class TensorSpec; +class TensorFunction; /** * Top-level API for a tensor implementation. All Tensor operations @@ -33,7 +32,6 @@ class TensorSpec; struct TensorEngine { using Aggr = eval::Aggr; - using Tensor = eval::Tensor; using TensorFunction = eval::TensorFunction; using TensorSpec = eval::TensorSpec; using Value = eval::Value; @@ -42,10 +40,10 @@ struct TensorEngine using map_fun_t = double (*)(double); virtual TensorSpec to_spec(const Value &value) const = 0; - virtual Value::UP from_spec(const TensorSpec &spec) const = 0; + virtual std::unique_ptr<Value> from_spec(const TensorSpec &spec) const = 0; virtual void encode(const Value &value, nbostream &output) const = 0; - virtual Value::UP decode(nbostream &input) const = 0; + virtual std::unique_ptr<Value> decode(nbostream &input) const = 0; virtual const TensorFunction &optimize(const TensorFunction &expr, Stash &) const { return expr; } diff --git a/eval/src/vespa/eval/eval/tensor_function.cpp b/eval/src/vespa/eval/eval/tensor_function.cpp index 951b9772505..1f87c0390af 100644 --- a/eval/src/vespa/eval/eval/tensor_function.cpp +++ b/eval/src/vespa/eval/eval/tensor_function.cpp @@ -129,6 +129,63 @@ void op_tensor_create(State &state, uint64_t param) { state.pop_n_push(i, result); } +const Value &extract_single_value(const TensorSpec &spec, const TensorSpec::Address &addr, State &state) { + auto pos = spec.cells().find(addr); + if (pos == spec.cells().end()) { + return state.stash.create<DoubleValue>(0.0); + } + return state.stash.create<DoubleValue>(pos->second); +} + +const Value &extract_tensor_subspace(const ValueType &my_type, const TensorSpec &spec, const TensorSpec::Address &addr, State &state) { + TensorSpec my_spec(my_type.to_spec()); + for (const auto &cell: spec.cells()) { + bool keep = true; + TensorSpec::Address my_addr; + for (const auto &binding: cell.first) { + auto pos = addr.find(binding.first); + if (pos == addr.end()) { + my_addr.emplace(binding.first, binding.second); + } else { + if (!(pos->second == binding.second)) { + keep = false; + } + } + } + if (keep) { + my_spec.add(my_addr, cell.second); + } + } + return *state.stash.create<Value::UP>(state.engine.from_spec(my_spec)); +} + +void op_tensor_peek(State &state, uint64_t param) { + const Peek &self = unwrap_param<Peek>(param); + TensorSpec::Address addr; + size_t child_cnt = 0; + for (auto pos = self.spec().rbegin(); pos != self.spec().rend(); ++pos) { + if (std::holds_alternative<TensorSpec::Label>(pos->second)) { + addr.emplace(pos->first, std::get<TensorSpec::Label>(pos->second)); + } else { + assert(std::holds_alternative<TensorFunction::Child>(pos->second)); + size_t index(state.peek(child_cnt++).as_double()); + size_t dim_idx = self.param_type().dimension_index(pos->first); + assert(dim_idx != ValueType::Dimension::npos); + const auto ¶m_dim = self.param_type().dimensions()[dim_idx]; + if (param_dim.is_mapped()) { + addr.emplace(pos->first, vespalib::make_string("%zu", index)); + } else { + addr.emplace(pos->first, index); + } + } + } + TensorSpec spec = state.engine.to_spec(state.peek(child_cnt++)); + const Value &result = self.result_type().is_double() + ? extract_single_value(spec, addr, state) + : extract_tensor_subspace(self.result_type(), spec, addr, state); + state.pop_n_push(child_cnt, result); +} + } // namespace vespalib::eval::tensor_function //----------------------------------------------------------------------------- @@ -302,6 +359,44 @@ Create::visit_children(vespalib::ObjectVisitor &visitor) const //----------------------------------------------------------------------------- +void +Peek::push_children(std::vector<Child::CREF> &children) const +{ + children.emplace_back(_param); + for (const auto &dim: _spec) { + if (std::holds_alternative<Child>(dim.second)) { + children.emplace_back(std::get<Child>(dim.second)); + } + } +} + +Instruction +Peek::compile_self(Stash &) const +{ + return Instruction(op_tensor_peek, wrap_param<Peek>(*this)); +} + +void +Peek::visit_children(vespalib::ObjectVisitor &visitor) const +{ + ::visit(visitor, "param", _param.get()); + for (const auto &dim: _spec) { + if (std::holds_alternative<TensorSpec::Label>(dim.second)) { + const auto &label = std::get<TensorSpec::Label>(dim.second); + if (label.is_mapped()) { + ::visit(visitor, dim.first, label.name); + } else { + ::visit(visitor, dim.first, label.index); + } + } else { + assert(std::holds_alternative<Child>(dim.second)); + ::visit(visitor, dim.first, std::get<Child>(dim.second).get()); + } + } +} + +//----------------------------------------------------------------------------- + Instruction Rename::compile_self(Stash &stash) const { @@ -376,6 +471,15 @@ const Node &create(const ValueType &type, const std::map<TensorSpec::Address,Nod return stash.create<Create>(type, spec); } +const Node &peek(const Node ¶m, const std::map<vespalib::string, std::variant<TensorSpec::Label, Node::CREF>> &spec, Stash &stash) { + std::vector<vespalib::string> dimensions; + for (const auto &dim_spec: spec) { + dimensions.push_back(dim_spec.first); + } + ValueType result_type = param.result_type().reduce(dimensions); + return stash.create<Peek>(result_type, param, spec); +} + const Node &rename(const Node &child, const std::vector<vespalib::string> &from, const std::vector<vespalib::string> &to, Stash &stash) { ValueType result_type = child.result_type().rename(from, to); return stash.create<Rename>(result_type, child, from, to); diff --git a/eval/src/vespa/eval/eval/tensor_function.h b/eval/src/vespa/eval/eval/tensor_function.h index f82f1c80f0f..7c7e8318305 100644 --- a/eval/src/vespa/eval/eval/tensor_function.h +++ b/eval/src/vespa/eval/eval/tensor_function.h @@ -4,6 +4,7 @@ #include <memory> #include <vector> +#include <variant> #include <vespa/vespalib/stllike/asciistream.h> #include <vespa/vespalib/stllike/string.h> #include <vespa/vespalib/util/arrayref.h> @@ -295,6 +296,38 @@ public: //----------------------------------------------------------------------------- +class Peek : public Node +{ + using Super = Node; +public: + using MyLabel = std::variant<TensorSpec::Label, Child>; +private: + Child _param; + std::map<vespalib::string, MyLabel> _spec; +public: + Peek(const ValueType &result_type_in, const Node ¶m, + const std::map<vespalib::string, std::variant<TensorSpec::Label, Node::CREF>> &spec) + : Node(result_type_in), _param(param), _spec() + { + for (const auto &dim: spec) { + if (std::holds_alternative<TensorSpec::Label>(dim.second)) { + _spec.emplace(dim.first, std::get<TensorSpec::Label>(dim.second)); + } else { + assert(std::holds_alternative<Node::CREF>(dim.second)); + _spec.emplace(dim.first, std::get<Node::CREF>(dim.second).get()); + } + } + } + const std::map<vespalib::string, MyLabel> &spec() const { return _spec; } + const ValueType ¶m_type() const { return _param.get().result_type(); } + bool result_is_mutable() const override { return true; } + InterpretedFunction::Instruction compile_self(Stash &stash) const final override; + void push_children(std::vector<Child::CREF> &children) const final override; + void visit_children(vespalib::ObjectVisitor &visitor) const final override; +}; + +//----------------------------------------------------------------------------- + class Rename : public Op1 { using Super = Op1; @@ -349,6 +382,7 @@ const Node &map(const Node &child, map_fun_t function, Stash &stash); const Node &join(const Node &lhs, const Node &rhs, join_fun_t function, Stash &stash); const Node &concat(const Node &lhs, const Node &rhs, const vespalib::string &dimension, Stash &stash); const Node &create(const ValueType &type, const std::map<TensorSpec::Address, Node::CREF> &spec, Stash &stash); +const Node &peek(const Node ¶m, const std::map<vespalib::string, std::variant<TensorSpec::Label, Node::CREF>> &spec, Stash &stash); const Node &rename(const Node &child, const std::vector<vespalib::string> &from, const std::vector<vespalib::string> &to, Stash &stash); const Node &if_node(const Node &cond, const Node &true_child, const Node &false_child, Stash &stash); diff --git a/eval/src/vespa/eval/eval/tensor_nodes.cpp b/eval/src/vespa/eval/eval/tensor_nodes.cpp index 96e8fbef6a3..cee7a1aeb11 100644 --- a/eval/src/vespa/eval/eval/tensor_nodes.cpp +++ b/eval/src/vespa/eval/eval/tensor_nodes.cpp @@ -14,6 +14,7 @@ void TensorRename::accept(NodeVisitor &visitor) const { visitor.visit(*this); } void TensorLambda::accept(NodeVisitor &visitor) const { visitor.visit(*this); } void TensorConcat::accept(NodeVisitor &visitor) const { visitor.visit(*this); } void TensorCreate::accept(NodeVisitor &visitor) const { visitor.visit(*this); } +void TensorPeek ::accept(NodeVisitor &visitor) const { visitor.visit(*this); } } // namespace vespalib::eval::nodes } // namespace vespalib::eval diff --git a/eval/src/vespa/eval/eval/tensor_nodes.h b/eval/src/vespa/eval/eval/tensor_nodes.h index 93c1da4f2a8..a471f027675 100644 --- a/eval/src/vespa/eval/eval/tensor_nodes.h +++ b/eval/src/vespa/eval/eval/tensor_nodes.h @@ -259,6 +259,80 @@ public: } }; +class TensorPeek : public Node { +public: + struct MyLabel { + vespalib::string label; + Node_UP expr; + MyLabel(vespalib::string label_in) + : label(label_in), expr() {} + MyLabel(Node_UP node) + : label(""), expr(std::move(node)) {} + bool is_expr() const { return bool(expr); } + }; + using Spec = std::map<vespalib::string, MyLabel>; + using Dim = std::pair<vespalib::string, MyLabel>; + using DimList = std::vector<Dim>; +private: + using DimRefs = std::vector<size_t>; + Node_UP _param; + DimList _dim_list; + DimRefs _expr_dims; +public: + TensorPeek(Node_UP param, Spec spec) + : _param(std::move(param)), _dim_list(), _expr_dims() + { + for (auto &dim: spec) { + if (dim.second.is_expr()) { + _expr_dims.push_back(_dim_list.size()); + } + _dim_list.emplace_back(dim.first, std::move(dim.second)); + } + } + const Node ¶m() const { return *_param; } + const DimList &dim_list() const { return _dim_list; } + vespalib::string dump(DumpContext &ctx) const override { + vespalib::string str = _param->dump(ctx); + str += "{"; + CommaTracker dim_list; + for (const auto &dim: _dim_list) { + dim_list.maybe_add_comma(str); + str += dim.first; + str += ":"; + if (dim.second.is_expr()) { + vespalib::string expr = dim.second.expr->dump(ctx); + if (starts_with(expr, "(")) { + str += expr; + } else { + str += "("; + str += expr; + str += ")"; + } + } else { + str += dim.second.label; + } + } + str += "}"; + return str; + } + void accept(NodeVisitor &visitor) const override ; + size_t num_children() const override { return (1 + _expr_dims.size()); } + const Node &get_child(size_t idx) const override { + assert(idx < num_children()); + if (idx == 0) { + return *_param; + } else { + return *_dim_list[_expr_dims[idx-1]].second.expr; + } + } + void detach_children(NodeHandler &handler) override { + handler.handle(std::move(_param)); + for (size_t idx: _expr_dims) { + handler.handle(std::move(_dim_list[idx].second.expr)); + } + } +}; + } // namespace vespalib::eval::nodes } // namespace vespalib::eval } // namespace vespalib diff --git a/eval/src/vespa/eval/eval/test/eval_spec.cpp b/eval/src/vespa/eval/eval/test/eval_spec.cpp index f89cf5edd0d..dc1e3405a6b 100644 --- a/eval/src/vespa/eval/eval/test/eval_spec.cpp +++ b/eval/src/vespa/eval/eval/test/eval_spec.cpp @@ -178,6 +178,7 @@ EvalSpec::add_tensor_operation_cases() { add_expression({"a","b"}, "concat(a,b,x)"); add_expression({"a","b"}, "concat(a,b,y)"); add_expression({}, "tensor(x[3]):{{x:0}:0,{x:1}:1,{x:2}:2}"); + add_expression({"a"}, "a{x:3}"); } void diff --git a/eval/src/vespa/eval/eval/test/tensor_conformance.cpp b/eval/src/vespa/eval/eval/test/tensor_conformance.cpp index b5426d9ca29..4eb13a1964c 100644 --- a/eval/src/vespa/eval/eval/test/tensor_conformance.cpp +++ b/eval/src/vespa/eval/eval/test/tensor_conformance.cpp @@ -810,6 +810,25 @@ struct TestContext { //------------------------------------------------------------------------- + void test_tensor_peek(const vespalib::string &expr, const TensorSpec ¶m, const TensorSpec &expect) { + TEST_DO(verify_result(Expr_TT(expr).eval(engine, param, spec(1.0)), expect)); + } + + void test_tensor_peek() { + auto param_double = spec({x({"0", "1"}),y(2)}, Seq({1.0, 2.0, 3.0, 4.0})); + auto param_float = spec(float_cells({x({"0", "1"}),y(2)}), Seq({1.0, 2.0, 3.0, 4.0})); + TEST_DO(test_tensor_peek("tensor(x[2]):[a{x:1,y:1},a{x:b-1,y:b-1}]", param_double, spec(x(2), Seq({4.0, 1.0})))); + TEST_DO(test_tensor_peek("tensor(x[2]):[a{x:1,y:1},a{x:b-1,y:b-1}]", param_float, spec(x(2), Seq({4.0, 1.0})))); + TEST_DO(test_tensor_peek("tensor<float>(x[2]):[a{x:1,y:1},a{x:b-1,y:b-1}]", param_double, spec(float_cells({x(2)}), Seq({4.0, 1.0})))); + TEST_DO(test_tensor_peek("tensor<float>(x[2]):[a{x:1,y:1},a{x:b-1,y:b-1}]", param_float, spec(float_cells({x(2)}), Seq({4.0, 1.0})))); + TEST_DO(test_tensor_peek("a{x:(b)}", param_double, spec(y(2), Seq({3.0, 4.0})))); + TEST_DO(test_tensor_peek("a{x:(b)}", param_float, spec(float_cells({y(2)}), Seq({3.0, 4.0})))); + TEST_DO(test_tensor_peek("a{y:(b)}", param_double, spec(x({"0", "1"}), Seq({2.0, 4.0})))); + TEST_DO(test_tensor_peek("a{y:(b)}", param_float, spec(float_cells({x({"0", "1"})}), Seq({2.0, 4.0})))); + } + + //------------------------------------------------------------------------- + void verify_encode_decode(const TensorSpec &spec, const TensorEngine &encode_engine, const TensorEngine &decode_engine) @@ -893,6 +912,7 @@ struct TestContext { TEST_DO(test_rename()); TEST_DO(test_tensor_lambda()); TEST_DO(test_tensor_create()); + TEST_DO(test_tensor_peek()); TEST_DO(test_binary_format()); } }; diff --git a/eval/src/vespa/eval/eval/visit_stuff.h b/eval/src/vespa/eval/eval/visit_stuff.h index ef21352535d..29efff3b5ec 100644 --- a/eval/src/vespa/eval/eval/visit_stuff.h +++ b/eval/src/vespa/eval/eval/visit_stuff.h @@ -2,6 +2,7 @@ #pragma once +#include <vespa/vespalib/objects/visit.h> #include <vespa/vespalib/stllike/string.h> #include <vector> diff --git a/eval/src/vespa/eval/tensor/default_tensor_engine.h b/eval/src/vespa/eval/tensor/default_tensor_engine.h index b7a9e4d43e7..29f9811e170 100644 --- a/eval/src/vespa/eval/tensor/default_tensor_engine.h +++ b/eval/src/vespa/eval/tensor/default_tensor_engine.h @@ -19,10 +19,10 @@ public: static const TensorEngine &ref() { return _engine; }; TensorSpec to_spec(const Value &value) const override; - Value::UP from_spec(const TensorSpec &spec) const override; + std::unique_ptr<Value> from_spec(const TensorSpec &spec) const override; void encode(const Value &value, nbostream &output) const override; - Value::UP decode(nbostream &input) const override; + std::unique_ptr<Value> decode(nbostream &input) const override; const TensorFunction &optimize(const TensorFunction &expr, Stash &stash) const override; |