Skip to content

Support arguments in jitted functions#2164

Merged
coolreader18 merged 8 commits into
RustPython:masterfrom
BenLewis-Seequent:jit-args
Sep 1, 2020
Merged

Support arguments in jitted functions#2164
coolreader18 merged 8 commits into
RustPython:masterfrom
BenLewis-Seequent:jit-args

Conversation

@BenLewis-Seequent
Copy link
Copy Markdown

To jit functions that have arguments, the type of the arguments needs to be known when it's compiled. this PR uses the annotations to determine the type of the arguments.

Copy link
Copy Markdown
Member

@coolreader18 coolreader18 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

Comment thread jit/src/lib.rs Outdated
self.values,
)
};
let cif_args = values.iter().map(|v| libffi::middle::Arg::new(v)).collect();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're going to recollect the vec anyway, you may as well just v.assume_init() in the mapping function and avoid the transmute.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't use assume_init as I need the reference into the values vector, cif_args is effectively a vector of pointers that are passed to libffi. This would be nicer if we had either rust-lang/rust#63291 or rust-lang/rust#63568.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, gotcha

Comment thread vm/src/obj/objfunction.rs Outdated
#[cfg(feature = "jit")]
use super::objfloat;
#[cfg(feature = "jit")]
use super::objint;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit; you could condense this into one #[cfg jit] use super::{objint, objfloat}

Comment thread vm/src/obj/objfunction.rs
.get_or_try_init(|| {
rustpython_jit::compile(&self.code.code)
let arg_types = PyFunction::get_jit_arg_types(&zelf, vm)?;
rustpython_jit::compile(&zelf.code.code, &arg_types)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of code in objfunction, maybe there could be a submodule obj/objfunction/jitfunc.rs or something to store all the logic for py<->jit conversion?

Comment thread vm/src/obj/objfunction.rs Outdated
))
}
} else {
Err(vm.new_runtime_error(format!("argument {} needs annotation", name)))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we need new_jit_error

Comment thread jit/src/lib.rs Outdated
}
}

#[derive(Copy, Clone, PartialEq)]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I didn't make this Copy is because I was thinking of eventually having composite types, like List(Box<JitType>) or Tuple(Vec<JitType>) or maybe some sort of product or sum type.

Comment thread jit/src/lib.rs Outdated
impl<'a> ArgsBuilder<'a> {
fn new(code: &'a CompiledCode) -> ArgsBuilder<'a> {
ArgsBuilder {
values: vec![MaybeUninit::uninit(); code.sig.args.len()],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this be Vec<Option<UntypedAbiArg>>? and you could like do self.values.iter().map(|val| val.as_ref().map(|val| middle::Abi::new(val)).collect::<Option<_>>()

And you could just store the Vec<Option<>> in the Args struct, since it just matters that it's not dropped

@ArniDagur
Copy link
Copy Markdown
Contributor

ArniDagur commented Aug 30, 2020

An example like the following can cause UB, where the function is given incorrect input:

def add_two(a: int, b: int) -> int:
    return a + b

add_two.__jit__()
result = add_two(2, 3.0)  # cpython would return 5.0 as a float

Here, I give a function that should take an int a float instead. CPython doesn't complain because it doesn't actually look at the type annotations. This branch understandably gives a nonsense result:

[arni][~/src/RustPython][jit-args]% target/release/rustpython
Welcome to the magnificent Rust Python 0.1.2 interpreter 😱 🖖
>>>>> def add_two(a: int, b: int) -> int:
..... 	return a + b
.....
>>>>> add_two.__jit__()
>>>>> add_two(2, 3.0)
4613937818241073154
>>>>> add_two(2, 3)
5
>>>>> def return_float() -> float:
..... 	return 3.0
.....
>>>>> add_two(2, return_float())
4613937818241073154

I was wondering if we're okay with this sort of undefined behaviour when incorrect input is given to jitted functions?

If not, we could either

  1. Do a runtime check of some sort; or
  2. In the native code we output, insert calls to the same VM methods that would have been executed if it was bytecode. These can handle data of unknown type. It would still improve performance because we wouldn't have to match on the instruction every time, right? You'd only go through the match statements in https://github.com/RustPython/RustPython/blob/master/vm/src/frame.rs#L319 once. When working with data whose type we can determine statically, such as the output of in-built functions such as range(...) we could then output more specialized native code as we do in this PR. Perhaps this requires a change in the bytecode format to include type information when it's known?

I'm not sure if 2. makes sense since I don't quite know how RustPython or interpreters in general work.

@darleybarreto
Copy link
Copy Markdown

I was wondering if we're okay with this sort of undefined behaviour when incorrect input is given to jitted functions?

If not, we could either

  1. Do a runtime check of some sort; or
  2. In the native code we output, insert calls to the same VM methods that would have been executed if it was bytecode. These can handle data of unknown type. It would still improve performance because we wouldn't have to match on the instruction every time, right? You'd only go through the match statements in https://github.com/RustPython/RustPython/blob/master/vm/src/frame.rs#L319 once. When working with data whose type we can determine statically, such as the output of in-built functions such as range(...) we could then output more specialized native code as we do in this PR. Perhaps this requires a change in the bytecode format to include type information when it's known?

Another option is to use some sort of multiple dispatch, as Julia does. Which also implies introducing some sort of type inference system and other complex things... Perhaps it would be easier to just throw an error for now, and eventually introduce more complicated machinery.

@coolreader18
Copy link
Copy Markdown
Member

I think ideally we're trying to make a jitted function transparent -- it should be no different in behavior from a non-jitted function, and it shouldn't error because of a jit limitation (e.g. i64 ints instead of BigInts). So yeah, I think we should definitely type-check and then fall back to non-jit behavior to either error or get whatever dynamic behavior would happen with that.

@coolreader18
Copy link
Copy Markdown
Member

Doing something similar to Julia's multiple dispatch definitely sounds cool though -- with

def add(a, b):
    return a + b

We could have like a hashmap of Vec<JitType> -> CompiledCode, and when it's called with e.g. (int, int), we try compiling with a as an int and b as an int, and if that succeeds we populate the hashmap with that signature. The same thing could happen for (float, float), or (float, int), or (int, float), etc

@BenLewis-Seequent
Copy link
Copy Markdown
Author

@ArniDagur Good catch, this PR did previously do a runtime check of the arguments types, and if it didn't match it would fallback to interpreting the bytecode instead, but a subsequent refactoring broke that.

@darleybarreto @coolreader18 I'm not familiar with Julia at all, but does sound like an interesting approach.

@youknowone
Copy link
Copy Markdown
Member

By looking PyPy practice, it make guards with typecheck and falling back to original code when the jit assumption failed.

@coolreader18 coolreader18 merged commit 3b2a16f into RustPython:master Sep 1, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants