1
#include <torch/csrc/jit/frontend/schema_matching.h>
3
#include <ATen/core/interned_strings.h>
4
#include <ATen/core/jit_type.h>
5
#include <c10/util/Exception.h>
6
#include <c10/util/Optional.h>
7
#include <c10/util/irange.h>
8
#include <caffe2/serialize/versions.h>
9
#include <torch/csrc/jit/frontend/builtin_functions.h>
10
#include <torch/csrc/jit/frontend/error_report.h>
11
#include <torch/csrc/jit/frontend/function_schema_parser.h>
12
#include <torch/csrc/jit/ir/ir.h>
13
#include <torch/csrc/jit/operator_upgraders/utils.h>
14
#include <torch/csrc/jit/operator_upgraders/version_map.h>
15
#include <torch/csrc/jit/runtime/operator.h>
19
static inline TypePtr unwrapOptional(TypePtr opt_type) {
20
if (auto dyn = opt_type->castRaw<c10::DynamicType>()) {
21
return unwrapOptional(dyn->fallback());
23
if (auto unwrap_list_type = opt_type->cast<OptionalType>()) {
24
return unwrap_list_type->getElementType();
29
static inline bool isIntOrFloatUsedAsList(
31
const Argument& arg) {
32
// Look for int[N] or float[N]
33
const auto& v_type = value->type();
34
if (v_type != FloatType::get() && v_type != IntType::get())
36
auto arg_type = unwrapOptional(arg.type());
37
auto list_type = arg_type->cast<ListType>();
38
return list_type && list_type->getElementType() == v_type && arg.N();
41
/// Returns true if `type` is a Tuple in which all the elements have the
42
/// same type or if it's a subtype of `list_type_`.
43
bool convertibleToList(const TypePtr& type, const TypePtr& list_type_) {
44
auto list_type = list_type_->castRaw<ListType>();
48
if (type->isSubtypeOf(*list_type_)) {
51
if (auto tuple = type->castRaw<TupleType>()) {
53
tuple->elements().begin(),
54
tuple->elements().end(),
55
[&](const TypePtr& t) {
56
// TODO: resolve VarType if necessary
57
return t->isSubtypeOf(*list_type->getElementType());
63
// Applies implicit conversion from value trying to turn it into type
64
// concrete_type. It succeeds if `return_value->isSubtypeOf(concrete_type)`
65
Value* tryConvertToType(
66
const SourceRange& loc,
68
const TypePtr& concrete_type,
70
bool allow_conversions) {
71
// treat conversion to Optional[T] as conversions to T
72
if (OptionalTypePtr op = concrete_type->cast<OptionalType>()) {
73
if (value->type()->kind() != OptionalType::Kind &&
74
!value->type()->isSubtypeOf(*NoneType::get())) {
75
return tryConvertToType(
76
loc, graph, op->getElementType(), value, allow_conversions);
80
// allow temporary, unannotated list literals `[]` to match to arbitrary list
82
if (value->node()->kind() == prim::EmptyListLiteral &&
83
concrete_type->cast<ListType>()) {
85
.insertNode(graph.createList(
86
concrete_type->cast<ListType>()->getElementType(), {}))
90
if (auto value_tuple = value->type()->cast<TupleType>()) {
91
// Allow homogeneous tuples to be casted implicitly to lists of appropriate
93
if (convertibleToList(value->type(), unwrapOptional(concrete_type))) {
94
auto unpacked = createTupleUnpack(value);
96
unwrapOptional(concrete_type)->expectRef<ListType>().getElementType();
97
value = graph.insertNode(graph.createList(elem_type, unpacked))->output();
100
// inductively apply implicit conversions to tuples
101
if (auto concrete_tuple = concrete_type->cast<TupleType>()) {
102
if (!value_tuple->isSubtypeOf(*concrete_tuple) &&
103
concrete_tuple->elements().size() == value_tuple->elements().size()) {
104
auto unpacked = createTupleUnpack(value);
105
std::vector<Value*> converted;
106
for (size_t i = 0; i < concrete_tuple->elements().size(); ++i) {
107
converted.emplace_back(tryConvertToType(
110
concrete_tuple->elements().at(i),
114
value = graph.insertNode(graph.createTuple(converted))->output();
119
// implicit conversions
120
if (allow_conversions) {
121
// Convert tensor or number to concrete int/float types
122
bool value_isa_tensor = value->type()->isSubtypeOf(*TensorType::get());
123
bool value_equals_number = *value->type() == *NumberType::get();
124
bool concrete_float = *concrete_type == *FloatType::get();
125
bool concrete_complex = *concrete_type == *ComplexType::get();
126
bool concrete_int = *concrete_type == *IntType::get();
127
bool concrete_number = *concrete_type == *NumberType::get();
128
if (value_isa_tensor) {
129
if (concrete_float) {
130
value = graph.insert(aten::FloatImplicit, {value}, {}, loc);
131
} else if (concrete_complex) {
132
value = graph.insert(aten::ComplexImplicit, {value}, {}, loc);
133
} else if (concrete_int) {
134
value = graph.insert(aten::IntImplicit, {value}, {}, loc);
135
} else if (concrete_number) {
136
value = graph.insert(aten::ScalarImplicit, {value}, {}, loc);
138
} else if (value_equals_number) {
139
if (concrete_float) {
140
value = graph.insert(aten::Float, {value}, {}, loc);
141
} else if (concrete_complex) {
142
value = graph.insert(aten::Complex, {value}, {}, loc);
143
} else if (concrete_int) {
144
value = graph.insert(aten::Int, {value}, {}, loc);
146
} else if (*value->type() == *BoolType::get()) {
147
if (concrete_float) {
148
value = graph.insert(aten::Float, {value}, {}, loc);
149
} else if (concrete_int) {
150
value = graph.insert(aten::Int, {value}, {}, loc);
151
} else if (concrete_number) {
152
value = graph.insert(aten::Int, {value}, {}, loc);
156
// Convert strings to device
157
if (value->type()->isSubtypeOf(*StringType::get()) &&
158
concrete_type->isSubtypeOf(*DeviceObjType::get())) {
159
return graph.insert(aten::device, {value}, {}, loc);
166
// Checks if `named_value` can be used as a value for `arg`. If `arg` is a
167
// VarType, it will be added to the type_env through `matchTypeVariables` as
168
// the corresponding actual type. If `allow_conversions` is true, implicit
169
// conversions to the `arg` type may be performed through `tryConvertToType`.
170
static Value* tryMatchArgument(
173
const SourceRange& loc,
174
const NamedValue& named_value,
175
std::ostream* failure_messages,
176
const std::function<std::ostream&()>& err,
177
bool allow_conversions,
179
Value* value = named_value.value(graph);
181
// Some functions that take lists of integers or floats for fixed size arrays
182
// also allow single ints/floats to be passed in their place. The single
183
// int/float is then repeated to the length of the list
184
if (isIntOrFloatUsedAsList(value, arg)) {
185
std::vector<Value*> repeated(*arg.N(), value);
187
graph.insertNode(graph.createList(value->type(), repeated))->output();
190
// Resolve VarType variables
191
const MatchTypeReturn matched =
192
matchTypeVariables(arg.type(), value->type(), type_env);
193
if (!matched.success()) {
194
if (failure_messages) {
195
err() << "Could not match type " << value->type()->repr_str() << " to "
196
<< arg.type()->repr_str() << " in argument '" << arg.name()
197
<< "': " << matched.reason() << ".\n";
201
const auto concrete_type = tryEvalTypeVariables(arg.type(), type_env);
202
if (!concrete_type) {
203
if (failure_messages) {
204
err() << "Type variables in type " << arg.type()->repr_str()
205
<< " could not be inferred from actual type "
206
<< value->type()->repr_str();
211
// Check if the value can be matched to the arg through any implicit
213
value = tryConvertToType(loc, graph, concrete_type, value, allow_conversions);
214
std::stringstream ss;
215
if (!value->type()->isSubtypeOfExt(
216
*concrete_type, /*why_not=*/(failure_messages) ? &ss : nullptr)) {
217
if (failure_messages) {
218
auto& ostream = err()
219
<< arg.formatTypeMismatchMsg(value->type()->repr_str());
221
if (auto pt = value->type()->cast<TensorType>()) {
222
if (pt->isInferredType()) {
223
std::string inferred_type_hint;
224
inferred_type_hint = c10::str(
225
"Inferred the value for argument '",
227
"' to be of type 'Tensor' ",
228
"because it was not annotated with an explicit type.\n");
229
ostream << inferred_type_hint;
233
if (auto v = value->type()->cast<ListType>()) {
234
if (v->getElementType()->isSubtypeOf(*TensorType::get())) {
235
ostream << "Empty lists default to List[Tensor]. Add a variable "
236
"annotation to the assignment to create an empty list "
237
"of another type (torch.jit.annotate(List[T, []]) where T "
238
"is the type of elements in the list for Python 2)\n";
250
c10::optional<size_t> findInputWithName(
251
const std::string& name,
252
at::ArrayRef<NamedValue> kwargs,
254
for (const auto i : c10::irange(kwargs.size())) {
255
// TS doesn't understand that the self argument in function
256
// scheams is renamed to input for the functional variant
257
if (is_aten && name == "self" && kwargs[i].name() == "input") {
260
if (kwargs[i].name() == name) {
267
/// Creates a list with the provided values if each value's type can be matched
268
/// to an argument with type `elem_type`. If a type in `varargs` does not match
269
/// `elem_type`, nullptr is returned. This is used for creating lists from
270
/// varargs so that calls like torch.zeros(1, 2, 3) will be matched to
271
/// aten::zeros(int[]).
272
static Value* tryCreateList(
273
const TypePtr& elem_type,
275
const SourceRange& loc,
276
at::ArrayRef<NamedValue> varargs,
277
std::ostream* failure_messages,
278
const std::function<std::ostream&()>& err,
279
bool convert_tensor_to_num,
281
Argument elem_arg("<varargs>", elem_type);
282
std::vector<Value*> list_elements;
283
for (const auto& named_value : varargs) {
284
// Try to convert named_value to elem_type
285
Value* matched_value = tryMatchArgument(
292
/*allow_conversions=*/convert_tensor_to_num,
294
if (!matched_value) {
297
list_elements.push_back(matched_value);
300
return graph.insertNode(graph.createList(elem_type, list_elements))->output();
303
// Check if it is possible to convert all the remaining non-kwarg arguments
304
// to a list. This allows zeros(IntArrayRef sizes) to work with zeros(1, 2) or
306
static bool varargsCanBeUsedAsList(
307
const FunctionSchema& schema,
309
const Argument& arg) {
310
// The arg must be the last one in the arg list that is not a kwarg
311
bool is_last_argument = arg_index + 1 == schema.arguments().size() ||
312
schema.arguments()[arg_index + 1].kwarg_only();
314
auto arg_type = arg.type();
315
if (auto dyn = arg_type->castRaw<c10::DynamicType>()) {
316
arg_type = dyn->fallback();
319
// The formal must be a list
320
bool argument_is_list = arg_type->kind() == TypeKind::ListType;
322
// matching varargs of typevar list nyi
323
bool typevar_list = argument_is_list &&
324
arg_type->castRaw<ListType>()->getElementType()->cast<VarType>();
326
// it must not be a broadcasting list like int[3],
327
// otherwise a single int is a valid input
328
bool arg_is_broadcasting_list = bool(arg.N());
330
return is_last_argument && argument_is_list && !arg_is_broadcasting_list &&
334
bool isBlockListedSchema(const FunctionSchema& schema) {
335
// Note (@zasdfgbnm):
336
// This is a workaround for https://github.com/pytorch/pytorch/issues/47964
337
// Currently JIT does not distinguish ScalarType vs int, so there is really
338
// no way to distinguish x.view(1) vs x.view(torch.int8). So we have to
339
// hardcode the aten::view.dtype here to block this overload. This blocklist
340
// should be removed when JIT fully suports ScalarType as its own type.
341
if (schema.name() == "aten::view" && schema.overload_name() == "dtype") {
344
// Note (@tugsbayasgalan)
345
// TorchScript doesn't suport kwargs so this op collides with aten.max.others
346
// since both of them have 2 Tensor inputs. Since we don't expect users to
347
// use this op in TS, we just skip it
348
if (schema.name() == "aten::max" && schema.overload_name() == "unary_out") {
351
if (schema.name() == "aten::min" && schema.overload_name() == "unary_out") {
357
static c10::optional<MatchedSchema> tryMatchSchema(
358
const FunctionSchema& schema,
359
const SourceRange& loc,
361
at::ArrayRef<NamedValue> args,
362
at::ArrayRef<NamedValue> kwargs,
363
c10::optional<NamedValue> self,
364
std::ostream* failure_messages,
365
bool allow_conversions) {
366
if (isBlockListedSchema(schema)) {
370
auto err = [&]() -> std::ostream& {
371
*failure_messages << "\n" << schema << ":\n";
372
return *failure_messages;
375
// For VarTypes, maps VarType name to actual type as it's used with these
378
std::vector<Value*> positional_inputs;
379
std::vector<bool> used_kwarg(kwargs.size(), false);
381
auto schema_namespace = schema.operator_name().getNamespace();
382
bool is_aten = false;
383
if (schema_namespace.has_value()) {
384
if (schema_namespace.value() == "aten") {
388
// if we finish the loop will we have consumed all arguments?
389
size_t used_args = 0;
390
for (const auto schema_i : c10::irange(schema.arguments().size())) {
391
const auto& arg = schema.arguments()[schema_i];
392
c10::optional<NamedValue> actual_named_value;
393
if (arg.name() == "self" && self) {
394
actual_named_value = self;
396
} else if (!arg.kwarg_only() && used_args < args.size()) {
397
// Try to convert all the remaining non-kwarg arguments (used_args) to a
398
// list. Allow zeros(IntArrayRef sizes) to work with zeros(1, 2) or
400
if (allow_conversions && varargsCanBeUsedAsList(schema, schema_i, arg)) {
401
auto value = args[used_args].value(graph);
402
const auto& actual_type = value->type();
403
// The actual cannot already be a list
404
if (actual_type->kind() != TypeKind::ListType &&
405
!convertibleToList(actual_type, unwrapOptional(arg.type()))) {
406
auto formal_type = unwrapOptional(arg.type())
407
->expectRef<ListType>()
410
Value* list = tryCreateList(
414
at::ArrayRef<NamedValue>(args).slice(used_args),
422
used_args = args.size();
423
positional_inputs.push_back(list);
428
// Set actual_named_value to the argument and mark the arg position as
430
actual_named_value = args[used_args];
433
auto kwarg_idx = findInputWithName(arg.name(), kwargs, is_aten)) {
434
const NamedValue& nv = kwargs[*kwarg_idx];
435
if (used_kwarg[*kwarg_idx]) {
436
if (failure_messages) {
437
err() << "Argument " << nv.name()
438
<< " specified twice in schema, submit a bug report!\n";
442
used_kwarg[*kwarg_idx] = true;
443
actual_named_value = nv;
444
} else if (arg.default_value()) {
445
// Argument has a default value and no value was provided, so use the
447
actual_named_value = NamedValue(*arg.default_value());
449
if (failure_messages) {
450
err() << "Argument " << schema.arguments()[schema_i].name()
451
<< " not provided.\n";
456
// Make sure the actual_named_value found matches the type of arg
457
Value* positional = tryMatchArgument(
469
positional_inputs.push_back(positional);
471
// check for unused self argument
472
if (self != c10::nullopt) {
473
if (failure_messages) {
474
err() << "Provided self argument not used in schema.\n";
479
if (schema.is_vararg()) {
480
for (; used_args < args.size(); ++used_args) {
481
positional_inputs.push_back(args[used_args].value(graph));
485
// check for unused positional arguments
486
if (used_args < args.size()) {
487
if (failure_messages) {
488
err() << "Expected at most " << used_args << " arguments "
489
<< "but found " << args.size() << " positional arguments.\n";
493
// check for unused kwargs
494
for (const auto i : c10::irange(kwargs.size())) {
495
const auto& nv = kwargs[i];
496
if (!used_kwarg[i]) {
497
if (failure_messages) {
498
if (!schema.argumentIndexWithName(nv.name())) {
499
err() << "Keyword argument " << nv.name() << " unknown.\n";
501
err() << "Keyword argument " << nv.name() << " specified twice.\n";
508
const auto& returns = schema.returns();
509
auto return_types = fmap(returns, [&](const Argument& r) {
510
TypePtr result = tryEvalTypeVariables(r.type(), type_env);
511
TORCH_INTERNAL_ASSERT(
512
result, r.type()->repr_str(), " has unbound type variables.");
515
// Codegen does not support return of namedtuples with undefined field names.
516
// Therefore, either all or none returns has field names.
517
bool return_has_field_names =
518
std::all_of(returns.begin(), returns.end(), [&](const Argument& r) {
519
return r.name().length() > 0;
521
c10::OptNameList return_field_names = c10::nullopt;
522
if (return_has_field_names) {
524
fmap(returns, [&](const Argument& r) { return r.name(); });
527
// construct the full name of the schema for easier look up
528
auto schema_name = getFullSchemaName(schema);
530
return MatchedSchema{
531
std::move(positional_inputs),
532
std::move(return_types),
533
std::move(return_field_names),
537
MatchedSchema matchSchema(
538
const ::c10::FunctionSchema& schema,
539
const SourceRange& loc,
541
at::ArrayRef<NamedValue> args,
542
at::ArrayRef<NamedValue> kwargs,
543
const c10::optional<NamedValue>& self) {
544
std::stringstream failure_messages;
545
if (auto result = tryMatchSchema(
553
/*allow_conversions=*/true)) {
556
throw ErrorReport(loc) << failure_messages.str();
559
static std::string prefixLine(
560
const std::string& str,
561
const std::string& prefix) {
562
std::stringstream ss;
563
bool was_newline = true;
568
was_newline = c == '\n';
573
std::pair<size_t, MatchedSchema> matchSchemas(
574
const std::vector<const FunctionSchema*>& schemas,
575
const SourceRange& loc,
577
at::ArrayRef<NamedValue> args,
578
at::ArrayRef<NamedValue> kwargs,
579
const c10::optional<NamedValue>& self,
580
bool render_errors) {
581
TORCH_INTERNAL_ASSERT(!schemas.empty());
582
// if there is only one schema, we do not need to try without conversions
583
// first. this is faster and puts less dead code in the graph.
584
if (schemas.size() == 1) {
585
return std::make_pair(
586
0, matchSchema(*schemas.at(0), loc, graph, args, kwargs, self));
588
std::stringstream failure_messages;
589
for (bool allow_conversions : {false, true}) {
590
// clear previous error messages
591
failure_messages.str("");
592
for (const auto i : c10::irange(schemas.size())) {
593
const auto matched_schema = tryMatchSchema(
600
render_errors ? &failure_messages : nullptr,
602
if (matched_schema) {
603
return std::make_pair(i, *matched_schema);
607
// we optimistically assume this call will not error, and avoid formatting the
608
// error strings. If we discover it did error, then we replay it, recording
610
if (!render_errors) {
612
schemas, loc, graph, args, kwargs, self, /*render_errors=*/true);
615
throw ErrorReport(loc) << "Arguments for call are not valid.\n"
616
<< "The following variants are available:\n"
617
<< prefixLine(failure_messages.str(), " ")
618
<< "\nThe original call is";
619
throw ErrorReport(loc) << failure_messages.str();
622
// pack outputs of a function following python rules. If there is a single value
623
// return a SimpleValue, otherwise pack all the values into a Tuple.
624
static Value* packOutputs(
626
at::ArrayRef<Value*> values,
627
c10::OptNameList field_names) {
628
if (values.size() == 1) {
631
std::shared_ptr<FunctionSchema> schema;
632
TupleTypePtr named_tuple = nullptr;
634
auto types = fmap(values, [](Value* v) { return v->type(); });
636
TupleType::createNamed(c10::nullopt, field_names.value(), types);
638
return g.insertNode(g.createTuple(values, named_tuple))->output();
641
// Given a successful match between operator schema and symbol, emit a node
642
// with the appropriate inputs and outputs.
643
static Value* emitBuiltinNode(
644
const MatchedSchema& matched_schema,
645
const SourceRange& loc,
648
c10::optional<size_t> version) {
649
auto n = graph.insertNode(graph.create(name, matched_schema.inputs, 0))
650
->setSourceRange(loc);
652
for (auto& ret : matched_schema.return_types) {
653
n->addOutput()->setType(ret);
656
// assert that we did indeed create an op that has implementation
657
// otherwise schema and dispatch are not in sync ONLY if the op is up
658
// to date with the server version
659
if (!version.has_value() ||
660
isOpSymbolCurrent(matched_schema.schema_name, version.value())) {
663
n->setHistoricSchemaName(matched_schema.schema_name);
666
return packOutputs(graph, n->outputs(), matched_schema.return_field_names);
669
std::string getFullSchemaName(const ::c10::FunctionSchema& schema) {
670
if (!schema.overload_name().empty()) {
671
return schema.operator_name().name + "." + schema.overload_name();
673
return schema.operator_name().name;
676
// Search for operators matching the provided symbol name and input types.
677
// If one is found, emit a node to the graph for that operator.
678
Value* emitBuiltinCall(
679
const SourceRange& loc,
682
at::ArrayRef<NamedValue> args,
683
at::ArrayRef<NamedValue> kwargs,
684
const c10::optional<NamedValue>& self) {
685
const auto& variants = getAllOperatorsFor(name);
686
const auto& builtin_functions = getAllBuiltinFunctionsFor(name);
688
// first let's set the graph's version
689
auto graph_version = graph.get_op_version();
691
std::stringstream failure_messages;
692
std::vector<const FunctionSchema*> schemas;
693
// we append them later to schemas because
694
// parseSchema returns rvalue which can not
695
// be casted to const pointer.
696
std::vector<FunctionSchema> upgrader_schemas;
697
schemas.reserve(variants.size());
698
for (const std::shared_ptr<Operator>& op : variants) {
699
bool found_upgrader = false;
700
auto op_name = getFullSchemaName(op->schema());
701
if (graph_version.has_value()) {
702
auto version_entry = get_operator_version_map().find(op_name);
703
if (version_entry != get_operator_version_map().end()) {
704
auto old_schema_entry =
705
findUpgrader(version_entry->second, graph_version.value());
706
if (old_schema_entry.has_value()) {
707
FunctionSchema old_schema =
708
parseSchema(old_schema_entry.value().old_schema);
709
upgrader_schemas.push_back(old_schema);
710
found_upgrader = true;
712
if (!isOpCurrentBasedOnUpgraderEntries(
713
version_entry->second, graph_version.value())) {
714
TORCH_INTERNAL_ASSERT(false, "Valid upgrader must be present");
720
schemas.push_back(&op->schema());
723
// we might have seen old historic
724
// ops that are deprecated
725
if (variants.empty()) {
727
loadPossibleHistoricOps(name.toQualString(), graph_version);
728
upgrader_schemas.reserve(oldSchemas.size());
729
for (const auto& old_schema_entry : oldSchemas) {
730
FunctionSchema old_schema = parseSchema(old_schema_entry);
731
upgrader_schemas.emplace_back(old_schema);
735
// TODO (tugsuu): make sure this is optimized later
736
for (const auto& schema : upgrader_schemas) {
737
schemas.push_back(&schema);
740
for (const auto method : builtin_functions) {
741
method->ensure_defined();
742
schemas.push_back(&method->getSchema());
745
// no operators found with the same name, print out similarly named operators
746
if (schemas.empty()) {
747
const auto close_symbols = findSimilarOperators(name);
748
auto error = ErrorReport(loc);
749
const auto& user_function_name = name.toQualString();
750
error << "Unknown builtin op: " << user_function_name << ".\n";
751
if (close_symbols.empty()) {
753
<< "Could not find any similar ops to " << user_function_name
754
<< ". This op may not exist or may not be currently supported in TorchScript.\n";
756
error << "Here are some suggestions: \n";
757
for (const auto& sym : close_symbols) {
758
error << "\t" << sym.toQualString() << "\n";
760
error << "\nThe original call is";
765
auto matched = matchSchemas(schemas, loc, graph, args, kwargs, self);
767
if (matched.first < variants.size() + upgrader_schemas.size()) {
768
return emitBuiltinNode(matched.second, loc, graph, name, graph_version);
770
auto& fn = *builtin_functions[matched.first - variants.size()];
771
// we inline builtin calls because they are normally very small
772
// wrappers and are not useful for keeping around to debug
774
graph, *toGraphFunction(fn).graph(), matched.second.inputs)
779
} // namespace torch::jit