Skip to content

Convert multik-api, multik-jvm, multik-default to use multiplaform. Add native and js targets for multik-jvm #20

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 56 commits into from
Jan 13, 2022

Conversation

luca992
Copy link
Contributor

@luca992 luca992 commented Apr 25, 2021

Convert use of service loader to use standard reflection (probably wasn't necessary?)

Reflection for discovering Engines on native doesn't work so add an initEnginesProvider function that only has an implementation for native

updating //apply("$rootDir/gradle/publish.gradle") still needs to be done

There might be some fat jar issues. That probably should be looked into

luca992 added 2 commits April 24, 2021 23:02
…ugin

multik-jvm now also compiles for native. 
convert use of service loader to use standard reflection (probably wasn't necessary)
Reflection for discovering Engines on native doesn't work so add an initEnginesProvider function that only has an implementation for native

updating //apply("$rootDir/gradle/publish.gradle") still needs to be done
@luca992
Copy link
Contributor Author

luca992 commented May 3, 2021

#14 okio's new filesystem api would make allow this for multiplatform also

@devcrocod devcrocod linked an issue May 14, 2021 that may be closed by this pull request
@f7deleon
Copy link

hey is there an ETA for this?

@luca992
Copy link
Contributor Author

luca992 commented Sep 22, 2021

I pulled the v0.0.2 in. And I added JS as a target.

Someone might want to double check my conversions from Java.lang.System.arraycopy() to using using kotlin's kotlin.collections.copyInto are correct. (the multiplication of array indexes times 2 is confusing)

@luca992 luca992 changed the title Convert multik-api, multik-jvm, multik-default to use multiplaform. Add native targets for multik-jvm Convert multik-api, multik-jvm, multik-default to use multiplaform. Add native and js targets for multik-jvm Sep 23, 2021
@luca992
Copy link
Contributor Author

luca992 commented Sep 23, 2021

So I was able to get JS passing all but one test (excluding JvmLinAlgTest tests) after I reified a bunch of functions to determine NDArray data types on JS. I also simplified/fixed loading engines on js and other targets.

Most tests are passing on all targets. (haven't had time to compare if the tests that are failing are also failing on the v0.0.2 branch.

But, it would be nice if you guys consider reviewing this pull request as it's hard to keep merging work into since the source set folders are reorganized with the multiplatform plugin ( src/kotlin/main -> src/kotlin/commonMain)

@luca992
Copy link
Contributor Author

luca992 commented Dec 20, 2021

@luca992, I reviewed the multik-api. I am delighted with the work you have done.

Basically, I am confused by the initialization of the engines.

Have you tried using multiplatform multik in some projects?

@devcrocod
SO, basically it works as follows:
Engine's companion object handles loading engines and holding the reference to the default engine.

Loading the engines happens slightly differently per target:

JVM

After some debuggingEngine's companion object's init function appears to be called automatically when when the program is started (not lazily). Which is defined as:

if (enginesProvider.isNotEmpty()) {
    loadEngine()
}

enginesProvider is defined per target and for jvm I utilize reflection to find which engine classes are available:

public actual val enginesProvider : Map<EngineType, Engine> get() {
        val engineClassNames = listOf(
            "org.jetbrains.kotlinx.multik.jvm.JvmEngine",
            "org.jetbrains.kotlinx.multik.jni.NativeEngine",
            "org.jetbrains.kotlinx.multik.default.DefaultEngine")
        val engines = mutableMapOf<EngineType, Engine>()
        engineClassNames.forEach { e ->
            try {
                val instance = Class.forName(e).kotlin.createInstance() as Engine
                engines[instance.type] = instance
            } catch (t: Throwable){ }
        }
        return engines
    }

So essentially, the companion object's init function automatically loads the available engines on JVM and sets the default.

Native and JS

Reflection is not fully available on non-JVM targets. As far as I am aware of, there isn't a way to automatically locate and instantiate the available engines on those other targets.

Without this ability Engine's companion object's init will have no effect on these platforms[1]. Since, you can't really supply engines before this is called.

So, I created a workaround for non-jvm targets with a global initEnginesProvider function which is only defined on non-jvm targets.

private val _enginesProvider : HashMap<EngineType, Engine> = HashMap()

public actual val enginesProvider : Map<EngineType, Engine>
    get() = _enginesProvider

public actual fun initEnginesProvider(engines: List<Engine>) {
    engines.forEach {
        _enginesProvider[it.type] = it
    }
    Engine.loadEngine()
}

So, for non-jvm targets simply supply the global enginesProvider with the engines you want at the start of your program. Like initEnginesProvider(listOf(JvmEngine())). It will handle adding engines to the enginesProvider map without duplicates and call Engine.loadEngine() to set the default engine.

[1] Disclaimer: For native builds the companion object behavior appears to be different than jvm and js. After debuggingEngine's companion object's init function on native, it appears to be called lazily only when the companion object is called. So, it would actually be possible to set enginesProvider before Engine's companion object's init function is called on native. (However, I made the native and js implementations the same for consistency)

@luca992
Copy link
Contributor Author

luca992 commented Dec 21, 2021

@devcrocod so this is the js issue I worked around with the reified functions:

    inline fun <reified T: Any> printClass(num: T){
        println("printClass num:class ${num::class}")
        println("printClass reified ${T::class}")
    }

    @Test
    fun testtt(){
        val a = listOf(1.0f,2.1f)
        println("a[0] ${a[0]::class}")
        printClass(a[0])

        println("a[1] ${a[0]::class}")
        printClass(a[1])
    }

In js outputs:

a[0] class Float
printClass num:class class Int
printClass reified class Float
a[1] class Float
printClass num:class class Double
printClass reified class Float

where as jvm and native output as expected:

a[0] class kotlin.Float
printClass num:class class kotlin.Float
printClass reified class kotlin.Float
a[1] class kotlin.Float
printClass num:class class kotlin.Float
printClass reified class kotlin.Float

Created an issue: https://youtrack.jetbrains.com/issue/KT-50455

calling toCommonNDArray() causes it to lose datatype info on JS. workaround this by:
create two implementations of toCommonNDArray one which uses reified and one which requires a datatype
ndarrayCommon has a new optional dtype param to force datatype
revert most reified.
@luca992
Copy link
Contributor Author

luca992 commented Dec 22, 2021

So I managed to revert almost all the reified.

The main problem was that calling in JS buildsIterable.toCommonNDArray() causes it to lose type info of the iterable you call it on. So, for example, if 1.0f was the first element of the iterable when ndarrayCommon() tries to get datatype with DataType.of(elements.first()) it will actually return an int data type instead of float.

So I made a workaround by creating two implementations of toCommonNDArray one which uses reified and one which requires a datatype to pass to
ndarrayCommon() in a new optional dtype param to force datatype

@devcrocod
Copy link
Member

Hi @occultus73
I remember you asked to add kotlin/js.
Please tell us more about your use case.
What backend are you using?

@occultus73
Copy link

occultus73 commented Dec 23, 2021

My boss/client said he wanted the capability to deploy the baysian context bandit library (basically a generic machine learning model) I built on multiplatform with a colleague to JavaScript environments. What exactly he had in mind I'm not sure, but he did express interest in the file io capabilities that come with node.js; as this allows the bandit to save and resume its learning progress in matrix files (I built custom binary file i/o because we didn't have .npy API then).

@devcrocod
Copy link
Member

@luca992
Thanks for reading the js issue in more detail. Unfortunately, it seems that from the side of the kotlin/js team this will not be solved now, but let's see what they answer.

Using dtype is probably the only way for now. It won't help when our data comes from outside, but I think this is a good trade-off. I'll take a look at the related changes.

As for Engines and EnginesProvider.
I would like to make an equivalent replacement for SPI. Because in the future, multik-native and multik-default will also be multiplatform. I was prompted to see how it was done in ktor:
https://github.com/ktorio/ktor/blob/1.5.4/ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonFeature.kt#L24
https://github.com/ktorio/ktor/blob/1.5.4/ktor-client/ktor-client-features/ktor-client-json/ktor-client-serialization/js/src/SerializerInitializer.kt
https://github.com/ktorio/ktor/blob/1.5.4/ktor-client/ktor-client-features/ktor-client-json/ktor-client-serialization/posix/src/SerializerInitializer.kt

It seems to me that for the moment you can leave what you have done.

@luca992
Copy link
Contributor Author

luca992 commented Dec 24, 2021

@luca992 Thanks for reading the js issue in more detail. Unfortunately, it seems that from the side of the kotlin/js team this will not be solved now, but let's see what they answer.

Using dtype is probably the only way for now. It won't help when our data comes from outside, but I think this is a good trade-off. I'll take a look at the related changes.

As for Engines and EnginesProvider. I would like to make an equivalent replacement for SPI. Because in the future, multik-native and multik-default will also be multiplatform. I was prompted to see how it was done in ktor: https://github.com/ktorio/ktor/blob/1.5.4/ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonFeature.kt#L24 https://github.com/ktorio/ktor/blob/1.5.4/ktor-client/ktor-client-features/ktor-client-json/ktor-client-serialization/js/src/SerializerInitializer.kt https://github.com/ktorio/ktor/blob/1.5.4/ktor-client/ktor-client-features/ktor-client-json/ktor-client-serialization/posix/src/SerializerInitializer.kt

It seems to me that for the moment you can leave what you have done.

Interesting. I thought I tried a similar approach but noticed the init function of the objects weren't always called on all platforms. Looks like they get around that by assigning the object to some global variable. I could try and test that approach again

@devcrocod
Copy link
Member

I could try and test that approach again

At your discretion.
As I said so far, the current solution is a good place to start.

Copy link
Member

@devcrocod devcrocod left a comment

Choose a reason for hiding this comment

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

@luca992
I looked at other modules and new changes.
Globally, I have no new questions.

I think to do this: you will make all the small edits that remain, after that, I merge your changes. Everything related to publishing, dokka, initializing engines, and js, I will issue it as an issue, if you want, you can take them to work. This will make it easier to pour in the changes and it will be more convenient for me to participate. What do you think?

@luca992
Copy link
Contributor Author

luca992 commented Dec 29, 2021

@luca992 I looked at other modules and new changes. Globally, I have no new questions.

I think to do this: you will make all the small edits that remain, after that, I merge your changes. Everything related to publishing, dokka, initializing engines, and js, I will issue it as an issue, if you want, you can take them to work. This will make it easier to pour in the changes and it will be more convenient for me to participate. What do you think?

Sounds good to me. 👍 I'm still confused about layout of memory in complex arrays. But if you think what I did was wrong, addressing it as an issue works for me

@luca992
Copy link
Contributor Author

luca992 commented Dec 29, 2021

Also, I think I am going to remove the ThreadLocal annotations and just enable the new native memory model. It seems to me that fully adopting the new memory model is what most devs are doing these days. For reference:

https://twitter.com/kpgalligan/status/1474101883686068227?s=20

Dealing with the difficult to resolve concurrent memory issues in the old memory model is annoying to say the least

Copy link
Member

@devcrocod devcrocod left a comment

Choose a reason for hiding this comment

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

@luca992
I'm merging your changes.

As I said, I created issues for a number of problems, including complex arrays. With complex arrays, nothing complicated, I can fix it myself. The principle of complex arrays is simple, the real and imaginary parts are sequentially stored there, that is, for each step it is x2.

There was a problem while running the tests. When calling the clean task, I got an exception: java.lang.NullPointerException: destinationDir must not be null. I found the corresponding issue in ktor, it will be necessary to upgrade to 1.6.20 where it was corrected.
Otherwise, the library passed the tests, everything is fine.

@devcrocod devcrocod merged commit 239dd9f into Kotlin:v0.2.0 Jan 13, 2022
@luca992
Copy link
Contributor Author

luca992 commented Jan 13, 2022

Okay great! Now that it's merged I could take a look at porting multik-native when I get a chance if you guys don't beat me to it.

@devcrocod
Copy link
Member

To port multik-native, you first need to figure out how to build OpenBLAS and bundle it all.
Plus, besides, it will be necessary to check all this on the appropriate platforms.

@luca992
Copy link
Contributor Author

luca992 commented Jan 13, 2022

To port multik-native, you first need to figure out how to build OpenBLAS and bundle it all.

Plus, besides, it will be necessary to check all this on the appropriate platforms.

Yeah, it looks simple enough to build a shared lib for each target. I'd probably just create a new "libs" folder in the root that contains the shared libs and the headers. Then link it up with kotlin cinterop

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.

Kotlin multiplatform
5 participants