diff options
author | Håvard Pettersen <havardpe@oath.com> | 2019-11-05 14:16:38 +0000 |
---|---|---|
committer | Håvard Pettersen <havardpe@oath.com> | 2019-11-08 10:22:11 +0000 |
commit | c6418d267d1c6825ec0652a9b26178b7be527b9c (patch) | |
tree | 5f7b64ee60f9bc2af868fad937480d7fc14c51f3 /eval/src | |
parent | b1b3da2b97d27e8efb796f3469ccd0a574dffacd (diff) |
tensor create operation
Diffstat (limited to 'eval/src')
22 files changed, 472 insertions, 76 deletions
diff --git a/eval/src/tests/eval/function/function_test.cpp b/eval/src/tests/eval/function/function_test.cpp index c2561a86d6f..49793a62958 100644 --- a/eval/src/tests/eval/function/function_test.cpp +++ b/eval/src/tests/eval/function/function_test.cpp @@ -802,9 +802,9 @@ TEST("require that tensor rename dimension lists must have equal size") { //----------------------------------------------------------------------------- TEST("require that tensor lambda can be parsed") { - EXPECT_EQUAL("tensor(x[10])(x)", Function::parse({""}, "tensor(x[10])(x)").dump()); - EXPECT_EQUAL("tensor(x[10],y[10])(x==y)", Function::parse({""}, "tensor(x[10],y[10])(x==y)").dump()); - EXPECT_EQUAL("tensor(x[10],y[10])(x==y)", Function::parse({""}, " tensor ( x [ 10 ] , y [ 10 ] ) ( x == y ) ").dump()); + EXPECT_EQUAL("tensor(x[10])(x)", Function::parse({}, "tensor(x[10])(x)").dump()); + EXPECT_EQUAL("tensor(x[10],y[10])(x==y)", Function::parse({}, "tensor(x[10],y[10])(x==y)").dump()); + EXPECT_EQUAL("tensor(x[10],y[10])(x==y)", Function::parse({}, " tensor ( x [ 10 ] , y [ 10 ] ) ( x == y ) ").dump()); } TEST("require that tensor lambda requires appropriate tensor type") { @@ -819,6 +819,55 @@ TEST("require that tensor lambda can only use dimension names") { //----------------------------------------------------------------------------- +TEST("require that verbose tensor create can be parsed") { + auto dense = Function::parse("tensor(x[3]):{{x:0}:1,{x:1}:2,{x:2}:3}"); + auto sparse = Function::parse("tensor(x{}):{{x:a}:1,{x:b}:2,{x:c}:3}"); + auto mixed = Function::parse("tensor(x{},y[2]):{{x:a,y:0}:1,{x:a,y:1}:2}"); + EXPECT_EQUAL("tensor(x[3]):{{x:0}:1,{x:1}:2,{x:2}:3}", dense.dump()); + EXPECT_EQUAL("tensor(x{}):{{x:a}:1,{x:b}:2,{x:c}:3}", sparse.dump()); + EXPECT_EQUAL("tensor(x{},y[2]):{{x:a,y:0}:1,{x:a,y:1}:2}", mixed.dump()); +} + +TEST("require that verbose tensor create can contain expressions") { + auto fun = Function::parse("tensor(x[2]):{{x:0}:1,{x:1}:2+a}"); + EXPECT_EQUAL("tensor(x[2]):{{x:0}:1,{x:1}:(2+a)}", fun.dump()); + ASSERT_EQUAL(fun.num_params(), 1u); + EXPECT_EQUAL(fun.param_name(0), "a"); +} + +TEST("require that verbose tensor create handles spaces and reordering of various elements") { + auto fun = Function::parse(" tensor ( y [ 2 ] , x [ 2 ] ) : { { x : 0 , y : 1 } : 2 , " + "{ y : 0 , x : 0 } : 1 , { y : 0 , x : 1 } : 3 , { x : 1 , y : 1 } : 4 } "); + EXPECT_EQUAL("tensor(x[2],y[2]):{{x:0,y:0}:1,{x:0,y:1}:2,{x:1,y:0}:3,{x:1,y:1}:4}", fun.dump()); +} + +TEST("require that verbose tensor create detects invalid tensor type") { + TEST_DO(verify_error("tensor(x[,y}):ignored", + "[tensor(x[,y}):]...[invalid tensor type]...[ignored]")); +} + +TEST("require that verbose tensor create detects incomplete addresses") { + TEST_DO(verify_error("tensor(x[1],y[1]):{{x:0}:1}", + "[tensor(x[1],y[1]):{{x:0}]...[incomplete address: '{x:0}']...[:1}]")); +} + +TEST("require that verbose tensor create detects invalid dimension names") { + TEST_DO(verify_error("tensor(x[1]):{{y:0}:1}", + "[tensor(x[1]):{{y]...[invalid dimension name: 'y']...[:0}:1}]")); +} + +TEST("require that verbose tensor create detects out-of-bounds indexes for indexed dimensions") { + TEST_DO(verify_error("tensor(x[1]):{{x:1}:1}", + "[tensor(x[1]):{{x:1]...[dimension index too large: 1]...[}:1}]")); +} + +TEST("require that verbose tensor create detects non-numeric indexes for indexed dimensions") { + TEST_DO(verify_error("tensor(x[1]):{{x:foo}:1}", + "[tensor(x[1]):{{x:]...[expected number]...[foo}:1}]")); +} + +//----------------------------------------------------------------------------- + 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 256c7b85f72..5504bb33137 100644 --- a/eval/src/tests/eval/node_types/node_types_test.cpp +++ b/eval/src/tests/eval/node_types/node_types_test.cpp @@ -33,14 +33,18 @@ struct TypeSpecExtractor : public vespalib::eval::SymbolExtractor { } }; -void verify(const vespalib::string &type_expr_in, const vespalib::string &type_spec, bool replace = true) { +void verify(const vespalib::string &type_expr_in, const vespalib::string &type_spec, bool replace_first = true) { vespalib::string type_expr = type_expr_in; - if (replace) { - // replace 'tensor' with 'Tensor' in type expression, see hack above - for (size_t idx = type_expr.find("tensor"); - idx != type_expr.npos; - idx = type_expr.find("tensor")) - { + // replace 'tensor' with 'Tensor' in type expression, see hack above + size_t tensor_cnt = 0; + for (size_t idx = type_expr.find("tensor"); + idx != type_expr.npos; + idx = type_expr.find("tensor", idx + 1)) + { + // setting 'replace_first' to false will avoid replacing the + // first 'tensor' instance to let the parser handle it as an + // actual tensor generator. + if ((tensor_cnt++ > 0) || replace_first) { type_expr[idx] = 'T'; } } @@ -228,6 +232,17 @@ TEST("require that lambda tensor resolves correct type") { TEST_DO(verify("tensor<float>(x[5],y[10],z[15])(1.0)", "tensor<float>(x[5],y[10],z[15])", false)); } +TEST("require that tensor create resolves correct type") { + TEST_DO(verify("tensor(x[3]):{{x:0}:double,{x:1}:double,{x:2}:double}", "tensor(x[3])", false)); + TEST_DO(verify("tensor(x{}):{{x:a}:double,{x:b}:double,{x:c}:double}", "tensor(x{})", false)); + TEST_DO(verify("tensor(x{},y[2]):{{x:a,y:0}:double,{x:a,y:1}:double}", "tensor(x{},y[2])", false)); + TEST_DO(verify("tensor<float>(x[3]):{{x:0}:double,{x:1}:double,{x:2}:double}", "tensor<float>(x[3])", false)); + TEST_DO(verify("tensor(x[3]):{{x:0}:double+double,{x:1}:double-double,{x:2}:double/double}", "tensor(x[3])", false)); + TEST_DO(verify("tensor(x[3]):{{x:0}:double,{x:1}:reduce(tensor(x[2]),sum),{x:2}:double}", "tensor(x[3])", false)); + TEST_DO(verify("tensor(x[3]):{{x:0}:double,{x:1}:tensor(x[2]),{x:2}:double}", "error", false)); + TEST_DO(verify("tensor(x[3]):{{x:0}:double,{x:1}:error,{x:2}:double}", "error", false)); +} + 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 7bca3d14c28..c30438b551d 100644 --- a/eval/src/tests/eval/tensor_function/tensor_function_test.cpp +++ b/eval/src/tests/eval/tensor_function/tensor_function_test.cpp @@ -40,12 +40,22 @@ struct EvalCtx { const TensorFunction &compile(const tensor_function::Node &expr) { return engine.optimize(expr, stash); } + Value::UP make_double(double value) { + return engine.from_spec(TensorSpec("double").add({}, value)); + } Value::UP make_true() { return engine.from_spec(TensorSpec("double").add({}, 1.0)); } Value::UP make_false() { return engine.from_spec(TensorSpec("double").add({}, 0.0)); } + Value::UP make_simple_vector() { + return engine.from_spec( + TensorSpec("tensor(x[3])") + .add({{"x",0}}, 1) + .add({{"x",1}}, 2) + .add({{"x",2}}, 3)); + } Value::UP make_tensor_matrix_first_half() { return engine.from_spec( TensorSpec("tensor(x[2])") @@ -225,6 +235,28 @@ TEST("require that tensor concat works") { TEST_DO(verify_equal(*expect, ctx.eval(prog))); } +TEST("require that tensor create works") { + EvalCtx ctx(SimpleTensorEngine::ref()); + 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(); + 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); + const auto &fun = create(ValueType::from_spec("tensor(x[3])"), + { + {{{"x", 0}}, a}, + {{{"x", 1}}, b}, + {{{"x", 2}}, c} + }, + 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 rename works") { EvalCtx ctx(SimpleTensorEngine::ref()); size_t a_id = ctx.add_tensor(ctx.make_tensor_matrix()); @@ -341,7 +373,13 @@ TEST("require that tensor function can be dumped for debugging") { 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 = inject(ValueType::from_spec("tensor(x[2])"), 1, stash); + const auto &x2_0 = const_value(my_value_1, stash); + const auto &x2_1 = const_value(my_value_2, stash); + const auto &x2 = create(ValueType::from_spec("tensor(x[2])"), + { + {{{"x", 0}}, x2_0}, + {{{"x", 1}}, x2_1} + }, stash); const auto &a3y10 = inject(ValueType::from_spec("tensor(a[3],y[10])"), 2, stash); const auto &a3 = reduce(a3y10, Aggr::SUM, {"y"}, stash); const auto &x3 = rename(a3, {"a"}, {"x"}, stash); diff --git a/eval/src/vespa/eval/eval/CMakeLists.txt b/eval/src/vespa/eval/eval/CMakeLists.txt index 23aa504f31e..20a7141393f 100644 --- a/eval/src/vespa/eval/eval/CMakeLists.txt +++ b/eval/src/vespa/eval/eval/CMakeLists.txt @@ -19,6 +19,7 @@ vespa_add_library(eval_eval OBJECT param_usage.cpp simple_tensor.cpp simple_tensor_engine.cpp + string_stuff.cpp tensor.cpp tensor_engine.cpp tensor_function.cpp diff --git a/eval/src/vespa/eval/eval/basic_nodes.h b/eval/src/vespa/eval/eval/basic_nodes.h index 6af0a04b31a..01ac86e4472 100644 --- a/eval/src/vespa/eval/eval/basic_nodes.h +++ b/eval/src/vespa/eval/eval/basic_nodes.h @@ -2,6 +2,7 @@ #pragma once +#include "string_stuff.h" #include <vespa/vespalib/util/hdr_abort.h> #include <vespa/vespalib/stllike/string.h> #include <vespa/vespalib/util/stringfmt.h> @@ -86,22 +87,6 @@ struct Leaf : public Node { void detach_children(NodeHandler &) override {} }; -/** - * Helper class used to insert commas on the appropriate places in - * comma-separated textual lists. - **/ -struct CommaTracker { - bool first; - CommaTracker() : first(true) {} - void maybe_comma(vespalib::string &dst) { - if (first) { - first = false; - } else { - dst.push_back(','); - } - } -}; - class Number : public Leaf { private: double _value; diff --git a/eval/src/vespa/eval/eval/function.cpp b/eval/src/vespa/eval/eval/function.cpp index 65c70a5bd7d..1cdd0478181 100644 --- a/eval/src/vespa/eval/eval/function.cpp +++ b/eval/src/vespa/eval/eval/function.cpp @@ -269,6 +269,29 @@ public: size_t operator_mark() const { return _operator_mark; } void operator_mark(size_t mark) { _operator_mark = mark; } + bool find_list_end() { + skip_spaces(); + char c = get(); + return (c == /* eof */ '\0' || c == ')' || c == ']' || c == '}'); + } + + bool find_expression_end() { + return (find_list_end() || (get() == ',')); + } + + size_t init_expression() { + size_t old_mark = operator_mark(); + operator_mark(num_operators()); + return old_mark; + } + + void fini_expression(size_t old_mark) { + while (num_operators() > operator_mark()) { + apply_operator(); + } + operator_mark(old_mark); + } + void apply_until(const nodes::Operator &op) { while ((_operator_stack.size() > _operator_mark) && (_operator_stack.back()->do_before(op))) @@ -293,6 +316,11 @@ public: void parse_value(ParseContext &ctx); void parse_expression(ParseContext &ctx); +Node_UP get_expression(ParseContext &ctx) { + parse_expression(ctx); + return ctx.pop_expression(); +} + int unhex(char c) { if (c >= '0' && c <= '9') { return (c - '0'); @@ -403,15 +431,24 @@ vespalib::string get_ident(ParseContext &ctx, bool allow_empty) { return ident; } +size_t get_size_t(ParseContext &ctx) { + ctx.skip_spaces(); + vespalib::string num; + for (; isdigit(ctx.get()); ctx.next()) { + num.push_back(ctx.get()); + } + if (num.empty()) { + ctx.fail("expected number"); + } + return atoi(num.c_str()); +} + void parse_if(ParseContext &ctx) { - parse_expression(ctx); - Node_UP cond = ctx.pop_expression(); + Node_UP cond = get_expression(ctx); ctx.eat(','); - parse_expression(ctx); - Node_UP true_expr = ctx.pop_expression(); + Node_UP true_expr = get_expression(ctx); ctx.eat(','); - parse_expression(ctx); - Node_UP false_expr = ctx.pop_expression(); + Node_UP false_expr = get_expression(ctx); double p_true = 0.5; if (ctx.get() == ',') { ctx.eat(','); @@ -430,8 +467,7 @@ void parse_call(ParseContext &ctx, Call_UP call) { if (i > 0) { ctx.eat(','); } - parse_expression(ctx); - call->bind_next(ctx.pop_expression()); + call->bind_next(get_expression(ctx)); } ctx.push_expression(std::move(call)); } @@ -444,7 +480,7 @@ std::vector<vespalib::string> get_ident_list(ParseContext &ctx, bool wrapped) { ctx.skip_spaces(); ctx.eat('('); } - for (ctx.skip_spaces(); !ctx.eos() && (ctx.get() != ')'); ctx.skip_spaces()) { + while (!ctx.find_list_end()) { if (!list.empty() || !wrapped) { ctx.eat(','); } @@ -484,11 +520,10 @@ Function parse_lambda(ParseContext &ctx, size_t num_params) { ctx.push_resolve_context(params, nullptr); ctx.skip_spaces(); ctx.eat('('); - parse_expression(ctx); + Node_UP lambda_root = get_expression(ctx); ctx.eat(')'); ctx.skip_spaces(); ctx.pop_resolve_context(); - Node_UP lambda_root = ctx.pop_expression(); if (param_names.size() != num_params) { ctx.fail(make_string("expected lambda with %zu parameter(s), was %zu", num_params, param_names.size())); @@ -497,27 +532,23 @@ Function parse_lambda(ParseContext &ctx, size_t num_params) { } void parse_tensor_map(ParseContext &ctx) { - parse_expression(ctx); - Node_UP child = ctx.pop_expression(); + Node_UP child = get_expression(ctx); ctx.eat(','); Function lambda = parse_lambda(ctx, 1); ctx.push_expression(std::make_unique<nodes::TensorMap>(std::move(child), std::move(lambda))); } void parse_tensor_join(ParseContext &ctx) { - parse_expression(ctx); - Node_UP lhs = ctx.pop_expression(); + Node_UP lhs = get_expression(ctx); ctx.eat(','); - parse_expression(ctx); - Node_UP rhs = ctx.pop_expression(); + Node_UP rhs = get_expression(ctx); ctx.eat(','); Function lambda = parse_lambda(ctx, 2); ctx.push_expression(std::make_unique<nodes::TensorJoin>(std::move(lhs), std::move(rhs), std::move(lambda))); } void parse_tensor_reduce(ParseContext &ctx) { - parse_expression(ctx); - Node_UP child = ctx.pop_expression(); + Node_UP child = get_expression(ctx); ctx.eat(','); auto aggr_name = get_ident(ctx, false); auto maybe_aggr = AggrNames::from_name(aggr_name); @@ -530,8 +561,7 @@ void parse_tensor_reduce(ParseContext &ctx) { } void parse_tensor_rename(ParseContext &ctx) { - parse_expression(ctx); - Node_UP child = ctx.pop_expression(); + Node_UP child = get_expression(ctx); ctx.eat(','); auto from = get_idents(ctx); ctx.skip_spaces(); @@ -545,15 +575,66 @@ void parse_tensor_rename(ParseContext &ctx) { ctx.skip_spaces(); } -void parse_tensor_lambda(ParseContext &ctx) { - vespalib::string type_spec("tensor"); - while(!ctx.eos() && (ctx.get() != ')')) { - type_spec.push_back(ctx.get()); - ctx.next(); +// '{a:w,x:0}' +TensorSpec::Address get_tensor_address(ParseContext &ctx, const ValueType &type) { + TensorSpec::Address addr; + ctx.skip_spaces(); + ctx.eat('{'); + while (!ctx.find_list_end()) { + if (!addr.empty()) { + ctx.eat(','); + } + auto dim_name = get_ident(ctx, false); + size_t dim_idx = type.dimension_index(dim_name); + if (dim_idx != ValueType::Dimension::npos) { + const auto &dim = type.dimensions()[dim_idx]; + ctx.skip_spaces(); + ctx.eat(':'); + if (dim.is_mapped()) { + addr.emplace(dim_name, get_ident(ctx, false)); + } else { + size_t idx = get_size_t(ctx); + if (idx < dim.size) { + addr.emplace(dim_name, idx); + } else { + ctx.fail(make_string("dimension index too large: %zu", idx)); + } + } + } else { + ctx.fail(make_string("invalid dimension name: '%s'", dim_name.c_str())); + } } - ctx.eat(')'); - type_spec.push_back(')'); - ValueType type = ValueType::from_spec(type_spec); + ctx.eat('}'); + if (addr.size() != type.dimensions().size()) { + ctx.fail(make_string("incomplete address: '%s'", as_string(addr).c_str())); + } + return addr; +} + +// pre: 'tensor<float>(a{},x[3]):' -> type +// expect: '{{a:w,x:0}:1,{a:w,x:1}:2,{a:w,x:2}:3}' +void parse_tensor_create(ParseContext &ctx, const ValueType &type) { + if (type.is_error()) { + ctx.fail("invalid tensor type"); + return; + } + ctx.skip_spaces(); + ctx.eat('{'); + nodes::TensorCreate::Spec create_spec; + while (!ctx.find_list_end()) { + if (!create_spec.empty()) { + ctx.eat(','); + } + auto address = get_tensor_address(ctx, type); + ctx.skip_spaces(); + ctx.eat(':'); + create_spec.emplace(address, get_expression(ctx)); + } + ctx.eat('}'); + ctx.push_expression(std::make_unique<nodes::TensorCreate>(type, std::move(create_spec))); +} + +void parse_tensor_lambda(ParseContext &ctx, const ValueType &type) { if (!type.is_dense()) { ctx.fail("invalid tensor type"); return; @@ -563,19 +644,34 @@ void parse_tensor_lambda(ParseContext &ctx) { ctx.push_resolve_context(params, nullptr); ctx.skip_spaces(); ctx.eat('('); - parse_expression(ctx); + Function lambda(get_expression(ctx), std::move(param_names)); ctx.eat(')'); ctx.pop_resolve_context(); - Function lambda(ctx.pop_expression(), std::move(param_names)); ctx.push_expression(std::make_unique<nodes::TensorLambda>(std::move(type), std::move(lambda))); } +void parse_tensor_generator(ParseContext &ctx) { + vespalib::string type_spec("tensor"); + while(!ctx.eos() && (ctx.get() != ')')) { + type_spec.push_back(ctx.get()); + ctx.next(); + } + ctx.eat(')'); + type_spec.push_back(')'); + ValueType type = ValueType::from_spec(type_spec); + ctx.skip_spaces(); + if (ctx.get() == ':') { + ctx.eat(':'); + parse_tensor_create(ctx, type); + } else { + parse_tensor_lambda(ctx, type); + } +} + void parse_tensor_concat(ParseContext &ctx) { - parse_expression(ctx); - Node_UP lhs = ctx.pop_expression(); + Node_UP lhs = get_expression(ctx); ctx.eat(','); - parse_expression(ctx); - Node_UP rhs = ctx.pop_expression(); + Node_UP rhs = get_expression(ctx); ctx.eat(','); auto dimension = get_ident(ctx, false); ctx.skip_spaces(); @@ -622,7 +718,7 @@ void parse_symbol_or_call(ParseContext &ctx) { ParseContext::InputMark before_name = ctx.get_input_mark(); vespalib::string name = get_ident(ctx, true); if (name == "tensor") { - parse_tensor_lambda(ctx); + parse_tensor_generator(ctx); } else if (!try_parse_call(ctx, name)) { size_t id = parse_symbol(ctx, name, before_name); if (name.empty()) { @@ -716,20 +812,14 @@ bool parse_operator(ParseContext &ctx) { } void parse_expression(ParseContext &ctx) { - size_t old_mark = ctx.operator_mark(); - ctx.operator_mark(ctx.num_operators()); + size_t old_mark = ctx.init_expression(); bool expect_value = true; for (;;) { if (expect_value) { parse_value(ctx); } - ctx.skip_spaces(); - if (ctx.eos() || ctx.get() == ')' || ctx.get() == ',') { - while (ctx.num_operators() > ctx.operator_mark()) { - ctx.apply_operator(); - } - ctx.operator_mark(old_mark); - return; + if (ctx.find_expression_end()) { + return ctx.fini_expression(old_mark); } expect_value = parse_operator(ctx); } diff --git a/eval/src/vespa/eval/eval/interpreted_function.h b/eval/src/vespa/eval/eval/interpreted_function.h index dbe5345fc9b..e3e8d18b44f 100644 --- a/eval/src/vespa/eval/eval/interpreted_function.h +++ b/eval/src/vespa/eval/eval/interpreted_function.h @@ -50,6 +50,10 @@ public: stack.pop_back(); stack.back() = value; } + void pop_n_push(size_t n, const Value &value) { + stack.resize(stack.size() - (n - 1), value); + stack.back() = value; + } }; class Context { friend class InterpretedFunction; diff --git a/eval/src/vespa/eval/eval/key_gen.cpp b/eval/src/vespa/eval/eval/key_gen.cpp index 86908b331ba..63cd39b5856 100644 --- a/eval/src/vespa/eval/eval/key_gen.cpp +++ b/eval/src/vespa/eval/eval/key_gen.cpp @@ -41,6 +41,7 @@ struct KeyGen : public NodeVisitor, public NodeTraverser { void visit(const TensorRename &) override { add_byte(14); } // dimensions should be part of key 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 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 4c911ee6e44..1ebf64ad269 100644 --- a/eval/src/vespa/eval/eval/llvm/compiled_function.cpp +++ b/eval/src/vespa/eval/eval/llvm/compiled_function.cpp @@ -131,7 +131,9 @@ CompiledFunction::detect_issues(const Function &function) nodes::TensorReduce, nodes::TensorRename, nodes::TensorLambda, - nodes::TensorConcat>(node)) { + nodes::TensorConcat, + nodes::TensorCreate>(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 c613a5fa94b..083e5d8593c 100644 --- a/eval/src/vespa/eval/eval/llvm/llvm_wrapper.cpp +++ b/eval/src/vespa/eval/eval/llvm/llvm_wrapper.cpp @@ -479,6 +479,9 @@ struct FunctionBuilder : public NodeVisitor, public NodeTraverser { void visit(const TensorConcat &node) override { make_error(node.num_children()); } + void visit(const TensorCreate &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 ebf065f32a1..f0afb34376d 100644 --- a/eval/src/vespa/eval/eval/make_tensor_function.cpp +++ b/eval/src/vespa/eval/eval/make_tensor_function.cpp @@ -97,6 +97,16 @@ struct TensorFunctionBuilder : public NodeVisitor, public NodeTraverser { stack.back() = tensor_function::concat(a, b, dimension, stash); } + void make_create(const TensorCreate &node) { + assert(stack.size() >= node.num_children()); + std::map<TensorSpec::Address, tensor_function::Node::CREF> spec; + for (size_t idx = node.num_children(); idx-- > 0; ) { + spec.emplace(node.get_child_address(idx), stack.back()); + stack.pop_back(); + } + stack.push_back(tensor_function::create(node.type(), 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(); @@ -184,6 +194,9 @@ struct TensorFunctionBuilder : public NodeVisitor, public NodeTraverser { void visit(const TensorConcat &node) override { make_concat(node, node.dimension()); } + void visit(const TensorCreate &node) override { + make_create(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 29ae02b9e65..f802a9ab03f 100644 --- a/eval/src/vespa/eval/eval/node_types.cpp +++ b/eval/src/vespa/eval/eval/node_types.cpp @@ -114,6 +114,14 @@ struct TypeResolver : public NodeVisitor, public NodeTraverser { void visit(const TensorConcat &node) override { bind_type(ValueType::concat(state.peek(1), state.peek(0), 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); + } + } + bind_type(node.type(), node); + } void visit(const Add &node) override { resolve_op2(node); } void visit(const Sub &node) override { resolve_op2(node); } diff --git a/eval/src/vespa/eval/eval/node_visitor.h b/eval/src/vespa/eval/eval/node_visitor.h index 10b389db792..d0996f370c2 100644 --- a/eval/src/vespa/eval/eval/node_visitor.h +++ b/eval/src/vespa/eval/eval/node_visitor.h @@ -35,6 +35,7 @@ struct NodeVisitor { virtual void visit(const nodes::TensorRename &) = 0; virtual void visit(const nodes::TensorLambda &) = 0; virtual void visit(const nodes::TensorConcat &) = 0; + virtual void visit(const nodes::TensorCreate &) = 0; // operator nodes virtual void visit(const nodes::Add &) = 0; @@ -103,6 +104,7 @@ struct EmptyNodeVisitor : NodeVisitor { void visit(const nodes::TensorRename &) override {} void visit(const nodes::TensorLambda &) override {} void visit(const nodes::TensorConcat &) override {} + void visit(const nodes::TensorCreate &) 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/string_stuff.cpp b/eval/src/vespa/eval/eval/string_stuff.cpp new file mode 100644 index 00000000000..70331a38cca --- /dev/null +++ b/eval/src/vespa/eval/eval/string_stuff.cpp @@ -0,0 +1,23 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "string_stuff.h" +#include <vespa/vespalib/util/stringfmt.h> + +namespace vespalib::eval { + +vespalib::string as_string(const TensorSpec::Address &address) { + CommaTracker label_list; + vespalib::string str = "{"; + for (const auto &label: address) { + label_list.maybe_comma(str); + if (label.second.is_mapped()) { + str += make_string("%s:%s", label.first.c_str(), label.second.name.c_str()); + } else { + str += make_string("%s:%zu", label.first.c_str(), label.second.index); + } + } + str += "}"; + return str; +} + +} diff --git a/eval/src/vespa/eval/eval/string_stuff.h b/eval/src/vespa/eval/eval/string_stuff.h new file mode 100644 index 00000000000..2e60691fa28 --- /dev/null +++ b/eval/src/vespa/eval/eval/string_stuff.h @@ -0,0 +1,32 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include "tensor_spec.h" +#include <vespa/vespalib/stllike/string.h> + +namespace vespalib::eval { + +/** + * Helper class used to insert commas on the appropriate places in + * comma-separated textual lists. + **/ +struct CommaTracker { + bool first; + CommaTracker() : first(true) {} + void maybe_comma(vespalib::string &dst) { + if (first) { + first = false; + } else { + dst.push_back(','); + } + } +}; + +/** + * Convert a tensor spec address into a string on the form: + * '{dim1:label,dim2:index, ...}' + **/ +vespalib::string as_string(const TensorSpec::Address &address); + +} diff --git a/eval/src/vespa/eval/eval/tensor_function.cpp b/eval/src/vespa/eval/eval/tensor_function.cpp index 10e589c832c..951b9772505 100644 --- a/eval/src/vespa/eval/eval/tensor_function.cpp +++ b/eval/src/vespa/eval/eval/tensor_function.cpp @@ -7,6 +7,7 @@ #include "tensor_engine.h" #include "simple_tensor_engine.h" #include "visit_stuff.h" +#include "string_stuff.h" #include <vespa/vespalib/objects/objectdumper.h> #include <vespa/log/log.h> @@ -117,6 +118,17 @@ void op_tensor_concat(State &state, uint64_t param) { state.pop_pop_push(state.engine.concat(state.peek(1), state.peek(0), dimension, state.stash)); } +void op_tensor_create(State &state, uint64_t param) { + const Create &self = unwrap_param<Create>(param); + TensorSpec spec(self.result_type().to_spec()); + size_t i = 0; + for (auto pos = self.spec().rbegin(); pos != self.spec().rend(); ++pos) { + spec.add(pos->first, state.peek(i++).as_double()); + } + const Value &result = *state.stash.create<Value::UP>(state.engine.from_spec(spec)); + state.pop_n_push(i, result); +} + } // namespace vespalib::eval::tensor_function //----------------------------------------------------------------------------- @@ -266,6 +278,30 @@ Concat::visit_self(vespalib::ObjectVisitor &visitor) const //----------------------------------------------------------------------------- +void +Create::push_children(std::vector<Child::CREF> &children) const +{ + for (const auto &cell: _spec) { + children.emplace_back(cell.second); + } +} + +Instruction +Create::compile_self(Stash &) const +{ + return Instruction(op_tensor_create, wrap_param<Create>(*this)); +} + +void +Create::visit_children(vespalib::ObjectVisitor &visitor) const +{ + for (const auto &cell: _spec) { + ::visit(visitor, ::vespalib::eval::as_string(cell.first), cell.second.get()); + } +} + +//----------------------------------------------------------------------------- + Instruction Rename::compile_self(Stash &stash) const { @@ -336,6 +372,10 @@ const Node &concat(const Node &lhs, const Node &rhs, const vespalib::string &dim return stash.create<Concat>(result_type, lhs, rhs, dimension); } +const Node &create(const ValueType &type, const std::map<TensorSpec::Address,Node::CREF> &spec, Stash &stash) { + return stash.create<Create>(type, 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 628943e51b2..f82f1c80f0f 100644 --- a/eval/src/vespa/eval/eval/tensor_function.h +++ b/eval/src/vespa/eval/eval/tensor_function.h @@ -7,7 +7,7 @@ #include <vespa/vespalib/stllike/asciistream.h> #include <vespa/vespalib/stllike/string.h> #include <vespa/vespalib/util/arrayref.h> -#include "make_tensor_function.h" +#include "tensor_spec.h" #include "lazy_params.h" #include "value_type.h" #include "value.h" @@ -239,7 +239,7 @@ class Join : public Op2 { using Super = Op2; private: - join_fun_t _function; + join_fun_t _function; public: Join(const ValueType &result_type_in, const TensorFunction &lhs_in, @@ -273,6 +273,28 @@ public: //----------------------------------------------------------------------------- +class Create : public Node +{ + using Super = Node; +private: + std::map<TensorSpec::Address, Child> _spec; +public: + Create(const ValueType &result_type_in, const std::map<TensorSpec::Address, Node::CREF> &spec_in) + : Node(result_type_in), _spec() + { + for (const auto &cell: spec_in) { + _spec.emplace(cell.first, Child(cell.second)); + } + } + const std::map<TensorSpec::Address, Child> &spec() const { return _spec; } + 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; @@ -326,6 +348,7 @@ const Node &reduce(const Node &child, Aggr aggr, const std::vector<vespalib::str 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 &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 6abcd8fb9bc..96e8fbef6a3 100644 --- a/eval/src/vespa/eval/eval/tensor_nodes.cpp +++ b/eval/src/vespa/eval/eval/tensor_nodes.cpp @@ -13,6 +13,7 @@ void TensorReduce::accept(NodeVisitor &visitor) const { visitor.visit(*this); } 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); } } // 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 2a13f86f017..776275f5884 100644 --- a/eval/src/vespa/eval/eval/tensor_nodes.h +++ b/eval/src/vespa/eval/eval/tensor_nodes.h @@ -4,9 +4,12 @@ #include "basic_nodes.h" #include "function.h" +#include "tensor_spec.h" +#include "aggr.h" +#include "string_stuff.h" #include <vespa/vespalib/stllike/string.h> +#include <vector> #include <map> -#include "aggr.h" namespace vespalib { namespace eval { @@ -209,6 +212,53 @@ public: } }; +class TensorCreate : public Node { +public: + using Spec = std::map<TensorSpec::Address, Node_UP>; +private: + using Child = std::pair<TensorSpec::Address, Node_UP>; + using ChildList = std::vector<Child>; + ValueType _type; + ChildList _cells; +public: + TensorCreate(ValueType type_in, Spec spec) + : _type(std::move(type_in)), _cells() + { + for (auto &cell: spec) { + _cells.emplace_back(cell.first, std::move(cell.second)); + } + } + const ValueType &type() const { return _type; } + vespalib::string dump(DumpContext &ctx) const override { + vespalib::string str = _type.to_spec(); + str += ":{"; + CommaTracker child_list; + for (const Child &child: _cells) { + child_list.maybe_comma(str); + str += as_string(child.first); + str += ":"; + str += child.second->dump(ctx); + } + str += "}"; + return str; + } + void accept(NodeVisitor &visitor) const override ; + size_t num_children() const override { return _cells.size(); } + const Node &get_child(size_t idx) const override { + assert(idx < _cells.size()); + return *_cells[idx].second; + } + const TensorSpec::Address &get_child_address(size_t idx) const { + assert(idx < _cells.size()); + return _cells[idx].first; + } + void detach_children(NodeHandler &handler) override { + for (Child &child: _cells) { + handler.handle(std::move(child.second)); + } + } +}; + } // namespace vespalib::eval::nodes } // namespace vespalib::eval } // namespace vespalib diff --git a/eval/src/vespa/eval/eval/test/eval_fixture.cpp b/eval/src/vespa/eval/eval/test/eval_fixture.cpp index 3f5fa4d72bb..30e55d0418b 100644 --- a/eval/src/vespa/eval/eval/test/eval_fixture.cpp +++ b/eval/src/vespa/eval/eval/test/eval_fixture.cpp @@ -2,6 +2,7 @@ #include <vespa/vespalib/testkit/test_kit.h> #include "eval_fixture.h" +#include <vespa/eval/eval/make_tensor_function.h> namespace vespalib::eval::test { diff --git a/eval/src/vespa/eval/eval/test/eval_spec.cpp b/eval/src/vespa/eval/eval/test/eval_spec.cpp index baa3ee989d4..f89cf5edd0d 100644 --- a/eval/src/vespa/eval/eval/test/eval_spec.cpp +++ b/eval/src/vespa/eval/eval/test/eval_spec.cpp @@ -177,6 +177,7 @@ EvalSpec::add_tensor_operation_cases() { add_expression({}, "tensor(x[10],y[10])(x==y)"); 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}"); } void diff --git a/eval/src/vespa/eval/eval/test/tensor_conformance.cpp b/eval/src/vespa/eval/eval/test/tensor_conformance.cpp index dc39dfb04a7..b5426d9ca29 100644 --- a/eval/src/vespa/eval/eval/test/tensor_conformance.cpp +++ b/eval/src/vespa/eval/eval/test/tensor_conformance.cpp @@ -797,6 +797,19 @@ struct TestContext { //------------------------------------------------------------------------- + void test_tensor_create(const vespalib::string &expr, double a, double b, const TensorSpec &expect) { + TEST_DO(verify_result(Expr_TT(expr).eval(engine, spec(a), spec(b)), expect)); + } + + void test_tensor_create() { + TEST_DO(test_tensor_create("tensor(x[3]):{{x:0}:a,{x:1}:b,{x:2}:3}", 1, 2, spec(x(3), N()))); + TEST_DO(test_tensor_create("tensor<float>(x[3]):{{x:0}:a,{x:1}:b,{x:2}:3}", 1, 2, spec(float_cells({x(3)}), N()))); + TEST_DO(test_tensor_create("tensor(x{}):{{x:a}:a,{x:b}:b,{x:c}:3}", 1, 2, spec(x({"a", "b", "c"}), N()))); + TEST_DO(test_tensor_create("tensor(x{},y[2]):{{x:a,y:0}:a,{x:a,y:1}:b}", 1, 2, spec({x({"a"}),y(2)}, N()))); + } + + //------------------------------------------------------------------------- + void verify_encode_decode(const TensorSpec &spec, const TensorEngine &encode_engine, const TensorEngine &decode_engine) @@ -879,6 +892,7 @@ struct TestContext { TEST_DO(test_concat()); TEST_DO(test_rename()); TEST_DO(test_tensor_lambda()); + TEST_DO(test_tensor_create()); TEST_DO(test_binary_format()); } }; |