Problem

Many of the current MXNet-Scala APIs are derived from the MXNet C++ backend, they are extracted using Scala Macros.

  • These APIs are not visible when used on an IDE since they are generated only at Compile time.  
  • The  parameters to these APIs are Key value arguments which forgoes type-safety with no documentation available,
  • The parameters are not type-safe since they are passed via a map of ["String", Any]
  •  User has no idea on what APIs are available or what parameters these APIs take, his best bet would be to look at Python API documentation on mxnet.apache.org and try out a similar approach in Scala. The current API looks something like the below.
Old Scala API
Symbol.Activation(name = "relu2")()(Map("data" -> fc2, "act_type" -> "relu"))


Proposed Approach

In this project, we will extract all the public APIs available from the C++ backend and generate a Scala trait for each module with the all APIs and the corresponding parameters. The concrete implementation of the module will extend the trait, the implementation comes from the existing Macro.

There will also be a new implementation on the Scala APIs which will be more user-friendly as shown below.

New Scala API
Symbol.Activation(Some(fc1), "relu", Some("relu2"))

Advantage for this API:

  • All arguments will be well documented for user to check.
  • Argument list will be fixed which avoid wrong args input comparing to the previous Scala API.
  • Less input from user required.

For example,  for NDArray APIs we will generate a file NDArrayBase.scala with NDArrayBase trait with all the API signature. NDArray class will extend this trait to provide the implementation using existing Macro or modified macro that uses the generated signature.

Alternative Approach considered

Create a functionBuilder and use a keyword build() to execute the function.

function_nameBuilder(requiredargs...)
	.arg1(arg1)
	.arg2(arg2)
	.arg3(arg3)
	.build()
 
class functionBuilder(requiredArg : String, map : mutable.Map[String, Any] = mutable.Map()) {

  def addArg1(arg1 : String) : functionBuilder = {
    map("arg1") = arg1
    new functionBuilder(requiredArg, map)
  }

  def addArg2(arg2 : Int) : functionBuilder = {
    map("arg2") = arg2
    new functionBuilder(requiredArg, map)
  }

  def build() : String = {
    "Build in progress"
  }
  
}

Advantage for this API:

  • Frustration free on many optional parameters
  • Clearer view to see how one function constructed
  • Better Java Support (if we have many arguments in a function that can be optional) 

 

Technical Challenges

There are four major difficulties we need to solve:

  • Extracting C++ APIs information and convert them into correct format in Scala.
  • Generating functions with arguments and docs
  • Handling different types of input and generating corresponding code

Here is an example on what it would look like for a single function:

Scala Sample: Activation
/**
  * Applies an activation function element-wise to the input.
  * 
  * The following activation functions are supported:
  * 
  * - `relu`: Rectified Linear Unit, :math:`y = max(x, 0)`
  * - `sigmoid`: :math:`y = \frac{1}{1 + exp(-x)}`
  * - `tanh`: Hyperbolic tangent, :math:`y = \frac{exp(x) - exp(-x)}{exp(x) + exp(-x)}`
  * - `softrelu`: Soft ReLU, or SoftPlus, :math:`y = log(1 + exp(x))`
  * - `softsign`: :math:`y = \frac{x}{1 + abs(x)}`
  * 
  * 
  * 
  * Defined in src/operator/nn/activation.cc:L150
  * @param data   The input array.
  * @param act_type   Activation function to be applied.
* @return Symbol
*/
def Activation(data : Option[Symbol], act_type : String, name : Option[String], attr : Option[Map[String, String]]) : Symbol= {

    val map : mutable.Map[String, Any] = mutable.Map()
    if (!data.isEmpty) map("data") = data.get
    map("act_type") = act_type
    val currName = name.getOrElse("")
    val currAttr = attr.getOrElse(Map())
    createSymbolGeneral("Activation", currName, currAttr, Seq(), map.toMap)

}

 

We are also facing some senario in our design:

A function in MXNet called FullyConnected, it contains 6 variables. If the 5th variable (no_bias) set to be true, then user don't need to pass in the 2nd and third variable. If that one set to be false, then user are required to pass in these variable. So it is nearly impossible to generate different override methods base on users' need. We have hundreds of methods like this and it will take a very long time. We hope if we can get this information from C++ APIs but unfortunately we don't have them in hand. In that case, a solution for this may just set all args to be None so user can choose to pass in or not. And Python API do the same thing https://mxnet.incubator.apache.org/api/python/symbol/symbol.html#mxnet.symbol.FullyConnected. In this case, we choose to set all optional args to None/Null by default in order to let user pass minimum input to the example.

Open Questions

Milestones

How do we generate the code

Code generation is essentially an important part in the MXNet installation section. There are currently two approaches: 

  1. Generate with write file.
  2. Generate with Macros

The second option are currently used inside of the MXNet Scala to generate the old APIs. However, more workload will be applied to new API as the arguments and implementation of the code are dynamic. As an example, the old API would be fixed into the format as shown below:

Old API Structure
Symbol.<function_name>(name : String = null, attr : Map[String, String] = null)(args : Seq[Symbol] = null)(kwargs : Map[String, Any]) : Symbol = {
	createSymbolGeneral(name, attr, args, kwargs)
}

The new API would look like:

Scala New API structure
Symbol.<function_name>(name : Option[String] = None, attr : Option[Map[String, String] = None, arg1 : Symbol, arg2 : MXFloat ...) : Symbol = {
	val map : mutable.Map[String, Any] = Mutable.Map()
	map.add("name", name)
	map.add("attr", attr)
	map.add("arg1_name", arg1)
	...
	functionGen(map.toMap)
}

We will need an additional function to handle these arguments called functionGen. It requires more complicated Tree-architecture design and a new Macros to implement.

 

How to combine with existing APIs

Because of the backwards support, we will not change the namespace of the existing APIs. There are currently three approaches we can take:

  • Add a new Class/Object called "Symbol.api._" with full implementation there
  • Create a new Class and change the name space for all of the functions (e.g Activation -> NewActivation) and let Symbol/NDArray extends that
  • Create a new Class and override the Same functions with different implementations.

After some research and discussion with Scala Interest Group, we found that the third approach will be nearly impossible to achieve. Here is a minimum reproduction of the problem of what we will be facing:

The problem we face
package scalaMig

object Test extends TestBase {
  
  def Activation(name : String = null)() = {
    println("Hello")
  }

  def main(args : Array[String]) = {
    Activation("hi")
  }


}

class TestBase {
  def Activation(data : Option[Int] = None) : Unit = {
    println("Hello World")
  }
}

This will cause the 

"Error:(3, 8) in object Test, multiple overloaded alternatives of method Activation define default arguments.
The members with defaults are defined in object Test in package scalaMig and class TestBase in package scalaMig.
object Test extends TestBase {"

This is a limitation of Scala language as it does not have a good support for overriding default args. If we need to solve this problem, we may need to create a bunch of override functions with different configuration. 


How to add API documentation

The current solution for this is to create a signature file contains abstract classes such as SymbolBase and NDArrayBase. These files will include the signature of the function as well as Function definition docs. These file can be used to generate the API site. The integration of the API is as shown as follows:

The problem we face
class NDArray extends NDArrayBase {
  val api : NDArrayAPIBase = NDArrayAPI
}


class NDArrayAPI extends NDArrayAPIBase {
}

As shown above, there will be in total 4 signature files generated through this process. We decided not to upload them as the size of them are massive. As an alternative, we create a hash check scheme to avoid operator changes. I used md5 hash in here:

  def MD5Generator(input : String) : String = {
    val md = MessageDigest.getInstance("MD5")
    md.update(input.getBytes("UTF-8"))
    val digest = md.digest()
    org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(digest)
  }

It basically take the Generated string into hash and store in this file. It should be the same across the different platform. The reason to do is is to avoid operator changes in Symbol and NDArray. One example, if one contributor changed the operators in NDArray and Symbol, he/she should comment the require line explained before in Scala package and update this file and also tag Scala Contributors to let them know there is a change in operator so we can determine if this will influence the user experience on Scala.

Glossary

 

References

  • No labels

2 Comments

  1. Dear all,

    This is my first 'contribution' if you can call it that, to an Apache project, so I'm not sure if this is the correct way of proceeding.

    I'm the author of Scorch (https://github.com/botkop/scorch), an imperative coding style neural net framework in Scala, much like PyTorch. If you look at the example on the README, you will see that the API is quite elegant, if I may say so. I was wondering if this could be something that can be labeled as an API usability improvement, as the title of this proposal suggests.

    Scorch makes use of Numsca (another project of mine, which can be found at https://github.com/botkop/numsca). It is very similar to NumPy, and is also written in Scala. Both Numsca and Scorch use ND4J as the underlying n-dimensional array layer. Just today, I started experimenting with MXNET, and I found that it not only provides practically everything ND4J has, but also seems to be a lot lot faster. So I'm thinking about rewriting both Numsca and Scorch using MXNET. 

    On the other hand, I would be more than happy to contribute to the Scala interface of MXNET instead. 

    Do you think Numsca and/or Scorch based on MXNET would be a useful contribution to MXNET? 

    Again, apologies if I'm barking up the wrong tree, and feel free to direct me to the proper channels.

    Thank you.

    Kind regards,

    Koen.

     

     

     

  2. Hi Koen,

    Welcome to Apache MXNet community. Thanks for your comments.

    The current Scala API are modeled after the Symbolic paradigm of programing, I think it would be best to make them better since there are users already using this interface. Would you be interested to collaborate on this? By working on this, you would also get exposure to the code base.

    Your work looks very interesting and we could collaborate further to see if it would add value to MXNet Scala users. Please free to join the MXNet channel on Slack, instructions are here

    Thanks for reaching out.