chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# SameDiff file format proposal
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
Proposed by: Alex Black (15-05-2020)
|
||||
|
||||
Discussed with: raver119
|
||||
|
||||
## Context
|
||||
|
||||
SameDiff models need to be serializable - i.e., something we can save to disk or send over the network.
|
||||
Additionally, we need to be able to save and load model files in C++, and have those be readable in other languages (mainly Java).
|
||||
|
||||
Currently, we have a FlatBuffers-based format for SameDiff graph serialization, but it has a number of problems, as discussed in this issue: https://github.com/eclipse/deeplearning4j/issues/8312
|
||||
|
||||
|
||||
## Decision
|
||||
|
||||
We will transition from a pure FlatBuffers to a Zip + FlatBuffers model format.
|
||||
|
||||
FlatBuffers will be used for the graph structure only. Parameters will be stored separately to the graph structure, also within the zip.
|
||||
|
||||
We will introduce the ability to support multiple versions of a graph in the model files.
|
||||
This will enable the model file to support storing
|
||||
* Multiple data types (for example, a FP32 version and a quantized INT8 version)
|
||||
* Multiple different checkpoints (parameters after 1000 iterations, after 5000, and so on)
|
||||
* Multiple versions of a given model (English vs. Chinese, or cased/uncased, etc)
|
||||
|
||||
By default when loading a graph (unless it is otherwise specified) we will load the most recent model tag.
|
||||
Tags must be valid file/folder identifiers, and are not case sensitive.
|
||||
|
||||
|
||||
The structure of the zip file will be as follows:
|
||||
```
|
||||
tags.txt //List of graph tags, one per line, in UTF8 format, no duplicates. Oldest first, newest last
|
||||
<tag_name>/graph.fb //The graph structure, in FlatBuffers format
|
||||
<tag_name>/params.txt //The mapping between variable names and parameter file names
|
||||
<tag_name>/params/*.fb //The set of NDArrays that are the parameters, in FlatBuffers format
|
||||
<tag_name>/trainingConfig.fb //The training configuration - updater, learning rate, etc
|
||||
<tag_name>/updater.txt //The mapping between variable names and the updater state file names
|
||||
<tag_name>/updater/*.fb //The set of NDArrays that are the updater state
|
||||
```
|
||||
|
||||
Note that params.txt will allow for parameter sharing via references to other parameters:
|
||||
```
|
||||
my_normal_param 0
|
||||
shared_param <other_tag_name>/7
|
||||
```
|
||||
This means the parameters values for parameter "my_normal_param" are present at `<tag_name>/params/0.fb` within the zip file, and the parameter values for "shared_param" are available at `<other_tag_name>/params/7.fb`
|
||||
|
||||
Note also that the motivation for using the params.txt file (instead of the raw parameter name as the file name) is that some parameters will have invalid or ambiguous file names - "my/param/name", "&MyParam*" etc
|
||||
|
||||
In terms of updater state, they will be stored in a similar format. For example, for the Adam updater with the M and V state arrays (each of same shape as the parameter):
|
||||
```
|
||||
my_param 0 1
|
||||
other_param 2 3
|
||||
```
|
||||
That means my_param(M) is `<tag_name>/updater/0.fb` and my_param(V) is at `<tag_name>/updater/1.fb`
|
||||
This format also allows for updater state sharing, if we need it.
|
||||
|
||||
|
||||
**Graph Structure**
|
||||
|
||||
The graph structure will be encoded in FlatBuffers format using a schema with 2 parts:
|
||||
1. A list of variables - each with name, datatype, and (for placeholders, constants and parameters) a shape
|
||||
2. A list of operations - each with a name, op name/type, input variable names, output variable names, and arguments
|
||||
|
||||
Note that both legacy and custom ops will be encoded in the same way. For legacy ops, we simply need the operation type, and the operation number.
|
||||
|
||||
Operation argument encoding will be done using named arguments: essentially, a `Map<String,T>` structure, where T is one of `{long, double, boolean, datatype}`.
|
||||
This allows for improved backward compatibility (no ambiguity as ops are modified after a graph file was written) and improved interpretability compared to using simple arrays of iargs, bargs, targs and dargs.
|
||||
One consequence/downside of this is that we need to define mapping between our named arguments and iargs/bargs/targs/dargs. In Java we have essentially done this manually, though clearly don't want to replicate this work in C++ (or any future languages).
|
||||
|
||||
To avoid the need to do a significant amount of work (such as moving the name/arg mapping to code generation) the following is proposed:
|
||||
The `Map<String,T>` is split up in the FlatBuffers schema into 4 pairs of fields.
|
||||
* `String[] iArgNames`, `long[] iArgs`
|
||||
* `String[] tArgNames`, `double[] dArgs`
|
||||
* `String[] bArgNames`, `boolean[] bArgs`
|
||||
* `String[] dArgNames`, `DataType[] dArgs`
|
||||
|
||||
Clearly the name and value arrays (for each pair) would each be the same length, and name/value correspondence is by array index.
|
||||
|
||||
This is essentially equivalent to the `Map<String,T>` representation, but has the benefit of not needing us to define the mapping for named args to array-style args any time soon in C++, but also allowing us to add it in the future (mainly before we can write graphs from C++, or have better/proper backward compatibility after op changes)
|
||||
|
||||
|
||||
**Extensibility to Other Types**
|
||||
|
||||
Suppose in the future we want to store other data for a variable, not just an array?
|
||||
Examples include lists and maps (for example, for NLP applications).
|
||||
|
||||
While we will not implement this right now, there are a number of options for adding this without breaking backward compatibility.
|
||||
|
||||
First: we can enhance the params.txt file format, perhaps using something like the following:
|
||||
```
|
||||
map_param 0 MAP
|
||||
```
|
||||
|
||||
Second: We can add a similar text file for other types. For example, a params_maps.txt, same format as params.txt, with content at `<tag_name>/params_maps/*.fb`
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Onnx runtime module
|
||||
|
||||
## Status
|
||||
Implemented
|
||||
|
||||
Proposed by: Adam Gibson (23-09-2020)
|
||||
|
||||
Discussed with: saudet
|
||||
|
||||
## Context
|
||||
|
||||
We need a way of providing nd4j a way of running onnx modules
|
||||
that is easily compatible with the onnx community. The gold standard for this
|
||||
is is using [onnxruntime](https://github.com/microsoft/onnxruntime/blob/master/docs/Java_API.md).
|
||||
|
||||
|
||||
## Decision
|
||||
|
||||
We will use javacpp's onnxruntime bindings in a similar manner to [nd4j-tensorflow](../nd4j-tensorflow)
|
||||
allowing nd4j to be used as an ndarray format that interops with onnxruntime.
|
||||
|
||||
We will implement a simple api similar to the [GraphRunner](../nd4j-tensorflow/src/main/java/org/nd4j/tensorflow/conversion/graphrunner/GraphRunner.java)
|
||||
This will sit on top of javacpp's lower level onnxruntime bindings.
|
||||
|
||||
This module will follow a similar structure to the nd4j-tensorflow module
|
||||
focusing on INDArrays as a data interchange format, but otherwise pass execution
|
||||
down to onnxruntime.
|
||||
|
||||
|
||||
The main api to the graph runner works as follows:
|
||||
|
||||
```java
|
||||
try(GraphRunner runner = new GraphRunner(...)) {
|
||||
Map<String,INDArray> inputs = new HashMap<>();
|
||||
// ..initialize inputs
|
||||
Map<String,INDArray> outputs = runner.run(inputs);
|
||||
// process outputs...
|
||||
}
|
||||
```
|
||||
|
||||
The core logic will contain the following components:
|
||||
|
||||
1. Loading onnx pb files
|
||||
2. A graph runner in similar nature to nd4j-tensorflow
|
||||
3. Interop with onnxruntime's version of an ndarray/tensor
|
||||
|
||||
Using different accelerators/backends
|
||||
-----------------------------------------
|
||||
|
||||
Similar to nd4j-tensorflow which uses javacpp for the specific version of
|
||||
tensorflow to use, this module will rely on the user picking the right dependency
|
||||
to link against. Different builds of cpu, gpu, .. exist [here](https://repo1.maven.org/maven2/org/bytedeco/tensorflow/1.15.3-1.5.4/)
|
||||
The equivalent of this in onnxruntime can be found [here](https://repo1.maven.org/maven2/org/bytedeco/onnxruntime/1.4.0-1.5.4/)
|
||||
|
||||
The user will need to include the version of onnxruntime they wish to use
|
||||
similar to how you link against a particular implementation in a c library
|
||||
or include a backend in nd4j. This will happen via maven.
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
# Import IR
|
||||
|
||||
## Status
|
||||
|
||||
Implemented
|
||||
|
||||
Proposed by: Adam Gibson (28-09-2020)
|
||||
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
## Context
|
||||
|
||||
Currently, there is a gap in the way samediff/nd4j operations are implemented
|
||||
vs. how other frameworks represent their models.
|
||||
|
||||
Keras, Tensorflow, and Pytorch use an attribute based format with names. Interop
|
||||
between Onnx ,Tensorflow, and Keras tends to follow the following formula:
|
||||
|
||||
1. Map names to equivalent names in the other framework for each operation
|
||||
configuration. Names being both op names and associated attributes of the
|
||||
operations such as in Conv2D where you have strides, kernel sizes.
|
||||
2. Map input/output tensors to the equivalent tensor type in each framework.
|
||||
3. Setup the complete graph in the equivalent framework. Sometimes the
|
||||
framework's concepts don't map 1 to 1. They should output equivalent results
|
||||
regardless though. In order to do this, sometimes the framework needs to
|
||||
add/remove operations in order to produce equivalent output in a different
|
||||
graph. The [tensorflow onnx import](https://github.com/onnx/tensorflow-onnx#how-tf2onnx-works)
|
||||
is a good example of this.
|
||||
|
||||
Samediff/nd4j have their internal op representations as a set of ordered
|
||||
arguments for execution in the form of:
|
||||
|
||||
1. t arguments: floating point arguments (float, double,..)
|
||||
2. integer arguments: integer arguments (long, integer)
|
||||
3. boolean argument: boolean arguments
|
||||
4. data type arguments: data types for input/output
|
||||
5. input arguments: ndarrays for input
|
||||
6. output arguments: often optional (dynamically created) output ndarray
|
||||
arguments. If the user wants to pass in outputs to control memory, they are
|
||||
allowed to do so.
|
||||
7. axis arguments: Integer arguments that represent the dimension(s) for an
|
||||
operation to be executed on.
|
||||
|
||||
[Reference implementation](https://github.com/KonduitAI/deeplearning4j/blob/master/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/api/ops/DynamicCustomOp.java#L58)
|
||||
|
||||
This maps well enough for execution, but not for file formats.
|
||||
|
||||
## Related Work
|
||||
This may encourage future work to be done to the
|
||||
[samediff file format](https://github.com/KonduitAI/deeplearning4j/blob/master/nd4j/ADRs/0001-SameDiff_File_Format.md).
|
||||
Implementation of serialization of file format via flatbuffers can be found
|
||||
[here](https://github.com/eclipse/deeplearning4j/blob/master/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/autodiff/samediff/SameDiff.java#L4748)
|
||||
Of note here for prior work is the
|
||||
[current code generation]
|
||||
(https://github.com/KonduitAI/dl4j-dev-tools/blob/master/codegen/src/main/ops/org/nd4j/codegen/ops/CNN.kt#L28)
|
||||
|
||||
The definitions for the kotlin dsl can be found
|
||||
[here](https://github.com/KonduitAI/dl4j-dev-tools/blob/master/codegen/src/main/kotlin/org/nd4j/codegen/dsl/OpBuilder.kt)
|
||||
|
||||
|
||||
While it does have the intended description,
|
||||
it’s kotlin specific and is only available for a very small subset
|
||||
of the ops where pre-created objects were created
|
||||
for specific operations. The goal of this ADR is to expand upon
|
||||
that and make it language agnostic by providing this information in a
|
||||
neutral file format that has code generation with it.
|
||||
|
||||
Current code generation efforts can be augmented using this file format.
|
||||
More on this decision making can be found [here](https://github.com/KonduitAI/dl4j-dev-tools/blob/master/codegen/adr/0007-configuration_objects.md)
|
||||
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
We expose a symbol based mapping in libnd4j in protobuf format, similar to how
|
||||
other frameworks are doing it, as a bridge/intermediary format.
|
||||
|
||||
This makes it easier to implement interop with the other frameworks, because it
|
||||
adds the necessary information that is needed to be able to define a direct
|
||||
mapping.
|
||||
|
||||
This could be a future file format depending on how the framework evolves. For
|
||||
now, this is considered a work around for making writing import code easier/more
|
||||
portable.
|
||||
|
||||
Similar to [ONNX](https://onnx.ai/) and [Tensorflow](https://tensorflow.org/)
|
||||
we use protobuf to express an attribute based file format and map
|
||||
samediff/nd4j operations to this format.
|
||||
|
||||
We use a translation layer that handles mapping from attributes to the ordered
|
||||
arguments approach reflected in samediff/nd4j.
|
||||
|
||||
For each operation, we define a mapping process to/from this attribute format to the
|
||||
order based execution format.
|
||||
|
||||
A separate but similar set of rules are used for mapping ndarrays.
|
||||
|
||||
This attribute based format is an Intermediary Representation that we then
|
||||
"compile" to the equivalent calls in libnd4j.
|
||||
|
||||
|
||||
The format definitions for the IR can be found [here](./src/main/proto/nd4j/nd4j.proto)
|
||||
|
||||
## Consequences
|
||||
|
||||
Migration to an attribute based import format makes working with other deep
|
||||
learning frameworks easier in the future.
|
||||
|
||||
|
||||
### Drawbacks
|
||||
|
||||
1. Yet another file format.
|
||||
2. Risk migrating to new file format in the future.
|
||||
3. A lot of up front manual work to index set of current operations.
|
||||
4. Backwards compatibility: yet another thing to maintain. We wrote converters
|
||||
for any forward compatibility. We address this by specifying an opset schema
|
||||
scheme similar to onnx.
|
||||
|
||||
### Advantages
|
||||
|
||||
1. Easy to maintain.
|
||||
2. Backwards compatible.
|
||||
3. Easily interops with existing other deep learning frameworks.
|
||||
4. No additional dependencies from what's already normal.
|
||||
5. Protobuf allows easy code generation for other languages.
|
||||
6. Industry standard conventions being used over proprietary tooling reducing
|
||||
friction for adoption for people coming from other frameworks
|
||||
7. Straightforward mapping of arguments for import
|
||||
8. Provide an easy bridge to existing libnd4j
|
||||
9. Allow automation of op descriptors in any language that would understand how
|
||||
to pass data to the c++ library.
|
||||
|
||||
|
||||
## Appendix A: Comparison with other Frameworks, implicit vs. explicit
|
||||
|
||||
We can find the existing attributes from the conventions of the
|
||||
libnd4j code base. The libnd4j [conv1d.cpp](https://github.com/KonduitAI/deeplearning4j/blob/master/libnd4j/include/ops/declarable/generic/nn/convo/conv1d.cpp#L104)
|
||||
file contains the following declaration:
|
||||
|
||||
```
|
||||
auto inputShapeInfo = inputShape->at(0);
|
||||
auto weightsShapeInfo = inputShape->at(1);
|
||||
sd::LongType const* biasShapeInfo = block.width() > 2 ? inputShape->at(2) : nullptr;
|
||||
|
||||
int kW = INT_ARG(0) > 0 ? INT_ARG(0) : static_cast<int>(shape::sizeAt(weightsShapeInfo, 0)); // filter(kernel) width
|
||||
int sW = INT_ARG(1); // strides width
|
||||
int pW = INT_ARG(2); // paddings width
|
||||
int dW = INT_ARG(3); // dilations width
|
||||
int paddingMode = INT_ARG(4); // 0-VALID, 1-SAME
|
||||
int isNCW = block.getIArguments()->size() > 5 ? !INT_ARG(5) : 1; // INT_ARG(4): 1-NWC, 0-NCW
|
||||
int wFormat = block.getIArguments()->size() > 6 ? INT_ARG(6) : 0; // 0 - [kW, iC, oC], 1 - [oC, iC, kW], 2 - [oC, kW, iC]
|
||||
```
|
||||
|
||||
We can see that there are macros in the libnd4j code base, which reflect how
|
||||
each argument is accessed. Each list of arguments has an expected order, that we
|
||||
need to explicitly map to a parseable structure.
|
||||
|
||||
In comparison, the
|
||||
[onnx Convolution operator](https://github.com/onnx/onnx/blob/master/docs/Operators.md#Conv)
|
||||
has *explicit* attributes of various types such as lists of ints and named
|
||||
tensors.
|
||||
|
||||
As shown above, these concepts exist internally in the operations and layers
|
||||
themselves in nd4j/samediff, but they are not exposed directly to the user.
|
||||
|
||||
|
||||
A theoretical op descriptor from libnd4j is as follows:
|
||||
```java
|
||||
private String name;
|
||||
private int nIn,nOut,tArgs,iArgs;
|
||||
private boolean inplaceAble;
|
||||
private List<String> inArgNames;
|
||||
private List<String> outArgNames;
|
||||
private List<String> tArgNames;
|
||||
private List<String> iArgNames;
|
||||
private List<String> bArgNames;
|
||||
private OpDeclarationType opDeclarationType;
|
||||
|
||||
public enum OpDeclarationType {
|
||||
CUSTOM_OP_IMPL,
|
||||
BOOLEAN_OP_IMPL,
|
||||
LIST_OP_IMPL,
|
||||
LOGIC_OP_IMPL,
|
||||
OP_IMPL,
|
||||
DIVERGENT_OP_IMPL,
|
||||
CONFIGURABLE_OP_IMPL,
|
||||
REDUCTION_OP_IMPL,
|
||||
BROADCASTABLE_OP_IMPL,
|
||||
BROADCASTABLE_BOOL_OP_IMPL
|
||||
}
|
||||
```
|
||||
|
||||
It contains all the op declarations and fields associated with a descriptor.
|
||||
|
||||
In the libnd4j code base, we represent the op descriptor types above
|
||||
*implicitly* through validation as well as the different macros present in the
|
||||
code base representing what an op execution looks like.
|
||||
|
||||
Validation for what can be present in the various names can be found
|
||||
[here](https://github.com/KonduitAI/deeplearning4j/blob/master/libnd4j/include/ops/declarable/impl/DeclarableOp.cpp#L734-L765)
|
||||
|
||||
The set of macro declarations in libnd4j can be found
|
||||
[here](https://github.com/eclipse/deeplearning4j/blob/master/libnd4j/include/system/op_boilerplate.h)
|
||||
|
||||
|
||||
## Appendix B: Format Comparison to other frameworks
|
||||
|
||||
An add op in tensorflow looks like:
|
||||
|
||||
```
|
||||
op {
|
||||
name: "Add"
|
||||
input_arg {
|
||||
name: "x"
|
||||
type_attr: "T"
|
||||
}
|
||||
input_arg {
|
||||
name: "y"
|
||||
type_attr: "T"
|
||||
}
|
||||
output_arg {
|
||||
name: "z"
|
||||
type_attr: "T"
|
||||
}
|
||||
attr {
|
||||
name: "T"
|
||||
type: "type"
|
||||
allowed_values {
|
||||
list {
|
||||
type: DT_BFLOAT16
|
||||
type: DT_HALF
|
||||
type: DT_FLOAT
|
||||
type: DT_DOUBLE
|
||||
type: DT_UINT8
|
||||
type: DT_INT8
|
||||
type: DT_INT16
|
||||
type: DT_INT32
|
||||
type: DT_INT64
|
||||
type: DT_COMPLEX64
|
||||
type: DT_COMPLEX128
|
||||
type: DT_STRING
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Onnx’s add can be found here
|
||||
https://github.com/onnx/onnx/blob/master/docs/Operators.md#Add
|
||||
|
||||
Onnx and tensorflow are purely attribute based formats.
|
||||
@@ -0,0 +1,195 @@
|
||||
|
||||
# Libnd4j NdArray padded buffers, strides for Arm_Compute Library wrapper
|
||||
|
||||
## Status
|
||||
Implemented
|
||||
|
||||
Proposed by: Abdelrauf (23/09/2020)
|
||||
|
||||
Discussed with:
|
||||
|
||||
## Context
|
||||
During the integration process of our library with arm_compute, I faced that our NdArray strides are not flexible. (i.e it cant be set properly without **special and manual handling**).
|
||||
Let's say our Nd Array shapes are `[3,4,2]` and the last index is moving faster (i.e C order). Then our strides will be `[ 8, 2, 1 ]`.
|
||||
As far as I know, our last index stride can be different (called as ews), but overall strides should follow the cyclic strict rule of dependency.:
|
||||
|
||||
strides[index-1] = strides[index] * shapes[index];
|
||||
On arm_compute besides strides there is also Padding `{top, right, bottom, left}` that can be used to increase strides and change offsets adn as well as total size. its mostly done for performance reasons. As from above we can see that **its just hosting NdArray shape in the buffer of the bigger NdArray shape**. In arm_compute those paddings applied to last 2 dimensions (on NCHW it will be H and W}. We can define it like this:
|
||||
|
||||
newH = pad.top + H + pad.bottom;
|
||||
newW = pad.left + W + pad.right;
|
||||
|
||||
so strides will be calculated for the shape `{N,C, newH, newW}` and offset of the first element will be:
|
||||
|
||||
offset = pad.left * strideOfNewW + pad.top * strideOfNewH
|
||||
|
||||
|
||||
## Proposal
|
||||
Introduce helper functions checking below case :
|
||||
|
||||
strides[index-1] >= strides[index] * shapes[index];
|
||||
|
||||
Add **generic method for the padded buffer** ( we can simulate arm_compute 2d padding and more)
|
||||
|
||||
int paddings[rank] = {...}; // total padding
|
||||
int paddingOffsets[rank] = {...}; //offset indices of the first element
|
||||
|
||||
This could be used to padd ndArray shapes and calculate strides based on it while keeping original shape, paddOffsets could be used to determine the beginning of the first element. Though this interface ismore generic its drawback is that on armcompute its possible to padd 1d into 2D while keeping rank but on this one we should supply 2d with one of its dimensions being 1.
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
1. All tests that were not tested **against subArray** could break. So they will require a fix
|
||||
2. Writing additional test cases
|
||||
|
||||
### Advantages
|
||||
- alignment possibility for CPUs where alignment is required for speed and vectorization.
|
||||
- easier integration with libraries. in the case of arm_compute, the last two dimensions are sometimes padded.
|
||||
|
||||
|
||||
### Disadvantages
|
||||
- its advantage is not so big for modern CPUs where unaligned vector loads possible
|
||||
- exposing it for users is not desirable: (excessive usage creates unnecessary memory spaces and performance problems)
|
||||
- could result in unnecessary complications for some function implementations
|
||||
- possibility of requiring additional tests and fixes
|
||||
|
||||
|
||||
### Technical details about the addition of this functionality into NdArray
|
||||
A little investigation showed that the current NdArray actually has constructors to specify strides.
|
||||
Here is the constructor that could be used
|
||||
[ShapeDescriptor.h](https://github.com/KonduitAI/deeplearning4j/blob/qwr_armcompute/libnd4j/include/array/ShapeDescriptor.h)
|
||||
Here are additions into ShapeDescriptor:
|
||||
- validate() //it willbe used for validation of strides and et cetera. This way we can create NdArray by just using ShapeDescriptor alone. And it will be more flexible with correctness
|
||||
- allocLength() //returns minimal buffer size for the given strides and shapes. (this was missing on libnd4j side)
|
||||
- paddedBufferDescriptor(..) //helper method for returning ShapeDescriptor for padded buffer.
|
||||
|
||||
|
||||
|
||||
#### [NdArrayFactory](https://github.com/KonduitAI/deeplearning4j/blob/qwr_armcompute/libnd4j/include/array/impl/NDArrayFactory.cpp#L39-L80)
|
||||
The method that is using ShapeDescriptor validation, and ShapeDescriptor paddedBuffer .
|
||||
|
||||
Furthermore to indicate that shape of the NdArray is using paddedBuffer we will flag with `ARRAY_HAS_PADDED_BUFFER` . so it will be possible to know if NdArray is padded.
|
||||
|
||||
Furthermore, it is still possible to recover Paddings from the allocation size of the padded NdArray. But its not an easy task to get PaddingOffsets from offset and recovered full shape. Thats why it requires storing them. Fortunately, for arm_compute tensors **manual padding** we just need to know **total size and the offset** of the first element. So we dont need to change internals that much
|
||||
|
||||
As our padded Buffer follows the strict ews() rule instead of the loose one. Paddings will be obtained from this rule:
|
||||
|
||||
strides[index-1] == strides[index] * shapes[index];
|
||||
|
||||
pseudo code for C order:
|
||||
|
||||
for (int j = rank - 1; j >= 0; j--) {
|
||||
shapesAfterPadding[j] = strides[j - 1] / strides[j]
|
||||
}
|
||||
shapesAfterPadding[0] = buffer.AllocSize / strides[0]
|
||||
//Paddings for index in 0..rank-1
|
||||
paddings[index] = shapesAfterPadding[index] - shape[index]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Technical notes on arm_compute library
|
||||
|
||||
The main drive for the above proposal to avoid unnecessary performance and memory allocation. And also we should keep on mind :
|
||||
- in each newer version of arm_compute there are new implementations in which the padding requirements were removed.
|
||||
|
||||
This **can diminish the necessity for the proposed changes** if such versions of the desired functions are implemented.
|
||||
|
||||
##### Notes on arm_compute tensors
|
||||
Arm_compute tensors are mostly 3d 4d with max 6d dimensions.
|
||||
So lets show C order NdArray({2,2,5,5},)
|
||||
|
||||
shapeInfo shapeInfo: [4, 2,2,5,5, 50,25,5,1, 8192,1,99]
|
||||
|
||||
of float type and its arm_compute tensor equivalent :
|
||||
- first of all, we map NdArray dataTypes into arm_compute [armcomputeUtils.cpp#L35-L75](https://github.com/KonduitAI/deeplearning4j/blob/qwr_armcompute/libnd4j/include/ops/declarable/platform/armcompute/armcomputeUtils.cpp#L35-L75)
|
||||
- it will be with the reversed shape. **`NdArray{n,z,y,x} -> TensorShape{x,y,z,n}`**
|
||||
-
|
||||
|
||||
total length in bytes: 400
|
||||
shapes: 5,5,2,2,1,1,
|
||||
strides in bytes: 4,20,100,200,0,0,
|
||||
strides as elements: (1,5,25,50)
|
||||
|
||||
Paddings in arm_compute Tensors. `Padding{left,right, top, bottom}`
|
||||
As both OpenCL and NEON use vector loads and stores instructions to access the data in buffers, so in order to avoid having special cases to handle for the borders all the images and tensors used in this library must be padded
|
||||
There are different ways padding can be calculated:
|
||||
|
||||
- Accurate padding.
|
||||
in this case it is importan to configure and then after that to allocate
|
||||
- auto padding.
|
||||
It guarantees that the allocation will have enough padding to run any of the provided functions
|
||||
- no padding
|
||||
- manual padding
|
||||
|
||||
#### how padding affects strides offset and total size
|
||||
in arm_compute Tensor:
|
||||
it's 2d {Width Height} can be padded and thats why it affects strides.
|
||||
Lets show it with the picture:
|
||||
|
||||
\ top /
|
||||
\ _____________________ /
|
||||
left | ^ | right
|
||||
| Width |
|
||||
| <-Height |
|
||||
| |
|
||||
| |
|
||||
----------------------
|
||||
/ bottom \
|
||||
/ \
|
||||
|
||||
Here is the stride calculation pseudo code for Tensor {x,y,z}
|
||||
|
||||
stride_x = element_size(); //float will be 4
|
||||
stride_y = (padding.left + _tensor_shape[0] + padding.right) * stride_x;
|
||||
stride_z = (padding.top + _tensor_shape[1] + padding.bottom) * stride_y;
|
||||
|
||||
required_offset_first_element = padding.left * stride_x + padding.top * stride_y;
|
||||
|
||||
|
||||
For example: if arm_tensor had `padding: left 0, right 1, top 0, bottom 1` :
|
||||
|
||||
total: 576
|
||||
shapes: 5,5,2,2,1,1,
|
||||
strides in bytes: 4,24,144,288,0,0,
|
||||
|
||||
|
||||
|
||||
### Notes on the current wrapper implementation
|
||||
|
||||
This is a simple wrapper for arm functions with input and output tensors:
|
||||
[armcomputeUtils.h#L95-L165](https://github.com/KonduitAI/deeplearning4j/blob/qwr_armcompute/libnd4j/include/ops/declarable/platform/armcompute/armcomputeUtils.h#L85-L133)
|
||||
|
||||
From above we could see :
|
||||
- we had to flag padded NdArrays so that we can use manual padding version of arm_compute Tensors
|
||||
- when padding information is changed during configure process we **have to copy** our NdArray buffer into **new allocated** arm_tensor buffer. and the same with the output.
|
||||
- for cases without padding , arm_tensor could use our buffer if its ews()==1.
|
||||
- its desired to call configure and run separately to avoid multiple configure calls ( this is not discussed here, for now)
|
||||
|
||||
|
||||
## arm_compute wrapper proposal
|
||||
|
||||
|
||||
So from above we can conclude that we have two options:
|
||||
|
||||
- creating our NdArray with auto_padding strides and modifying the current wrapper. Still configure will be called foreach run. But with auto padding it is using more memory for small ndarrays
|
||||
- to be able to use accurate padding properly we should call configure before NdArray memory allocation so that we can import it. For that I should investigate graph, DeclarableOps and NdArrays usage lifecycle.
|
||||
|
||||
Here is auto padding:
|
||||
|
||||
// Some kernels compute 32 elements at the time, worst case scenario they
|
||||
// will read 32 values after the last element
|
||||
extra_pad_x = _tensor_shape.num_dimensions() < 1 ? 0 : 32;
|
||||
pad_x = _tensor_shape.num_dimensions() < 1 ? 0 : 4;
|
||||
pad_y = _tensor_shape.num_dimensions() < 2 ? 0 : 4;
|
||||
|
||||
PaddingSize(pad_y, pad_x + extra_pad_x, pad_y, pad_x);
|
||||
|
||||
## Discussion
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
# Import IR
|
||||
|
||||
## Status
|
||||
Implemented
|
||||
|
||||
Proposed by: Adam Gibson (28-09-2020)
|
||||
|
||||
Discussed with: N/A
|
||||
|
||||
## Context
|
||||
|
||||
Generally, every neural network file format defines a sequence of operations
|
||||
to execute mathematical operations that comprises a neural network.
|
||||
|
||||
Each element in the sequence is a node that contains information such as the
|
||||
desired operation, and a set of attributes that represent parameters
|
||||
in to the mathematical function to execute.
|
||||
|
||||
In order to write import/export for different frameworks, we need to adapt
|
||||
an attribute based format from various popular deep learning frameworks.
|
||||
Nd4j has a different list based format for operation execution arguments.
|
||||
In the [previous ADR](./Import_IR.md), we added an IR which makes it easier to
|
||||
interop with other frameworks.
|
||||
|
||||
In this ADR, this work is extended to add a file format for
|
||||
describing lists of operations as MappingRules which allow transformations
|
||||
from one framework to another.
|
||||
|
||||
These transformations manipulate protobuf as input and output Nd4j's
|
||||
new OpDescriptor format as output.
|
||||
|
||||
|
||||
##Related work
|
||||
|
||||
See [the import IR](./0003-Import_IR.md)
|
||||
|
||||
## Decision
|
||||
|
||||
We implement a mapping process framework that defines transforms on an input file format.
|
||||
A MappingProcess defines a list of MappingRules which represent a sequence of transformations
|
||||
on each attribute of an op definition.
|
||||
|
||||
To assist in mapping, a mapping context with needed information like rule arguments
|
||||
for transformation, current node, and whole graph are used as input.
|
||||
|
||||
The input is a protobuf file for a specific framework and the output is an op descriptor
|
||||
described [here](./0003-Import_IR.md).
|
||||
|
||||
A MappingRule converts 1 or more attributes in to 1 more or arg definitions. A potential definition
|
||||
can be found in Appendix E.
|
||||
|
||||
Attributes are named values supporting a wide variety of types from floats/doubles
|
||||
to lists of the same primitive types. See Appendix C for a theoretical definition.
|
||||
|
||||
Arg Definitions are the arguments for an OpDescriptor described in [the import IR ADR.](./0003-Import_IR.md)
|
||||
See Appendix D for a potential definition of arg definitions.
|
||||
|
||||
All of this together describes how to implement a framework agnostic
|
||||
interface to convert between a target deep learning framework and the nd4j format.
|
||||
|
||||
|
||||
## Implementation details
|
||||
|
||||
In order to implement proper mapping functionality, a common interface is implemented.
|
||||
Below are the needed common types for mapping:
|
||||
|
||||
1. IRNodeDef: A node definition in a graph
|
||||
2. IRTensor: A tensor type for mapping
|
||||
3. IROpList: A list of operations
|
||||
4. IRAttrDef: An attribute definition
|
||||
5. IRAttrValue: An attribute value
|
||||
6. IROpDef: An op definition for the IR
|
||||
7. IRDataType: A data type
|
||||
8. IRGraph: A graph abstraction
|
||||
|
||||
Each one of these types is a wrapper around a specific framework's input types
|
||||
of the equivalent concepts.
|
||||
|
||||
Each of these wrappers knows how to convert the specific concepts
|
||||
in to the nd4j equivalents for interpretation by a mapper which applies
|
||||
the mapping rules for a particular framework.
|
||||
|
||||
Doing this will allow us to share logic between mappers and making 1 implementation of
|
||||
mapping possible by calling associated getter methods for concepts like data types and nodes.
|
||||
|
||||
## Serialization
|
||||
|
||||
In order to persist rules using protobuf, all rules will know how to serialize themselves.
|
||||
A simple serialize() and load() methods are implemented which covers conversion using
|
||||
interface methods up to the user to implement which describes how to persist the protobuf
|
||||
representation. This applies to any of the relevant functionality such as rules and processes.
|
||||
|
||||
|
||||
|
||||
## Custom types
|
||||
|
||||
Some types will not map 1 to 1 or are directly applicable to nd4j.
|
||||
In order to combat this, when an unknown type is discovered during mapping,
|
||||
adapter functions for specific types must be specified.
|
||||
|
||||
Supported types include:
|
||||
|
||||
1. Long/Int
|
||||
2. Double/Float
|
||||
3. String
|
||||
4. Boolean
|
||||
5. Bytes
|
||||
6. NDArrays
|
||||
|
||||
|
||||
An example:
|
||||
|
||||
A Dim in tensorflow can be mapped to a long in nd4j.
|
||||
|
||||
Shape Information can be a list of longs or multiple lists depending on the
|
||||
context.
|
||||
|
||||
## Consequences
|
||||
### Advantages
|
||||
* Allows a language neutral way of describing a set of transforms necessary
|
||||
for mapping an set of operations found in a graph from one framework to the nd4j format.
|
||||
|
||||
* Allows a straightforward way of writing an interpreter as well as mappers
|
||||
for different frameworks in nd4j in a standardized way.
|
||||
|
||||
* Replaces the old import and makes maintenance of imports/mappers more straightforward.
|
||||
|
||||
### Disadvantages
|
||||
|
||||
* More complexity in the code base instead of a more straightforward java implementation.
|
||||
|
||||
* Risks introducing new errors due to a rewrite
|
||||
|
||||
|
||||
## Appendix A: Contrasting MappingRules with another implementation
|
||||
|
||||
We map names and types to equivalent concepts in each framework.
|
||||
Onnx tensorflow does this with an [attribute converter](https://github.com/onnx/onnx-tensorflow/blob/08e41de7b127a53d072a54730e4784fe50f8c7c3/onnx_tf/common/attr_converter.py)
|
||||
|
||||
This is done by a handler (one for each op).
|
||||
More can be found [here](https://github.com/onnx/onnx-tensorflow/tree/master/onnx_tf/handlers/backend)
|
||||
|
||||
|
||||
## Appendix B: Challenges when mapping nd4j ops
|
||||
|
||||
The above formats are vastly different. Onnx and tensorflow
|
||||
are purely attribute based. Nd4j is index based.
|
||||
This challenge is addressed by the IR by adding names to each property.
|
||||
|
||||
|
||||
In order to actually map these properties, we need to define rules for doing so.
|
||||
Examples of why these mapping rules are needed below:
|
||||
|
||||
1. Different conventions for the same concept. One example that stands out from conv
|
||||
is padding. Padding can be represented as a string or have a boolean that says what a string equals.
|
||||
In nd4j, we represent this as a boolean: isSameMode. We need to do a conversion inline in order
|
||||
to invoke nd4j correctly.
|
||||
|
||||
2. Another issue is implicit concepts. Commonly, convolution requires you to configure a layout
|
||||
of NWHC (Batch size, Height, Width, Channels)
|
||||
or NCHW (Batch size, Channels,Height, Width). Tensorflow allows you to specify it,
|
||||
nd4j also allows you to specify it. Onnx does not.
|
||||
|
||||
A more in depth conversation on this specific issue relating to the
|
||||
2 frameworks can be found [here](https://github.com/onnx/onnx-tensorflow/issues/31)
|
||||
In order to address these challenges, we introduce a MappingRule allowing
|
||||
us to define a series of steps to map the input format to the nd4j format
|
||||
in a language neutral way via a protobuf declaration.
|
||||
|
||||
|
||||
## Appendix C: A theoretical attribute definition
|
||||
```kotlin
|
||||
enum class AttributeValueType {
|
||||
FLOAT,
|
||||
LIST_FLOAT,
|
||||
BYTE,
|
||||
LIST_BYTE,
|
||||
INT,
|
||||
LIST_INT,
|
||||
BOOL,
|
||||
LIST_BOOL,
|
||||
STRING,
|
||||
LIST_STRING
|
||||
}
|
||||
|
||||
interface IRAttribute<ATTRIBUTE_TYPE,ATTRIBUTE_VALUE_TYPE> {
|
||||
|
||||
fun name(): String
|
||||
|
||||
fun floatValue(): Double
|
||||
|
||||
fun listFloatValue(): List<Float>
|
||||
|
||||
fun byteValue(): Byte
|
||||
|
||||
fun listByteValue(): List<Byte>
|
||||
|
||||
fun intValue(): Long
|
||||
|
||||
fun listIntValue(): List<Long>
|
||||
|
||||
fun boolValue(): Boolean
|
||||
|
||||
fun listBoolValue(): List<Boolean>
|
||||
|
||||
fun attributeValueType(): AttributeValueType
|
||||
|
||||
fun internalAttributeDef(): ATTRIBUTE_TYPE
|
||||
|
||||
fun internalAttributeValue(): ATTRIBUTE_VALUE_TYPE
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Appendix D: A theoretical kotlin definition of argument descriptors and op descriptors can be found below:
|
||||
```kotlin
|
||||
interface IRArgDef<T,DATA_TYPE> {
|
||||
fun name(): String
|
||||
|
||||
fun description(): String
|
||||
|
||||
fun dataType(): IRDataType<DATA_TYPE>
|
||||
|
||||
fun internalValue(): T
|
||||
|
||||
fun indexOf(): Integer
|
||||
}
|
||||
|
||||
interface IROpDef<T,ARG_DEF_TYPE,DATA_TYPE,ATTRIBUTE_TYPE,ATTRIBUTE_VALUE_TYPE> {
|
||||
fun opName(): String
|
||||
|
||||
fun internalValue(): T
|
||||
|
||||
fun inputArgs(): List<IRArgDef<ARG_DEF_TYPE,DATA_TYPE>>
|
||||
|
||||
fun outputArgs(): List<IRArgDef<ARG_DEF_TYPE,DATA_TYPE>>
|
||||
|
||||
fun attributes(): List<IRAttribute<ATTRIBUTE_TYPE,ATTRIBUTE_VALUE_TYPE>>
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##Appendix E: A theoretical kotlin definition of Mapping Rules, MappingProcess and ArgDef can be found below:
|
||||
```kotlin
|
||||
interface MappingProcess<T,TENSOR_TYPE,ATTRIBUTE_TYPE,ATTRIBUTE_VALUE_TYPE,DATA_TYPE> {
|
||||
fun opName(): String
|
||||
|
||||
fun frameworkVersion(): String
|
||||
|
||||
fun inputFramework(): String
|
||||
|
||||
fun rules(): List<MappingRule<ATTRIBUTE_TYPE,ATTRIBUTE_VALUE_TYPE>>
|
||||
|
||||
|
||||
fun applyProcess(inputNode: IRNode<T,TENSOR_TYPE,ATTRIBUTE_TYPE,ATTRIBUTE_VALUE_TYPE,DATA_TYPE>): OpDeclarationDescriptor
|
||||
|
||||
fun applyProcessReverse(input: OpDeclarationDescriptor): IRNode<T,TENSOR_TYPE,ATTRIBUTE_TYPE,ATTRIBUTE_VALUE_TYPE,DATA_TYPE>
|
||||
|
||||
fun createDescriptor(argDescriptors: List<OpNamespace.ArgDescriptor>): OpDeclarationDescriptor
|
||||
}
|
||||
|
||||
interface MappingRule<ATTRIBUTE_TYPE,ATTRIBUTE_VALUE_TYPE> {
|
||||
fun name(): String
|
||||
|
||||
/**
|
||||
* Convert 1 or more attributes in to a list of {@link ArgDescriptor}
|
||||
*/
|
||||
fun convert(inputs: List<IRAttribute<ATTRIBUTE_TYPE,ATTRIBUTE_VALUE_TYPE>> ): List<OpNamespace.ArgDescriptor>
|
||||
|
||||
fun convertReverse(input: List<OpNamespace.ArgDescriptor>): List<IRAttribute<ATTRIBUTE_TYPE,ATTRIBUTE_VALUE_TYPE>>
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
# Interpreter
|
||||
|
||||
## Status
|
||||
Rejected
|
||||
|
||||
Proposed by: Adam Gibson (28-09-2020)
|
||||
|
||||
Discussed with: N/A
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
## Decision
|
||||
|
||||
An interpreter uses the [import IR](./0003-Import_IR.md) and the [mapping rule IR](./0004-Mapping_IR.md)
|
||||
to execute and map operations from one framework to nd4j's file format and back.
|
||||
|
||||
This also allows execution of different frameworks via conversion in the nd4j engine.
|
||||
|
||||
|
||||
A combination of the 2 allows a uniform interface to be used for the interpreter.
|
||||
|
||||
1 or more MappingRules will be used to transform 1 file format to another.
|
||||
|
||||
|
||||
## Mapping Rules Execution
|
||||
|
||||
Mapping Rules are named functions that contain the function signature
|
||||
(input and outputs). These mapping rules are used by the interpreter
|
||||
to know which functions to execute.
|
||||
|
||||
The interpreter has built in implementations of the defined functions
|
||||
for the desired transforms.
|
||||
|
||||
|
||||
## Import process
|
||||
|
||||
An import process is defined for an overall framework.
|
||||
It maps input graphs to samediff graphs using
|
||||
specified mapping processes for op names and frameworks.
|
||||
An import process is all that is needed to create a graph.
|
||||
Below are the needed concepts for an import process to implement.
|
||||
|
||||
|
||||
## Graph creation
|
||||
|
||||
In order for execution to happen, a graph needs to be built.
|
||||
This happens in java via the samediff builder.
|
||||
|
||||
The conversion happens as follows:
|
||||
input node -> convert node to op descriptor via defined mapping rules -> add op descriptor to graph
|
||||
|
||||
The op descriptor is converted to a CustomOp which is then added to the graph via
|
||||
[addArgsFor](https://github.com/KonduitAI/deeplearning4j/blob/88d3c4867fb87ec760b445c6b9459ecf353cec47/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/autodiff/samediff/SameDiff.java#L1078)
|
||||
|
||||
This handles declarative graph creation setting dependencies up. Delegation of the graph structure
|
||||
creation to the existing Samediff library enables the scope of this interpreter to be focused on
|
||||
mapping operations.
|
||||
|
||||
## Custom Sub graphs
|
||||
|
||||
One common use case is mapping sub graphs to custom layers. A custom layer can be thought of as a sequence of operations.
|
||||
In order to map this, a named process can be created. Generally, if you know what ops the sub graph is made of,
|
||||
you only need to declare a set of rules based on the rules that map individual ops in the existing framework.
|
||||
|
||||
## Consequences
|
||||
### Advantages
|
||||
* Uses a common interface across different frameworks making maintenance simple
|
||||
|
||||
* Allows an easy to maintain abstraction for interop with different file formats
|
||||
|
||||
* Allows an easy entry point in to the framework without knowing much about the framework.
|
||||
|
||||
### Disadvantages
|
||||
|
||||
* Need to ensure compatibility across different frameworks
|
||||
|
||||
* Requires extensive testing to ensure proper compatibility
|
||||
|
||||
* May not necessarily support all ops people are expecting. This will be addressed
|
||||
in a new ADR.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Junit 5 tag usage
|
||||
|
||||
## Status
|
||||
Proposed
|
||||
|
||||
Proposed by: Adam Gibson (21-03-2021)
|
||||
|
||||
Discussed with: N/A
|
||||
|
||||
## Context
|
||||
DL4J was a junit 4 based code based for testing.
|
||||
It's now based on junit 5's jupiter API, which has support for [Tags](https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/Tag.html).
|
||||
|
||||
DL4j's code base has a number of different kinds of tests that fall in to several categories:
|
||||
1. Long and flaky involving distributed systems (spark, parameter-server)
|
||||
2. Code that requires large downloads, but runs quickly
|
||||
3. Quick tests that test basic functionality
|
||||
4. Comprehensive integration tests that test several parts of a code base
|
||||
|
||||
Due to the variety of behaviors across different tests, it's hard to tell what's actually needed
|
||||
for running and validating whether changes work against such a complex test base.
|
||||
|
||||
Much of the time, most of the tests aren't related to a given change.
|
||||
Often times, quick sanity checks are all that's needed in order to make sure a change works.
|
||||
|
||||
A common set of tags is used to filter which tests are needed to run when.
|
||||
This allows us to retain complex integration tests and run them on a set schedule
|
||||
to catch regressions while allowing a defined subset of tests to run for a quick feedback loop.
|
||||
|
||||
|
||||
|
||||
|
||||
## Decision
|
||||
|
||||
A few kinds of tags exist:
|
||||
1. Time based: long-time,short-time
|
||||
2. Network based: has-download
|
||||
3. Distributed systems: spark, multi-threaded
|
||||
4. Functional cross-cutting concerns: multi module tests, similar functionality (excludes time based)
|
||||
5. Platform specific tests that can vary on different hardware: cpu, gpu
|
||||
6. JVM crash: (jvm-crash) Tests with native code can crash the JVM for tests. It's useful to be able to turn those off when debugging.: jvm-crash
|
||||
7. RNG: (rng) for RNG related tests
|
||||
8. Samediff:(samediff) samediff related tests
|
||||
9. Training related functionality
|
||||
|
||||
|
||||
## Consequences
|
||||
### Advantages
|
||||
* Ability to sort through and filter tests based on different running environments
|
||||
|
||||
* Ability to reason about test suites as a whole dynamically across modules
|
||||
|
||||
* Avoid the need to define test suites
|
||||
|
||||
* Ability to define groups of tags based in profiles
|
||||
|
||||
* Ability to dynamically filter tests from the maven command line
|
||||
|
||||
|
||||
### Disadvantages
|
||||
|
||||
* Documentation and maintenance burden needing to know what tags do what
|
||||
|
||||
* Test maintenance for newcomers who may not know how to tag tests
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Nd4j Classifiers
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
Proposed by: Adam Gibson (5-5-2021)
|
||||
|
||||
Discussed with: saudet (Samuel Audet)
|
||||
|
||||
## Context
|
||||
Nd4j relies upon the c++ library [libnd4j](../libnd4j) for native math execution.
|
||||
It uses [javacpp](https://github.com/bytedeco/javacpp) to link against
|
||||
libnd4j. Libnd4j is capable of being compiled a myriad of different ways allowing different trade offs to be made
|
||||
in terms of performance and dependencies. This presents complexity in exposing this flexibility to the end user.
|
||||
|
||||
|
||||
|
||||
## Decision
|
||||
|
||||
In order to allow users to pick which configuration they would like to use, while avoiding adding a lot of different artifact
|
||||
ids to the project, the following javacpp platform extensions are used:
|
||||
compiled type (avx etc or blank if normal) - software linked against (cudnn, onednn, armcompute) - version
|
||||
|
||||
|
||||
An example for the one dnn platform extension could be:
|
||||
dnnl-2.2
|
||||
avx256-dnnl-2.2
|
||||
|
||||
This presents 2 examples where a special compilation is enabled and one where it's not
|
||||
both linking against dnnl/onednn 2.2.
|
||||
|
||||
|
||||
## Discussion
|
||||
|
||||
Saudet: Javacpp's extensions can actually support optional
|
||||
inclusion. It plays well with other platform declarations.
|
||||
This means you can use platform like:
|
||||
```bash
|
||||
mvn -Djavacpp.platform.extension=-avx512 -Djavacpp.platform=windows-x86_64 clean ...
|
||||
```
|
||||
|
||||
to enable certain extensions.
|
||||
|
||||
## Consequences
|
||||
### Advantages
|
||||
* Adds more extensions than the previous release. This allows nd4j-native/cuda to play nice
|
||||
with the standard javacpp.platform scheme when [reducing the number of dependencies](https://github.com/bytedeco/javacpp-presets/wiki/Reducing-the-Number-of-Dependencies)
|
||||
while also enabling the new accelerated extensions, but in an optional manner.
|
||||
|
||||
* Allows users to pick how they want libnd4j to be included in their build
|
||||
|
||||
* Maintains 2 artifact ids people have to know without too much extensive knowledge.
|
||||
|
||||
* Allows us to keep a sane default for people with optimizations being optional
|
||||
|
||||
|
||||
### Disadvantages
|
||||
|
||||
* Could be deprecated in the future depending on how libnd4j evolves
|
||||
|
||||
* Complexity for the user with the number of new extensions to be used.
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Nd4j eager shape computation
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
Proposed by: Adam Gibson (11-19-2021)
|
||||
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
## Context
|
||||
Nd4j's model import framework often has the need to
|
||||
compute shapes as variables are created.
|
||||
|
||||
This is in order to resolve how to properly
|
||||
create a graph based on a graph descriptor from another framework
|
||||
such as tensorflow or pytorch.
|
||||
|
||||
This is often called eager mode. This proposal focuses on just eager shape computation
|
||||
intended for use in model import. The assumption is that we could
|
||||
build on this later for fully eager computation.
|
||||
|
||||
|
||||
## Decision
|
||||
|
||||
In order to aid building model import easier,
|
||||
this proposal is focused on implementing just dynamic shape computation
|
||||
for use in the model import context.
|
||||
|
||||
This will be composed of a few parts:
|
||||
|
||||
1. Each outputVariables() call in SDVariable triggers
|
||||
an Nd4j.getExecutioner().exec(..) call on the relevant operation
|
||||
to extract out op shapes. It then sets the appropriate shapes
|
||||
based on the result for each SDVariable field.
|
||||
|
||||
|
||||
2. This will intentionally include dummy calls for control flow ops
|
||||
such as if, enter, and while. Shapes from these don't matter
|
||||
beyond knowing the number of outputs.
|
||||
|
||||
|
||||
3. Each SameDiff instance will have an eager mode boolean
|
||||
that will determine whether this functionality is invoked.
|
||||
This eager mode variable will be required for some model import use cases.
|
||||
Usually the model import framework will turn eager on as needed
|
||||
without the user needing to be involved.
|
||||
|
||||
|
||||
4. Each SameDiff instance will have a separate ArrayHolder
|
||||
that will be used for looking up ndarrays relevant
|
||||
to the eager computation. This will not use proper sessions
|
||||
but instead store that will be used once for computing shapes.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Discussion
|
||||
Paul: Originally we would need full eager mode
|
||||
|
||||
Adam: We don't need a fully implemented eager mode
|
||||
just for model import. Full eager mode would mean proper session support,
|
||||
training support. This would just be incremental shape calculations
|
||||
for model import.
|
||||
|
||||
## Consequences
|
||||
### Advantages
|
||||
|
||||
* Allows more model import flexibility
|
||||
* Adds a base for real eager mode later on
|
||||
|
||||
### Disadvantages
|
||||
|
||||
* Adds more complexity to model import with the addition
|
||||
dynamic shape calculations during model import
|
||||
|
||||
* Could be hard to debug if you want to see the full would be imported graph
|
||||
when a computation is blocking it
|
||||
|
||||
* The import workflow has more state attached to it
|
||||
with the eager array holder attached to each samediff instance
|
||||
and the need for another flag for turning the feature on/off
|
||||
@@ -0,0 +1,72 @@
|
||||
# Import node pre processing
|
||||
|
||||
## Status
|
||||
Discsusion
|
||||
|
||||
Proposed by: Adam Gibson (11-25-2021)
|
||||
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
## Context
|
||||
Nd4j's model import framework supports different protobuf based frameworks
|
||||
for importing and executing models. This was introduced in [0003-Import_IR.md](0003-Import_IR.md)
|
||||
One problem with importing models is compatibility between different versions of frameworks.
|
||||
Often,migrations are needed to handle compatibility between versions. A node pre processor is proposed
|
||||
that: when combined with the model import framework allows for
|
||||
annotation based automatic upgrades of graphs.
|
||||
|
||||
## Decision
|
||||
|
||||
In order to handle preprocessing a node to handle things like upgrades.
|
||||
An end user can specify a pre processor via a combination of 2 interfaces:
|
||||
1. An annotation for specifying a class that implements a relevant rule
|
||||
for processing. This will automatically be discoverable via annotation scanning
|
||||
similar to other frameworks. This annotation looks as follows:
|
||||
```kotlin
|
||||
annotation class NodePreProcessor(val nodeTypes: Array<String>, val frameworkName: String)
|
||||
|
||||
```
|
||||
The information include the nodeTypes which are the operation types to scan for when doing upgrades on a graph.
|
||||
The framework name: relevant if multiple import modules are on the classpath. Filters rules
|
||||
by their intended framework for import.
|
||||
|
||||
2. The necessary pre processing hook that will handle processing the node
|
||||
and may modify the graph. Graph modification maybe necessary if we need to add new nodes to compensate
|
||||
for modification of a node such as an attribute moving to being an input.
|
||||
|
||||
```kotlin
|
||||
|
||||
interface NodePreProcessorHook<NODE_TYPE : GeneratedMessageV3,
|
||||
TENSOR_TYPE : GeneratedMessageV3,
|
||||
ATTRIBUTE_TYPE : GeneratedMessageV3,
|
||||
ATTRIBUTE_VALUE_TYPE : GeneratedMessageV3, DATA_TYPE>
|
||||
where DATA_TYPE: ProtocolMessageEnum {
|
||||
|
||||
|
||||
fun modifyNode(
|
||||
node: IRNode<NODE_TYPE, TENSOR_TYPE, ATTRIBUTE_TYPE, ATTRIBUTE_VALUE_TYPE, DATA_TYPE>,
|
||||
graph: IRGraph<GeneratedMessageV3, GeneratedMessageV3, GeneratedMessageV3, GeneratedMessageV3, GeneratedMessageV3, GeneratedMessageV3, ProtocolMessageEnum>
|
||||
): IRNode<NODE_TYPE, TENSOR_TYPE, ATTRIBUTE_TYPE, ATTRIBUTE_VALUE_TYPE, DATA_TYPE>
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Discussion
|
||||
|
||||
## Consequences
|
||||
### Advantages
|
||||
|
||||
* An automatic way of extending support for model import by providing users a
|
||||
hook mechanism for handling graph modification
|
||||
|
||||
* Extends the model import process to handle nodes in an op specific way
|
||||
allowing a way of handling op specific interactions simplifying maintenance
|
||||
of other aspects of the import framework
|
||||
|
||||
### Disadvantages
|
||||
|
||||
* Adds a 3rd kind of hook for model import, thus more for users to learn
|
||||
* Can be difficult to implement if user doesn't know how to work with graphs
|
||||
* May have unforeseen consequences of testing due to graph modification
|
||||
after creation
|
||||
@@ -0,0 +1,98 @@
|
||||
# Testing
|
||||
|
||||
## Status
|
||||
**Proposed**
|
||||
|
||||
Proposed by: Adam Gibson (13th December 2021)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
Testing historically on a large code base like deeplearning4j often involves
|
||||
platform specific code with several categories as documented in
|
||||
[the Test Architectures ADR](./0006 - Test architecture.md)
|
||||
|
||||
|
||||
There are multiple levels of testing which test ever larger chunks of the application.
|
||||
|
||||
**Unit tests** typically test code at the smallest possible unit. In Java this usually encompasses just a single class.
|
||||
|
||||
**Component tests** are meant to test a component consisting of multiple units working together. A logical component
|
||||
usually consists of a few classes at most and don't cross the boundary between two components.
|
||||
|
||||
**Integration tests** are meant to test how components integrate with each other. Their most important job is to ensure
|
||||
that those components properly interface with each other.
|
||||
|
||||
**End-to-End tests**, often also called system tests, are meant to test the entire system. They interface with the
|
||||
application through the same UI as a regular user does.
|
||||
|
||||
**Regression tests** are meant to mimic a specific behavior or usage that results in a bug. They are created before
|
||||
the bug fix and need to reproduce the bug but expect the correct behavior, i.e. they should fail at first. Once the bug
|
||||
is fixed, they should pass without any change in the test definition. These test cases accumulate as bug reports come in
|
||||
and guard us from recreating that particular bug in that particular situation.
|
||||
|
||||
|
||||
The Eclipse Deeplearning4j project has mostly what we would call End-To-End tests.
|
||||
We want to run a set of tests on different classifiers (eg: cuda version + cudnn, cuda version + non cudnn, cpu, arm32,arm64,..)
|
||||
in order to verify platform specific behavior works as intended.
|
||||
|
||||
When testing, we generally have a few things we test the behavior of:
|
||||
1. Compatibility across backends
|
||||
2. Performance
|
||||
3. Regressions in behavior (gradient checks failing, ops providing wrong results)
|
||||
4. Different runtime tests: standalone, spark
|
||||
|
||||
|
||||
Verifying behavior across these different backends even at release time
|
||||
is time-consuming and error-prone taking hours to run with some tests
|
||||
being inconsistent (oftentimes spark and multi threading clashing with OMP math threads causing crahses/slowdowns)
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
We put anything that is considered an end-to-end test requiring platform
|
||||
specific behavior in to its own module.
|
||||
These tests would already be tagged. We would have an accompanying pom.xml
|
||||
that accommodated downloading snapshots to allow us to run specified tests
|
||||
on different classifiers.
|
||||
|
||||
The goal would be to allow specifying the following parameters from the command line:
|
||||
1. Classifiers to run
|
||||
2. Groups of tests to run
|
||||
3. Version to run (defaults to the latest snapshots)
|
||||
|
||||
|
||||
Future work may extend this behavior to add performance tests as well.
|
||||
|
||||
The intended workflow would be to allow the following steps:
|
||||
1. Clone the code base
|
||||
2. Cd in to the test module
|
||||
3. Specify the combination of tests you want to run on which platform
|
||||
|
||||
This allows easy configuration on CI and creation of different scripts for validation
|
||||
along the lines of behavior we want to run. Examples include:
|
||||
1. run model import tests (keras, tensorflow, onnx)
|
||||
2. Run spark tests
|
||||
3. Run basic dl4j tests
|
||||
|
||||
|
||||
These distinctions would be achieved through a mix of test tags and test
|
||||
name filters.
|
||||
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
* Tests become more accessible
|
||||
* It becomes much easier to set up test suites to be run on different classifiers on CI
|
||||
as a recurring job
|
||||
* Release testing/validation on specific platforms like embedded pis, nanos don't require you to build the
|
||||
binaries, but instead you can just download them and run binaries cross compiled on CI to verify behavior
|
||||
* Allows specifying older versions of library as necessary
|
||||
|
||||
### Disadvantages
|
||||
* Lose old behavior with tests breaking old assumptions causing contributors
|
||||
to learn a specific way of running tests
|
||||
* Requires discipline when tagging tests
|
||||
* A fairly complex pom.xml will be required for flexibly running tests
|
||||
@@ -0,0 +1,129 @@
|
||||
# Model Hub Zoo - Download
|
||||
|
||||
## Status
|
||||
**Discussion**
|
||||
|
||||
Proposed by: Adam Gibson (1st Jan 2022)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
Model zoos or hubs are web services from different vendors to provide
|
||||
a venue for researchers and engineering teams who want to open source their
|
||||
work to publish models. The use case is typically to finetune models.
|
||||
|
||||
Finetuning models means adapting a model trained for one task to generalize for another
|
||||
by replacing its objective.
|
||||
|
||||
Coupling this with binary file formats, distributing model files typically happens
|
||||
as large binary files + some optional metadata. In the case of tensorflow and onnx
|
||||
it's using protobuf. With pytorch it uses python pickle archives.
|
||||
|
||||
Model hubs provide SDKs for downloading and using models within python.
|
||||
|
||||
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
The goal is to interop with these model hubs using an integrated python library
|
||||
and add the appropriate tooling for converting these models to something consumable
|
||||
by the model import framework.
|
||||
|
||||
|
||||
A user is able to download models with a standard python interface using
|
||||
ModelHub. A ModelHub implementation might look like:
|
||||
```python
|
||||
class ModelHub(object):
|
||||
def __init__(self):
|
||||
self.hub_url = ''
|
||||
self.framework_name = ''
|
||||
|
||||
def download_model(self,path: string):
|
||||
...
|
||||
|
||||
def stage_model(self,model):
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
The storage type will be specific to the model hub. The concrete functions
|
||||
enums like this have is to specify to the underlying web service what kind of model we want.
|
||||
|
||||
|
||||
|
||||
In order to load a model, a model hub tends to provide different ways of
|
||||
downloading a model. This can be via a compressed archive or uncompressed.
|
||||
|
||||
In order to use this we need to be able to specify the access type.
|
||||
This will be an enum such as:
|
||||
|
||||
```python
|
||||
enum StorageType {
|
||||
COMPRESSED,UNCOMPRESSED
|
||||
}
|
||||
```
|
||||
Loading a model will be done using either samediff or deeplearning4j.
|
||||
This will leverage and extend the existing work in the model import
|
||||
work built previously.
|
||||
|
||||
|
||||
### Storage directory
|
||||
|
||||
Every model downloaded by this interface will be stored in an uncompressed
|
||||
format (.onnx,.pb,..) files with their original names under a standard unified directory
|
||||
separated by framework. This ensures ease of use and debugging
|
||||
in case a user wants to directly import a model or view
|
||||
it in a model viewer like netron.
|
||||
|
||||
This directory will default to a .modelhub directory under $USER.
|
||||
A user can also override this directory with a MODELHUB_PATH
|
||||
environment variable.
|
||||
|
||||
Note that the models will be stored as duplicates copied under
|
||||
the $MODELHUB_PATH. The reason for this is to preserve
|
||||
the original framework's underlying model in case a user
|
||||
needs to work around bugs or needs to work with the underlying
|
||||
libraries in a separate environment.
|
||||
|
||||
This also has the added benefit of allowing a previous model cache from the underlying
|
||||
libraries to avoid download. In this case, models will just be
|
||||
stored under the $MODELHUB_PATH in the form they need to be in
|
||||
to work with the model import framework.
|
||||
|
||||
|
||||
### Staging models
|
||||
|
||||
Every model will be downloaded by its original SDK and then preprocessed.
|
||||
We will call this staging. Staging is a secondary step that takes
|
||||
each model and adjusts it to be its end form that can be worked with.
|
||||
|
||||
For example in tensorflow, we may need to freeze models first
|
||||
before storing them for use with import.
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
* Allow interop of different ecosystems
|
||||
* Provide a foundation for finetuning models (finetuning models is out of the scope of this ADR)
|
||||
* Provide a way to download and manage models for testing model import functionality
|
||||
* Provide a way to enhance the built in dl4j model zoo by importing models from other ecosystems
|
||||
in a standardized way
|
||||
* Unified way of downloading and accessing models for multiple frameworks
|
||||
* Standardized directory allowing models to be easily worked with.
|
||||
* Keeps underlying framework models around for debugging. Also prevents underlying libraries
|
||||
from re-downloading libraries and benefiting from already downloaded models
|
||||
if a user uses the underlying model libraries elsewhere.
|
||||
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* More complexity in maintaining an ongoing SDK for downloading and maintaining models in different ecosystems
|
||||
* Potential storage complexity involved in running and maintaining/testing models
|
||||
* No way to know when a model hub changes
|
||||
* Loading and importing models from different ecosystems can be messy. Additional work may need to be done per model
|
||||
in order to make them usable. That additional work is out of the scope of this ADR.
|
||||
* More storage on a user's system due to the secondary cache
|
||||
@@ -0,0 +1,122 @@
|
||||
# Model Hub Zoo Download Implementation
|
||||
|
||||
## Status
|
||||
**Discussion**
|
||||
|
||||
Proposed by: Adam Gibson (1st Jan 2022)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
Following on from work in [downloading models](0011%20-%20Model%20Hub-Zoo%20Download.md)
|
||||
We need to be able to interop in different ecosystems. This ADR will address the
|
||||
specs for implementing interop with the following ecosystems:
|
||||
1. [Onnx](https://github.com/onnx/models/)
|
||||
2. [Tensorflow](https://www.tensorflow.org/hub)
|
||||
3. [Huggingface](https://huggingface.co/spaces)
|
||||
4. [Pytorch model zoo](https://pytorch.org/serve/model_zoo.html)
|
||||
5. [Keras applications](https://keras.io/api/applications/)
|
||||
|
||||
## Proposal
|
||||
|
||||
This proposal will be broken up in to separate sections detailing
|
||||
the work and implementation needed to implement the loading of models
|
||||
from each of these ecosystems.
|
||||
|
||||
Each section will cover how to implement the download and loading of model
|
||||
download workflow described in [downloading models](0011%20-%20Model%20Hub-Zoo%20Download.md)
|
||||
|
||||
We will also cover how we will handle staging of models for each framework.
|
||||
|
||||
|
||||
### Onnx
|
||||
|
||||
Onnx is pretty straightforward as a github repo download.
|
||||
These models do not have any special structure beyond the zip file.
|
||||
Our downloader will focus on the already uncompressed models
|
||||
for ease of simplicity.
|
||||
|
||||
### Tensorflow
|
||||
|
||||
Tensorflow will use the tf hub web service. Our access will be focused
|
||||
on using the uncompressed pb models + handling other conversion code
|
||||
for freezing models to be reused.
|
||||
|
||||
For our purposes a staged model will be a frozen model
|
||||
that can be directly imported.
|
||||
|
||||
|
||||
### Pytorch
|
||||
|
||||
Pytorch will need to be converted to onnx. Pytorch serving uses
|
||||
the [model archive tool](https://github.com/pytorch/serve/tree/master/model-archiver/)
|
||||
for handling model storage.
|
||||
|
||||
Unfortunately, this requires a bit of to integrate with.
|
||||
Pytorch serve archives can vary in format. We will typically
|
||||
want to extract the model from it to manipulate it.
|
||||
|
||||
Separately, pytorch has various model zoos both official and community provided:
|
||||
1. [Example of community provided](https://github.com/rwightman/pytorch-image-models)
|
||||
2. [Torchvision model zoo](https://pytorch.org/vision/stable/models.html)
|
||||
3. [Pytorch hub](https://pytorch.org/hub/)
|
||||
|
||||
At the end, we will want to convert the model to onnx.
|
||||
This will be considered a staged model that is consumable
|
||||
by the framework.
|
||||
|
||||
|
||||
|
||||
|
||||
### Huggingface
|
||||
|
||||
Huggingface spaces uses git repositories to store models.
|
||||
URLs are accessible using the [huggingface hub SDK](https://huggingface.co/docs/hub/how-to-downstream)
|
||||
|
||||
Huggingface hub supports 3 frameworks: pytorch, tensorflow, JAX
|
||||
|
||||
Our initial support will only focus on pytorch and tensorflow.
|
||||
JAX will come at a later date when we have implemented JAX
|
||||
for the model import framework.
|
||||
|
||||
When loading a model, we will need to know which model type we are running
|
||||
so we can convert it to onnx. We will know this by letting a user specify
|
||||
the model type when they go to download it.
|
||||
|
||||
|
||||
For each of tensorflow and pytorch we will be storing models under their respective
|
||||
frameworks reusing the staging techniques from the tensorflow and onnx frameworks.
|
||||
|
||||
Huggingface paths should be repositories + the framework_name specifier.
|
||||
We use AutoModel(pytorch) and AutoTFModel(tensorflow) for converting models
|
||||
to onnx and saved_model -> pb respectively.
|
||||
|
||||
### Keras
|
||||
|
||||
Keras applications are simple archives that contain .h5 files.
|
||||
We will use the keras applications library to download and cache the models.
|
||||
|
||||
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Greatly strengthens our ability to test and execute models
|
||||
needed for different use cases
|
||||
* Allows us to be flexible in what we can enable users to do with
|
||||
our framework as a starting point
|
||||
* Further, builds out the work from downloading models and greatly increases
|
||||
the testing allowed for our model import framework
|
||||
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* Different work is needed for each ecosystem
|
||||
* APIs may change and need to be updated
|
||||
* Just pre-processing models does not mean they are guaranteed to be imported. Additional
|
||||
work will need to be done on model import to allow models to execute. This comes with additional validation work.
|
||||
* Not a comprehensive solution, users will still need to know things like the inputs and outputs
|
||||
and may still need to refer to the underlying docs for a given model to use effectively.
|
||||
* A user may still need to understand how to pre-process different kinds of models
|
||||
@@ -0,0 +1,79 @@
|
||||
# Model Hub Zoo Download Implementation
|
||||
|
||||
## Status
|
||||
**Accepted**
|
||||
|
||||
Proposed by: Adam Gibson (3rd Jan 2022)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
In order to properly consume models from OmniHub a proper interface
|
||||
is needed to allow people to download models. Due to the sheer volume
|
||||
of models out there on the market now a scalable way of interfacing with
|
||||
various pretrained models from java is needed.
|
||||
|
||||
## Proposal
|
||||
|
||||
An interface allowing people to create pretrained model instances
|
||||
using a new interface. Note that this will supplant the old dl4j
|
||||
model zoo. A user will be able to download models via the following interface:
|
||||
|
||||
```java
|
||||
//access the samediff interface to create a resnet18 model
|
||||
Pretrained.samediff().resnet18(..).create();
|
||||
|
||||
|
||||
```
|
||||
|
||||
A user will have 2 interfaces from the Pretrained namespace class:
|
||||
samediff() and dl4j().
|
||||
|
||||
|
||||
Similar to the [codegen work](../contrib/codegen-tools/codegen)
|
||||
this will use poet to generate interfaces that are dynamically generated
|
||||
from a DSL. This DSL uses kotlin and poet.
|
||||
|
||||
We download model configurations and setup code that downloads a model
|
||||
and instantiates it in the user's code. These models will be loaded locally
|
||||
using a .omnihub directory downloaded from a pre specified github repo.
|
||||
|
||||
|
||||
Both the directory and the URL of the model zoo will be configurable
|
||||
using environment variables. These environment variables + default values will be:
|
||||
```bash
|
||||
export OMNIHUB_HOME="$USER/.omnihub"
|
||||
export OMNIHUB_URL="https://media.githubusercontent.com/media/KonduitAI/omnihub-zoo/main"
|
||||
```
|
||||
The default directory will be under $HOME/.omnihub/samediff
|
||||
$HOME/.omnihub/dl4j for downloaded models.
|
||||
|
||||
Note this is the same directory as the python omnihub downloads
|
||||
specified in [zoo download](./0011%20-%20OmniHub-Zoo%20Download.md)
|
||||
Samediff and dl4j will be subdirectories for converted models to be downloaded to.
|
||||
|
||||
Underneath the covers each Pretrained namespace will point to different
|
||||
URL sub folders to resolve models from. dl4j() will point to a /dl4j folder
|
||||
and samediff() will point to a /samediff folder.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Java will be used to consume models and allow for creation of the model zoo
|
||||
repository using git lfs as the base
|
||||
* Increases the number of models have access to without needing to convert them manually
|
||||
|
||||
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* Workflow for converting models is not completely automated and requires
|
||||
manual curation
|
||||
* The tool isn't a complete solution to adding new models as they come out
|
||||
* Models found are not guaranteed to be converted and may need manual interference
|
||||
@@ -0,0 +1,57 @@
|
||||
# Replace old model zoo
|
||||
|
||||
## Status
|
||||
**Accepted**
|
||||
|
||||
Proposed by: Adam Gibson (3rd Jan 2022)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
The deeplearning4j-zoo module has been around for a long time
|
||||
and provides a few models out of the box. It also relies on manually implementing models
|
||||
and does not allow deeplearning4j to benefit from the innovation happening in the space.
|
||||
|
||||
There is also no samediff support in this current model zoo.
|
||||
|
||||
|
||||
## Proposal
|
||||
Replace the model zoo with omnihub. Migrate existing models
|
||||
from the azure hosting to the omnihub github repo at:
|
||||
https://github.com/KonduitAI/omnihub-zoo
|
||||
|
||||
This will allow users to selectively download pretrained models
|
||||
from the deeplearning4j zoo.
|
||||
|
||||
|
||||
Redirect all URL calls in the old zoo to the new one
|
||||
to prevent disruption of user's workflows.
|
||||
|
||||
Extend deeplearning4j-zoo's ZooModel to support the new
|
||||
omnihub zoo.
|
||||
|
||||
Provide a bridge interface to ZooModel with OmniHubZooModel
|
||||
allowing for seamless transition and expansion of new models.
|
||||
|
||||
Migrate off of azure storage for the models saving costs
|
||||
and reducing complexity.
|
||||
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Reduced maintenance cost
|
||||
* Increased model availability for users
|
||||
* Allows for support of samediff
|
||||
* Reuse existing model zoo as is but increase support for new models
|
||||
|
||||
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* Potential bugs
|
||||
* New infra means manually uploading new models
|
||||
* Need to ensure smooth migration of ZooModel interface to seamlessly
|
||||
work with the new model zoo
|
||||
@@ -0,0 +1,94 @@
|
||||
# Replace old model zoo
|
||||
|
||||
## Status
|
||||
**Discussion**
|
||||
|
||||
Proposed by: Adam Gibson (12th Jan 2022)
|
||||
|
||||
TODO:
|
||||
1. centralized MD5 sum directory
|
||||
2. List of directories stored in a local .dl4jresources config file
|
||||
3. Configuration file format for listing directories and their types
|
||||
4. Additional checks for old directories at default values not covered by newer support
|
||||
5. Pre cataloging based on default dataset directories found from prior releases
|
||||
6.
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
A number of current downloaders exist for various resources
|
||||
deeplearning4j needs to function. These include the following:
|
||||
1. Strumpf resource resolver (manages test resources)
|
||||
and relies on azure. The original code for strumpf can be found [here](https://github.com/KonduitAI/strumpf)
|
||||
2. Deeplearning4j model zoo (the legacy model zoo)
|
||||
3. Omnihub: The new model zoo replacing #2.
|
||||
4. Deeplearning4j datasets: dataset download for various datasetiterators
|
||||
|
||||
|
||||
These have accumulated over the years and have made maintenance of download related logic
|
||||
complex.
|
||||
|
||||
Relevant ADRs include:
|
||||
[Omnihub zoo download](./0011%20-%20OmniHub-Zoo%20Download.md)
|
||||
[Omnihub zoo download implementations](./0012%20-%20OmniHub-Zoo%20Download%20Implementations.md)
|
||||
[Omnihub replace old model zoo](./0013%20-%20OmniHub-Zoo%20Consumption.md)
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
All resources are hosted on github LFS.
|
||||
A resource abstraction for binding the various resource types in
|
||||
to 1 abstraction and downloader.
|
||||
|
||||
A Resource is how we handle this. It is be aware of the following concepts:
|
||||
1. A base url for downloading a file
|
||||
2. A cache directory for managing the resource
|
||||
3. Common download + retry logic for ensuring a download succeeds
|
||||
|
||||
|
||||
A Resource manages a remote resource like a file. Similar to the current resource types
|
||||
in deeplearning4j-common. These resources are mostly be stored on git LFS.
|
||||
|
||||
As part of this introduction of a unified resource abstraction
|
||||
is cache aware exposing the cache so users can delete if they wish.
|
||||
|
||||
For existing datasets we use the old sources but have a common abstraction
|
||||
for knowing which dataset we want to download.
|
||||
|
||||
Another problem is file verification.
|
||||
|
||||
The legacy model zoo uses simpler adler checksums for verification.
|
||||
Some download cache verification implementations use md5sum.
|
||||
|
||||
We use md5sum and standardize this for all resources.
|
||||
|
||||
|
||||
Note that in order to avoid maintenance burdens md5 checksum verification
|
||||
is optional. By default, if a resource returns null or an empty
|
||||
string verification is not performed. This distinction is important
|
||||
for resource types such as test resources vs end user assets like pretrained model
|
||||
weights.
|
||||
|
||||
This is also important for compatibility. Due to the legacy checksum
|
||||
verification in the zoo module, md5 checksum verification can come later.
|
||||
|
||||
This leads us to 5 resource types:
|
||||
1. Omnihub: The omnihub pretrained models
|
||||
2. Datasets: the legacy datasets for custom iterators like mnist and lfw
|
||||
3. Dl4j zoo: The legacy zoo models
|
||||
4. Strumpf: the legacy test resource manager
|
||||
5. Custom: custom resources where a user can specify a URL and file destination
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Reduced costs by migrating to github
|
||||
* One module for handling resources
|
||||
* Replaces legacy abstractions while preserving backwards compatibility
|
||||
* Allows easier management of local resources
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* Potential bugs
|
||||
* Migration will take time
|
||||
@@ -0,0 +1,45 @@
|
||||
# Java 9+ Support
|
||||
|
||||
## Status
|
||||
**Discussion**
|
||||
|
||||
Proposed by: Adam Gibson (7th Feb 2022)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
With the next LTS upcoming (java 17) forcing module metadata to be present. This means supporting java 9+ modules
|
||||
with mdoule-info.java being present.
|
||||
|
||||
This ADR addresses the changes made to support java 9 modules. Many of the changes are related to adding
|
||||
[module-info.java](https://www.oracle.com/corporate/features/understanding-java-9-modules.html)
|
||||
|
||||
## Proposal
|
||||
|
||||
1. [moditect plugin](https://github.com/moditect/moditect) metadata for every module
|
||||
a. Each module has a module.name with 1 inherited declaration in the root pom for generating and adding
|
||||
module metadata.
|
||||
b. Github Workflow upgrades for generating module metadata before publishing
|
||||
c. Moditect plugin declarations are optional by default and selectively invoked
|
||||
|
||||
2. Add build steps invoking the plugin ensuring proper metadata gets added
|
||||
|
||||
3. Each maven jar plugin invocation needs a unique module name for jars that have classifiers such as
|
||||
the nd4j backends this avoids duplicate name errors when trying to invoke metadata
|
||||
|
||||
4. Proper package split package declarations enable proper module exposure. This means some classes have been moved
|
||||
around. Mainly internal classes are affected such as the preset names.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Allows easier integration in to various module systems including jigsaw and OSGI
|
||||
* Allows proper integration in to newer java versions enabling users to produce modularized applications
|
||||
* Allows better support for newer java versions
|
||||
* Our plugin approach enables backwards compatibility with java 8
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* Breaking backwards compatibility
|
||||
* May introduce new bugs due to renaming
|
||||
@@ -0,0 +1,92 @@
|
||||
# SDValue
|
||||
|
||||
## Status
|
||||
**Discussion**
|
||||
|
||||
Proposed by: Adam Gibson (8th Mar 2022)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
Onnx and Tensorflow both support miscellaneous data structures to support the
|
||||
building and execution of data flow graphs.
|
||||
|
||||
Programming primitives such as lists, maps and optional types
|
||||
can be valuable in building data flow graphs allowing the developer to express a
|
||||
wide variety of operations encoded within a neural network.
|
||||
|
||||
Tensorflow uses [RaggedTensors](https://www.tensorflow.org/guide/ragged_tensor).
|
||||
Onnx uses [sequences](https://github.com/onnx/onnx/blob/main/docs/IR.md)
|
||||
Of note is onnx also supports optionals and maps.
|
||||
|
||||
|
||||
These primitives can actually be passed in as inputs and returned as outputs
|
||||
from a graph as well.
|
||||
|
||||
In order to simplify the usage of these primitives a value type is used
|
||||
to indicate what type of value it is being passed in (maps, lists, tensors,..)
|
||||
|
||||
|
||||
SDValue is the Samediff interpretation of this and allows [TensorArrays](./nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/linalg/api/ops/impl/shape/tensorops/TensorArray.java) to be passed around.
|
||||
as well as other types to be used within execution of a graph.
|
||||
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
Create an SDValue class and associated enum abstraction for passing around and manipulating
|
||||
variables of different types.
|
||||
|
||||
The following types will be supported:
|
||||
1. LIST
|
||||
2. DICT
|
||||
3. TENSOR
|
||||
|
||||
|
||||
An SDValue is passed around similar to placeholders and are utilized within InferenceSession to
|
||||
execute operations within a graph.
|
||||
|
||||
An SDValue's real underlying type will map to the following:
|
||||
1. LIST: INDArray[]
|
||||
2. DICT: Map<String,INDArray>
|
||||
3. TENSOR: INDArray
|
||||
|
||||
Method calls that will use this off of samediff and InferenceSession will be:
|
||||
```java
|
||||
sd.output(Map<String,SDValue> values,...);
|
||||
|
||||
```
|
||||
|
||||
Each of these are named values with a value type. The scope of this for execution just augmenti
|
||||
arrays of arrays or TensorArrays.
|
||||
|
||||
The below describes how these operations will work:
|
||||
|
||||
|
||||
### Operations
|
||||
The following operations on sequences are supported:
|
||||
1. addItemToSequence(String varName,INDArray item,int atIndex): add an array at a particular index
|
||||
2. removeItemFromSequence(String varName,int indexOfItem): remove an item at a particular index, note that empty variables are removed
|
||||
3. SDVariable sequence(String name,INDArray[] arrays): create the sequence with the given variable name
|
||||
4. setItemForSequenceAtIndex(String varName,INDArray item,int index): set the item at the particular index
|
||||
5. sequenceLength(String varName): returns the length of a sequence
|
||||
|
||||
|
||||
Note that all indices above also support negative indexing allowing you to index from the end of the list similar
|
||||
to python.
|
||||
|
||||
### Model import
|
||||
For model import, this also extends the work in [0004-Mapping_IR.md](0004-Mapping_IR.md)
|
||||
to support sequences/lists of tensors. This means IRTensor and similar concepts will also support sequences.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Allows flexible import of models that use sequences to pass around ndarrays
|
||||
* Allows flexible management of ndarrays using a variable name as a group.
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* Not robust enough to execute ops on
|
||||
* Still need to implement Ragged Tensors at some point separately.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Invoke
|
||||
|
||||
## Status
|
||||
**Discussion**
|
||||
|
||||
Proposed by: Adam Gibson (10th April 2022)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
A common use case in model import is the ability to modularize graphs as sub functions.
|
||||
Typically, these graphs are attributes of a node. Samediff already possesses the ability to store
|
||||
samediff graphs in a dictionary called SameDiffFunctionInstances.
|
||||
|
||||
Graphs with control flow ops such as switch, if and while also tend to heavily use sub graphs.
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
Invoke builds on the work from [SDValue](./0018%20-%20SDValue.md) and leverages returning an ExecutionResult (a dictionary of name to SDValue)
|
||||
passing the result of the invoke function as an op output in a larger parent graph.
|
||||
|
||||
|
||||
### InvokeParams
|
||||
Invoke takes node outputs from the parent graph and passes them through renamed to match the expected inputs and outputs from the sub graph.
|
||||
This takes the form of InvokeParams:
|
||||
```java
|
||||
public static class InvokeParams {
|
||||
private String functionName;
|
||||
private SDVariable[] inputs;
|
||||
private String[] inputVarNames;
|
||||
private String[] outputVarNames;
|
||||
private String[] subGraphInputVarNames;
|
||||
private String[] subGraphOutputVarNames;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
InvokeParams has the expected input and output names for the graph and matching subgraph input and output variable names.
|
||||
|
||||
|
||||
### Outputs
|
||||
|
||||
Invoke has a special outputVariables() method for returning output values that match the outputs of the subgraph.
|
||||
All outputs will have the same data types as the underlying result.
|
||||
|
||||
These outputs are all converted to the ARRAY type
|
||||
since the output variables are derived from the output of a function. For easy readability these op output variables will have
|
||||
the same name of the output with a _functionName where functionName is the name of the function used to lookup the subgraph
|
||||
to invoke.
|
||||
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Allows invocation of subgraphs easily as an op
|
||||
* Allows for more flexibility in how a user sets a graph up
|
||||
* Can be combined with control flow to easily build loops
|
||||
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* More complexity in a graph
|
||||
* Potential source of bugs when naming parameters and setting up the proper invoke calls
|
||||
@@ -0,0 +1,160 @@
|
||||
# Control flow
|
||||
|
||||
## Status
|
||||
**Discussion**
|
||||
|
||||
Proposed by: Adam Gibson (10th April 2022)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
Samediff supports control flow such as if statements and while loops. However this is not enough and common looping structures
|
||||
are still hard to use. Onnx has introduced a Loop operation. Loop requires a graph with pre configured graph.
|
||||
The graph takes in and outputs:
|
||||
1. current iteration
|
||||
2. max number of iterations
|
||||
3. extra condition to use
|
||||
|
||||
It approximates a for loop with the following code:
|
||||
```java
|
||||
boolean cond = ...;
|
||||
int maxIterations = ...;
|
||||
for(int i = 0; i < maxIterations && cond; i++) {
|
||||
loop body...
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
The loop body is represented as a sub graph attribute on the operation.
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
Similar to onnx's loop operation coupled with [Invoke](./0019%20-%20Invoke.md)
|
||||
we provide a new loop that leverages invoke and some fixed conventions of the graph to use a loop body:
|
||||
```java
|
||||
/**
|
||||
* Loop with conditions.
|
||||
* For more information see the underlying class
|
||||
* {@link ControlFlow#loopWithConditions(String[], String, SameDiff, SameDiff, String, SDVariable[], String[], String[])}
|
||||
* @param loopParams the loop parameters to loop with
|
||||
* @return
|
||||
*/
|
||||
public SDVariable[] loopWithConditions(ControlFlow.LoopParams loopParams) {
|
||||
```
|
||||
|
||||
|
||||
### LoopParams
|
||||
LoopParams looks like the following:
|
||||
```java
|
||||
public static class LoopParams {
|
||||
private String[] outputVarNames;
|
||||
private String loopName;
|
||||
private SameDiff parent;
|
||||
private SameDiff functionBody;
|
||||
private String functionName;
|
||||
private SDVariable[] loopVars;
|
||||
private String[] functionBodyInputs;
|
||||
private String[] functionBodyOutputs;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
LoopParams has the following fields:
|
||||
1. outputVarNames: the output variable names for the loop
|
||||
2. The name of the loop controls the name in the control flow with scopeName/loopName/variableName as the convention
|
||||
scopeName is the current frame (such as within the loop body), the loop name is the loop name showing which loop it is
|
||||
and the variableName represents the variable within the loop and frame.
|
||||
3. Parent: the invoking samediff function
|
||||
4. functionName: The name of the function to be looked up from the parent and invoked using invoke
|
||||
5. loopVars: the input and outputs of the loops
|
||||
6. functionBodyInputs: the function input names to use with Invoke
|
||||
7. functionBodyOutputs: the function output names to be returned from Invoke
|
||||
|
||||
|
||||
|
||||
### Example Usage
|
||||
|
||||
```java
|
||||
//setup the parent graph to pass inputs to the lambda
|
||||
SameDiff parent = SameDiff.create();
|
||||
SDVariable input = parent.placeHolder("input",DataType.FLOAT);
|
||||
//setup the loop body
|
||||
SameDiff loopBody = SameDiff.create();
|
||||
SDVariable loopInput = loopBody.placeHolder("input", DataType.FLOAT);
|
||||
SDVariable output = loopBody.math().add("output",loopInput,1.0);
|
||||
//initialize the control flow with the default parameters such as the current iteration, the max number of iterations and the conditional output from the graph
|
||||
SDVariable[] args = ControlFlow.initializeLoopBody(new String[]{"curr_iteration", "max_iterations", "cond_in"}, parent, 5, true);
|
||||
SDVariable[] childArgs = ControlFlow.initializeLoopBody(new String[]{"curr_iteration", "max_iterations", "cond_in"}, loopBody, 5, true);
|
||||
|
||||
//input names for the input graph with the 4th input being the input from the parent
|
||||
String[] inputNames = {
|
||||
"curr_iteration",
|
||||
"max_iterations",
|
||||
"cond_in",
|
||||
"input"
|
||||
};
|
||||
|
||||
//output names from the output of the lmabda with the 4th being the result of the lamda's application of the input
|
||||
String[] outputNames = {
|
||||
"curr_iteration",
|
||||
"max_iterations",
|
||||
"cond_in",
|
||||
"output"
|
||||
};
|
||||
|
||||
|
||||
//setup the loop variables for input
|
||||
SDVariable[] finalArgs = new SDVariable[args.length + 1];
|
||||
for(int i = 0; i < args.length; i++) {
|
||||
finalArgs[i] = args[i];
|
||||
}
|
||||
finalArgs[3] = input;
|
||||
|
||||
|
||||
//put it all together in the loop parameters
|
||||
ControlFlow.LoopParams loopParams = ControlFlow.LoopParams.builder()
|
||||
.parent(parent)
|
||||
.functionBody(loopBody)
|
||||
.functionBodyInputs(inputNames)
|
||||
.functionBodyOutputs(outputNames)
|
||||
.loopVars(finalArgs)
|
||||
.loopName("loop")
|
||||
.functionName("func")
|
||||
.build();
|
||||
|
||||
//control the output parameter names
|
||||
String[] finalOutputNames = new String[outputNames.length];
|
||||
for(int i = 0; i < finalOutputNames.length; i++) {
|
||||
finalOutputNames[i] = outputNames[i] + "_final";
|
||||
}
|
||||
|
||||
//test the output variables, the names will match the specified output names
|
||||
SDVariable[] loopWithConditions = parent.loopWithConditions(finalOutputNames,loopParams);
|
||||
|
||||
INDArray assertion = Nd4j.ones(5).addi(5);
|
||||
Map<String, INDArray> output2 = parent.output(Collections.singletonMap("input", Nd4j.ones(5)), "output_final");
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* More explicit wrapper for loop
|
||||
* More conventions for looping
|
||||
* Easier to use
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* More parameters than while loop
|
||||
* Inherits the complexity of invoke with the number of parameters needed
|
||||
* Number of inputs and outputs must match. Might not be intuitive for the user.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Create View
|
||||
|
||||
## Status
|
||||
**Discussion**
|
||||
|
||||
Proposed by: Adam Gibson (27th April 2022)
|
||||
Discussed with: Paul Dubs
|
||||
Finalized by: Adam Gibson (5th May 2022)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
Samediff's op graph mainly focuses on immutability to prevent bugs making many variable inputs and outputs for various ops copy on write.
|
||||
|
||||
This prevents performance gains with in place ops such as +=.
|
||||
|
||||
Currently, the samediff strided_slice operation works to provide a view of an array
|
||||
but the view is a copy. This limitation prevents performance gains you typically see in views.
|
||||
|
||||
|
||||
## Decision
|
||||
|
||||
CreateView is an op that takes in a set of SDVariables that represents index information similar to nd4j's point, interval,all, and new axis. This op allows for dynamic generation of views of variables.
|
||||
|
||||
CreateView is a building block for other ops to execute in place operations. Usage of CreateView should be deliberate and only used in certain circumstances.
|
||||
CreateView simply (using the aforementioned index inputs) creates a view using the existing data buffer and returns an output that wraps the same exact buffer as is rendered as an alternative view in a similar way as nd4j's indexing mechanisms.
|
||||
|
||||
|
||||
|
||||
These inputs are represented as follows:
|
||||
|
||||
1. Interval: INTERVAL_TYPE,2,1,start,end,stride,inclusive
|
||||
2. Point: POINT_TYPE,1,1,offset, DEFAULT_INCLUSIVE
|
||||
3. All: ALL_TYPE,0,1, DEFAULT_INCLUSIVE
|
||||
4. New Axis: NEW_AXIS,1,10, DEFAULT_INCLUSIVE
|
||||
|
||||
|
||||
This describes the general pattern the above described buffers follow:
|
||||
1. type of index
|
||||
2. Number of indices (representing offsets)
|
||||
3. Stride
|
||||
4. Inclusive/exclusive
|
||||
|
||||
|
||||
Of note here are a few constants representing types to be passed to the ops:
|
||||
1. *_TYPE: a pre-defined constant representing the kind of index this is
|
||||
2. DEFAULT_INCLUSIVE: whether the index's end is inclusive or not (only needed for intervals)
|
||||
this is by default 0 most of the time since the value is only relevant for intervals.
|
||||
|
||||
These are created as INT64 ndarrays passed in to the
|
||||
operation itself.
|
||||
|
||||
An omission of indexing here is SpecifiedIndex. Since SpecifiedIndex requires a copy most of the time, this op will mainly be focused on indexing that is guaranteed to use the same buffer.
|
||||
|
||||
|
||||
## In place exception in gradient checks
|
||||
|
||||
|
||||
Usually, arrays during training should not modify their outputs. Instead, new output arrays are allocated with calculated results being inserted into these pre-defined outputs.
|
||||
|
||||
However, CreateView is, by definition, special since it is a building block for enabling manipulation of a view of the same data buffer as the input. The gradient checks in the [NonInplaceValidationListener](https://github.com/eclipse/deeplearning4j/blob/master/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/autodiff/validation/listeners/NonInplaceValidationListener.java#L43) make an exception to this rule to account for this particular behaviour.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Self contained op for creating a view leveraging nd4j's existing indexing engine but
|
||||
better integrated as a libnd4j op allowing for better dynamic indexing of arrays
|
||||
* Similar in usage to indexes
|
||||
* Contains the potential bugs from views to a pre-specified op
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* Could introduce new bugs upon use
|
||||
* Not the easiest interface requiring some constant methods
|
||||
* Not fully integrated in to the main engine/not as transparent as a tool as it should be
|
||||
@@ -0,0 +1,44 @@
|
||||
# Dynamic Indexing
|
||||
|
||||
## Status
|
||||
**Discussion**
|
||||
|
||||
Proposed by: Adam Gibson (30th April 2022)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
Dynamic indexing has a wide variety of applications for building deep learning graphs.
|
||||
|
||||
Expressing a dynamic index that gets resolved at runtime entails specifying a negative index as the desired index for an indexing operation.
|
||||
At runtime, the indexing engine will then count backwards from the element the user specified. An example being: -1 starts at the end, -2 starts at the second to last.
|
||||
|
||||
Samediff and nd4j have numpy style indexing support.
|
||||
|
||||
|
||||
Samediff does this 1 of 2 ways. Either through nd4j's indexing engine building on [previous work](./0021%20-%20Create%20View.md) or using a similar interface to nd4j's indexing engine to build a strided slice operation call.
|
||||
Previously, the indexing would not allow initialization with a negative index without passing in an ndarray. The problem with this is ndarrays are not known in samediff till execution time.
|
||||
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
|
||||
We support the ability to dynamically resolve indices during execution. This happens as follows:
|
||||
1. Each index has a concept of initialized().
|
||||
This returns a boolean indicating whether the operation was initialized or not. Initialized means there are no negative values present in the index and a specific boolean flag has been set representing the index being fully initialized.
|
||||
2. If a negative index is specified, the index is set on the index itself but it will not be considered initialized.
|
||||
3. At runtime, upon use a deep copy of the index happens and then initialization will occur upon use relative to the ndarray using it. This currently happens at the java level. Indexing does not exist fully at the c++ level.
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Allows for more flexible indexing only possible with just in time resolution.
|
||||
* Less errors are thrown during indexing
|
||||
|
||||
|
||||
### Disadvantages
|
||||
* A bit more overhead in the indexing process
|
||||
* Harder to debug
|
||||
@@ -0,0 +1,198 @@
|
||||
# UDFs
|
||||
|
||||
## Status
|
||||
|
||||
Implemented
|
||||
|
||||
Proposed by: Adam Gibson (31 Jan 2023)
|
||||
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
Finalized by: Adam Gibson (2nd Feb 2023)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
Users should be able to define their own custom operations in SameDiff, including custom gradients.
|
||||
Currently, defining a User-Defined Function (UDF) is not properly integrated into SameDiff and requires handling multiple aspects of the system, such as:
|
||||
|
||||
1. Registering the operation with the ImportClassMappings operation registry
|
||||
2. Implementing special code paths in the operation executioners to support calling user-defined code
|
||||
3. Implementing special code paths for serialization to correctly load a SameDiff graph with the operation
|
||||
4. Making a direct function call in SameDiff to properly register the operation as part of the graph.
|
||||
|
||||
## Proposal
|
||||
To support custom UDFs in SameDiff, the following components will be created:
|
||||
|
||||
1. An operation type for the FlatBuffersMapper (used for saving graphs) to know how to save and load UDFs
|
||||
2. A base class extending DynamicCustom with clear method and constructor overrides for defining a custom operation
|
||||
3. An execution method where the user passes in relevant inputs, which will be used by the operation executioner (instead of accessing the low-level code)
|
||||
4. A hook in SameDiff to register the UDF, such as sd.udf(...)
|
||||
5. An annotation or subclass scanner to discover and register user-defined operations in relevant areas, such as the ImportClassMappings.
|
||||
|
||||
These components work together to allow for the following:
|
||||
|
||||
1. Scanning for annotations to discover and register user-defined operations with the operation registry
|
||||
2. Users extending the base class to create their custom UDFs
|
||||
3. Integrating UDFs into SameDiff by registering the operation with the graph, for example:
|
||||
java
|
||||
```java
|
||||
SameDiff sd = SameDiff.create();
|
||||
UserDefinedCustomOp userDefinedCustomOp = ...;
|
||||
SDVariable[] opOutputs = sd.doUdf(userDefinedCustomOp);
|
||||
```
|
||||
|
||||
When an operation is registered, it is saved and loaded with the graph like any other operation.
|
||||
Dynamic creation of operations via reflection when a graph is loaded, using the annotation scanning.
|
||||
|
||||
Below is an example:
|
||||
```java
|
||||
@UserDefinedOp // Annotation for discovering custom ops to register
|
||||
public class TestAddUdf extends UserDefinedCustomOp { // Class to extend
|
||||
|
||||
|
||||
// Empty constructor. Used when creating a graph from flatbuffers in the underlying { org.nd4j.autodiff.samediff.serde.FlatBuffersMapper}.
|
||||
public TestAddUdf() {
|
||||
super();
|
||||
}
|
||||
|
||||
// Other constructors can be whatever the user wishes. Custom ops usually take in a
|
||||
// SameDiff instance and one or more SDVariable args. These are the minimum components to instantiate an op.
|
||||
// Each of these calls super(...) to properly configure the op to be used within the SameDiff graph passed in.
|
||||
|
||||
public TestAddUdf(SameDiff sameDiff, SDVariable arg) {
|
||||
super(sameDiff, arg);
|
||||
}
|
||||
|
||||
public TestAddUdf(SameDiff sameDiff, SDVariable[] args) {
|
||||
super(sameDiff, args);
|
||||
}
|
||||
|
||||
// Used to calculate output variables when registering an op with a graph.
|
||||
@Override
|
||||
public List<DataType> calculateOutputDataTypes(List<DataType> dataTypes) {
|
||||
// A user must implement this method. It is used in SameDiff to determine the number of
|
||||
// output variables needed when it can't be determined from getNumOutputs().
|
||||
return Arrays.asList(dataTypes.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPropertiesForFunction(Map<String, Object> properties) {
|
||||
// A user can define properties as fields. If so, they must implement this method and propertiesForFunction().
|
||||
// These are used to create an op from scratch when saving/loading a model.
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> propertiesForFunction() {
|
||||
// Returns properties (fields on the java class) as a map. Properties can be any value that
|
||||
// is a field on the op itself. These properties are optional and may not be needed,
|
||||
// depending on the op. All properties will end up being passed to the underlying iArguments,
|
||||
// tArguments, and other associated data structures inherited from DynamicCustomOp.
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumOutputs() {
|
||||
// Returns the number of outputs for the op. If an op has a variable number of outputs,
|
||||
// a user will need to use an SDVariable.eval() call to return an int to determine the number of outputs.
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String opName() {
|
||||
// The op name, required for proper registration with the registry.
|
||||
return "test_add_udf";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureFromArguments() {
|
||||
// A hook for configuring the op after creation. Used for configuration from specified arguments,
|
||||
// such as ints, floats/doubles, and input variables. The arguments referenced are the underlying
|
||||
// arguments that get passed to every c/c++ ops, including iArguments, tArguments, dArguments,
|
||||
// inputArguments, and outputArguments.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureWithSameDiff(SameDiff sameDiff) {
|
||||
this.sameDiff = sameDiff;
|
||||
// Implemented this method for handling initialization after the op is created. It initiates values using relevant
|
||||
// SameDiff metadata, such as obtaining input and output argument metadata from SDVariable found as args().
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isInplaceCall() {
|
||||
// Indicates whether the inputs are also the outputs.
|
||||
// Note that extra care should be taken to avoid bugs when an operation is in-place.
|
||||
// This is particularly important when an input to an operation is a view.
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public List<LongShapeDescriptor> calculateOutputShape() {
|
||||
// Describes how to calculate the output shape based on the inputs.
|
||||
// Note that calculateOutputShape is called when dynamically creating output arrays to store the result
|
||||
// of an operation's execution.
|
||||
// It is not called when an operation is in-place.
|
||||
return Arrays.asList(inputArguments.get(0).shapeDescriptor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LongShapeDescriptor> calculateOutputShape(OpContext oc) {
|
||||
// Describes how to calculate the output shape based on the inputs from the operation context.
|
||||
// Note that calculateOutputShape is called when dynamically creating output arrays to store
|
||||
// the result of an operation's execution.
|
||||
// This is different from the above method as the inputs are obtained from the operation
|
||||
// context instead of the operation itself.
|
||||
// It is not called when an operation is in-place.
|
||||
return Arrays.asList(oc.getInputArrays().get(0).shapeDescriptor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SDVariable> doDiff(List<SDVariable> f1) {
|
||||
// The doDiff method must be implemented by the user if the operation is to be used for training.
|
||||
// It should return one gradient for each input.
|
||||
return new AddBpOp(sameDiff, larg(), rarg(), f1.get(0)).outputs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
// The exec method for the operation itself, consisting of operation execution
|
||||
// and setting the outputs for the operation.
|
||||
AddOp addOp = new AddOp();
|
||||
addOp.addInputArgument(inputArguments.get(0), inputArguments.get(1));
|
||||
Nd4j.getExecutioner().exec(addOp);
|
||||
this.outputArguments.addAll(addOp.outputArguments);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec(OpContext opContext) {
|
||||
// The exec method for the operation itself, consisting of operation execution and
|
||||
// setting the outputs for the operation context.
|
||||
Nd4j.getExecutioner().exec(new AddOp(), opContext);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
With the above definition, a user just has to pass in a created op as an instantiated object.
|
||||
As long as an op is annotated it is properly integrated with the samediff graph.
|
||||
|
||||
When executing, the special code paths in the op executioners will call exec() or exec(opContext)
|
||||
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Allows users to define their own ops to make optimizations or to introduce custom ops
|
||||
for use within samediff
|
||||
* Augments model import by combining annotation scanning from model import with
|
||||
annotation registration of udfs with similar annotations
|
||||
|
||||
### Disadvantages
|
||||
* A bit lower level which means users can misuse the api or encounter bugs they might not
|
||||
otherwise with ops maintained in the core framework.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Graph Execution Trace Collection and Reproduction
|
||||
|
||||
## Status
|
||||
Implemented
|
||||
|
||||
Proposed by: Adam Gibson (20 Mar 2023)
|
||||
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
Finalized by: Adam Gibson (24 Mar 2023)
|
||||
|
||||
## Context
|
||||
|
||||
Reproducing a specific graph execution between the SameDiff and DL4J APIs can be
|
||||
challenging, as both use the underlying libnd4j operations to execute code.
|
||||
Currently, users enable verbose or debug mode in the op executioner to observe
|
||||
executed operations and manually compare the output of the two APIs. This method is
|
||||
suboptimal and time-consuming.
|
||||
|
||||
In the context of this proposal, the term "vector" refers to an `std::vector` in C++
|
||||
that stores the metadata of each operation execution. It does not refer to a
|
||||
mathematical vector or a tensor typically used in deep learning libraries. The
|
||||
`std::vector` is a dynamic array-like container provided by the C++ Standard Library,
|
||||
which is used here to store the sequence of operation executions.
|
||||
|
||||
## Decision
|
||||
|
||||
To improve the process, we will save execution traces in a format that can generate a
|
||||
SameDiff graph, emulating the executed steps. Once enabled, operation executions will
|
||||
be collected in a vector, storing only metadata such as input/output shapes and
|
||||
arguments for each operation. These executions will be stored in the vector
|
||||
sequentially.
|
||||
|
||||
For instance, when executing a convolution operation, we can trigger the scope in C++
|
||||
to indicate the current operation. This enables tracking the execution of the
|
||||
convolution operation and its nested operations, like the im2col operation.
|
||||
|
||||
Graph tracing can be enabled using the following command:
|
||||
```java
|
||||
Nd4j.toggleTrace(true);
|
||||
```
|
||||
|
||||
Using the vector of executions, we can reproduce a graph. To save the graph, use:
|
||||
```java
|
||||
SameDiff sd = SameDiff.collectTrace();
|
||||
sd.save(new File("mygraph.fb"));
|
||||
```
|
||||
|
||||
Afterward, purge the trace to prevent memory leaks:
|
||||
```java
|
||||
Nd4j.purgeTrace();
|
||||
```
|
||||
|
||||
When purge is done you can disable trace with:
|
||||
```java
|
||||
Nd4j.toggleTrace(false);
|
||||
```
|
||||
|
||||
## Consequences
|
||||
### Advantages
|
||||
* Simplifies graph reproduction
|
||||
* Enables decomposition of nested op execution, such as attention
|
||||
* Increases complexity when implementing ops, requiring the developer to notify the tracer of the current parent op
|
||||
|
||||
### Disadvantages
|
||||
* Generated graph may lack names or other metadata
|
||||
* Execution tracing should be performed only when executing one op at a time
|
||||
@@ -0,0 +1,183 @@
|
||||
# Workspaces
|
||||
|
||||
## Status
|
||||
|
||||
Implemented
|
||||
|
||||
Proposed by: Adam Gibson (14 Mar 2023)
|
||||
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
Neural networks require a significant amount of memory during execution, often in the range of billions of parameters.
|
||||
To improve performance and manage memory usage, we can take advantage of the fact that neural network allocations are cyclic in nature.
|
||||
Since most workloads repeatedly allocate the same ndarrays, we create a memory abstraction known as "workspaces" to avoid redundant memory allocation.
|
||||
This approach helps to optimize memory usage and enhance overall performance.
|
||||
|
||||
## Proposal
|
||||
|
||||
This architecture decision record discusses the implementation of the workspaces concept using ringbuffers within a
|
||||
namespace-like abstraction and Java's try/with resources for memory allocation and garbage collection.
|
||||
Workspaces require a configuration with several parameters for controlling memory allocation. (See the description section for more details.)
|
||||
|
||||
A MemoryManager is used to allocate an INDArray, and an operation (element-wise multiplication) is performed.
|
||||
The workspace and INDArray are automatically closed and released when the try blocks are exited.
|
||||
|
||||
The workspace tracks different types of memory, including:
|
||||
1. allocated memory
|
||||
2. external memory
|
||||
3. unreferenced memory
|
||||
4. workspace memory
|
||||
5. gradient memory.
|
||||
|
||||
We reduce memory usage by reusing the ring buffers described above. The key trick in reducing allocations is to reuse the same memory for the same operations
|
||||
learned through the learning policy. This is done by using a ring buffer to store the memory for each operation.
|
||||
In doing this, the user can reuse existing memory for training/inference increasing performance and reducing memory usage.
|
||||
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
To create a named scope that reuses memory instead of allocating it again, you can use ringbuffers within a namespace-like abstraction,
|
||||
and combine it with java's try/with resources to indicate a scope of memory as well as to automatically garbage collect relevant memory.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
In order to use a workspace we need to have a configuration to determine how a workspace is created
|
||||
and how it allocates memory. The following parameters are possible:
|
||||
|
||||
1. initialSize: The initial size of the workspace in bytes. If the workspace exceeds this size, it will be automatically expanded.
|
||||
|
||||
2. maxSize: The maximum size of the workspace in bytes. If the workspace tries to expand beyond this size, an exception will be thrown.
|
||||
|
||||
3. overallocationLimit: The amount of extra memory to allocate beyond the initial size when the workspace is created.
|
||||
This is useful for workloads that have high variability in their memory usage.
|
||||
|
||||
4. policyAllocation: The allocation policy for the workspace, which can be STRICT (strict allocation),
|
||||
OVERALLOCATE (overallocation), or ALWAYS (always allocate new memory).
|
||||
|
||||
5. policyLearning: The learning policy for the workspace, which can be NONE (no learning),
|
||||
OPTIMIZED (optimized learning), or TRAINING (full training mode).
|
||||
|
||||
6. policyMirroring: The mirroring policy for the workspace, which can be ENABLED (enable mirroring),
|
||||
DISABLED (disable mirroring), or HOST_ONLY (mirror only to host memory).
|
||||
|
||||
7. policySpill: The spill policy for the workspace, which can be FAIL (fail if workspace runs out of memory),
|
||||
REALLOCATE (reallocate memory on the fly), or EXTERNAL (spill to external memory).
|
||||
|
||||
8. overallocationLimit: The amount of extra memory to allocate beyond the initial size when the workspace is created.
|
||||
This is useful for workloads that have high variability in their memory usage.
|
||||
|
||||
9. tempBlockSize: The size of the temporary memory blocks used by the workspace, in bytes.
|
||||
|
||||
10. useCycleDetector: Whether to enable the cycle detector for the workspace, which detects and prevents memory leaks.
|
||||
workspaceMode: The workspace mode, which can be ENABLED (enable workspace mode), SINGLE (use a single global workspace),
|
||||
or NONE (disable workspace mode).
|
||||
|
||||
11. helperAllowFallback: Whether to allow fallback to the CPU when using GPU memory.
|
||||
|
||||
12. helperMinSize: The minimum size in bytes for workspace helper operations.
|
||||
|
||||
|
||||
|
||||
Example usage:
|
||||
In this example, we use try/with blocks to automatically close the workspace and release the INDArray from the workspace memory when
|
||||
the try block is exited.
|
||||
|
||||
We create a workspace with the specified configuration within the try block, and get the MemoryManager for the workspace.
|
||||
We allocate an INDArray using the workspace memory within another try block, and perform some operation
|
||||
on it (in this case, an element-wise multiplication).
|
||||
|
||||
|
||||
```java
|
||||
// create a workspace configuration with 1 GB initial size and host memory only
|
||||
WorkspaceConfiguration config = WorkspaceConfiguration.builder()
|
||||
.initialSize(1024 * 1024 * 1024) // 1 GB initial size
|
||||
.policyMirroring(MirroringPolicy.HOST_ONLY) // use host memory only
|
||||
.build();
|
||||
|
||||
// create a workspace with the specified configuration
|
||||
try (Workspace workspace = Nd4j.getWorkspaceManager().createNewWorkspace(config)) {
|
||||
|
||||
// get the memory manager for the workspace
|
||||
MemoryManager memMgr = workspace.getMemoryManager();
|
||||
|
||||
// allocate an INDArray using the workspace memory
|
||||
try (INDArray input = memMgr.allocate(new long[]{32, 32}, DataBuffer.Type.FLOAT)) {
|
||||
|
||||
// use the INDArray for some operation, e.g. element-wise multiplication
|
||||
input.muli(2);
|
||||
|
||||
} // the INDArray is automatically released from the workspace memory when the try block is exited
|
||||
|
||||
} // the workspace is automatically closed when the try block is exited
|
||||
```
|
||||
|
||||
Since we used try/with blocks to create the workspace and allocate the INDArray, they will be automatically
|
||||
closed and released from the workspace memory when the try blocks are exited, regardless of
|
||||
whether an exception is thrown or not.
|
||||
|
||||
|
||||
In order to create a workspace we need to track the following kinds of memory:
|
||||
Allocated memory: This is memory that has been explicitly allocated by the workspace for a particular operation or computation.
|
||||
|
||||
External memory: This is memory that has been allocated outside of the workspace, but is being used by operations within the workspace.
|
||||
External memory can be useful when working with large datasets or models that do not fit entirely within the workspace.
|
||||
|
||||
Unreferenced memory: This is memory that has been allocated by the workspace, but is no longer being used by any operations or computations.
|
||||
Unreferenced memory can be automatically deallocated by the workspace to free up memory resources.
|
||||
|
||||
Workspace memory: This is memory that has been explicitly allocated by the workspace itself for managing memory and workspace state.
|
||||
Workspace memory can include things like memory for managing scope, tracking allocations and deallocations, and managing internal structures.
|
||||
|
||||
Gradient memory: This is memory that is used for storing gradients during backpropagation during training.
|
||||
DL4J's workspaces can track different types of gradient memory, including standard gradients, external gradients, and deferred gradients.
|
||||
|
||||
|
||||
Note that misuse can cause memory leaks in the following ways:
|
||||
Not closing the workspace properly: If a workspace is not properly closed after use, it can cause a memory leak.
|
||||
This can happen when a user forgets to close the workspace or when an exception occurs and the workspace is not closed in the catch block.
|
||||
|
||||
Using a workspace for too long: If a workspace is used for too long, it can cause a memory leak.
|
||||
This can happen if the workspace is reused too many times or if it is not cleared after each use.
|
||||
|
||||
Holding onto references: If references to objects created within a workspace are held onto for too long, it can cause a memory leak.
|
||||
This can happen if objects are not released from the workspace after they are no longer needed.
|
||||
|
||||
Using too many workspaces: If too many workspaces are created, it can cause a memory leak.
|
||||
This can happen if workspaces are created unnecessarily or if they are not properly managed.
|
||||
|
||||
Incorrect workspace configuration: If the workspace is configured incorrectly, it can cause a memory leak.
|
||||
This can happen if the workspace is not allocated enough memory or if the allocation policy is not set correctly.
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Memory allocation: Workspaces allow for pre-allocation of memory to avoid the overhead associated with dynamic memory allocation during training.
|
||||
|
||||
* Memory reuse: By reusing allocated memory rather than allocating new memory for each operation, workspaces help to reduce memory fragmentation and improve performance.
|
||||
|
||||
* Scope management: Workspaces are created within a particular scope and can be closed once they are no longer needed. This allows for efficient memory management and prevents memory leaks.
|
||||
|
||||
* Automatic deallocation: When a workspace is closed, any memory that was allocated within the workspace is automatically deallocated, freeing up memory resources for other operations.
|
||||
|
||||
Multiple workspaces: DL4J allows for the creation of multiple workspaces, which can be useful when running multiple models or training processes simultaneously.
|
||||
|
||||
### Disadvantages
|
||||
|
||||
* Increased code complexity: Implementing workspaces in your code can add an additional layer of complexity and require more careful management of workspace creation and usage.
|
||||
|
||||
* Memory overhead: Workspaces require some overhead for workspace creation, management, and tracking, which can increase memory usage.
|
||||
|
||||
* Workspace size limitations: Since workspaces are pre-allocated with a fixed size, there may be cases where the allocated size is not sufficient for larger models or datasets. This can limit the performance and accuracy of the training process.
|
||||
|
||||
* Training slowdowns: Depending on the specific use case and how workspaces are implemented, there may be cases where using workspaces could actually slow down the training process rather than speed it up.
|
||||
|
||||
* Learning curve: Using workspaces effectively requires a good understanding of how they work and how to manage them properly, which may require some additional learning and training time.
|
||||
@@ -0,0 +1,72 @@
|
||||
# JavaCPP Pointer Tracking with AspectJ
|
||||
|
||||
## Status
|
||||
Implemented
|
||||
|
||||
Proposed by: Adam Gibson (18 Apr 2023)
|
||||
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
Finalized by: Adam Gibson (18 Apr 2023)
|
||||
|
||||
## Context
|
||||
|
||||
Tracking memory allocations and deallocations for JavaCPP pointers is challenging. This is
|
||||
important for understanding memory usage patterns and identifying memory leaks in
|
||||
applications that use JavaCPP. Currently, developers rely on manual tracking
|
||||
and debugging techniques,
|
||||
which are time-consuming and error-prone.
|
||||
|
||||
Aspect Oriented Programming (AOP) with AspectJ is used to intercept the allocation and
|
||||
deallocation of JavaCPP pointers, allowing for automatic tracking and reporting of memory usage.
|
||||
In this context, we are implementing an aspect and a memory counter for tracking JavaCPP pointer
|
||||
allocations and deallocations.
|
||||
|
||||
AOP is only enabled when used with compile time weaving. You have to build the profiler with
|
||||
the specified modules in order to enable the weaving.
|
||||
|
||||
The code should not be built with AOP by default due to the overhead.
|
||||
|
||||
## Decision
|
||||
|
||||
We use AspectJ to create an aspect called `MemoryCounterAspect` that intercepts
|
||||
the allocation and deallocation of JavaCPP pointers. This aspect leverages two
|
||||
around advice methods, `allocateMemory` and `deallocate`, to track the memory usage.
|
||||
|
||||
The `allocateMemory` method is triggered when a new JavaCPP pointer is created. It calculates
|
||||
the difference in physical bytes before and after the pointer allocation, and then increments
|
||||
the memory counter accordingly.
|
||||
|
||||
The `deallocate` method is triggered when a JavaCPP pointer is deallocated. It calculates the difference
|
||||
in physical bytes before and after the pointer deallocation, and then decrements the memory counter accordingly.
|
||||
|
||||
The memory counter, `MemoryCounter`, maintains two counters: `allocated` for tracking the total allocated memory,
|
||||
and `instanceCounts` for tracking the number of instances of each JavaCPP pointer class.
|
||||
|
||||
Example usage:
|
||||
|
||||
```java
|
||||
|
||||
|
||||
// Perform operations involving JavaCPP pointers
|
||||
// ...
|
||||
|
||||
// Get memory usage information
|
||||
Map<String, Long> allocatedMemory = MemoryCounter.getAllocated().getCounts();
|
||||
Map<String, Long> instanceCounts = MemoryCounter.getInstanceCounts().getCounts();
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
* Simplifies memory tracking for JavaCPP pointers
|
||||
* Reduces manual debugging efforts
|
||||
* Provides valuable insights into memory usage patterns and potential memory leaks
|
||||
|
||||
### Disadvantages
|
||||
* AspectJ introduces additional overhead and may slightly impact performance
|
||||
* The tracking aspect may not cover all possible allocation and deallocation scenarios, depending on the JavaCPP library's
|
||||
behavior and the application's usage patterns
|
||||
* The aspect may need to be updated to stay in sync with changes in the JavaCPP library
|
||||
@@ -0,0 +1,76 @@
|
||||
Function Tracing in nd4j and libnd4j Code Base
|
||||
Implemented by: Adam Gibson (26-04-2023)
|
||||
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
Overview
|
||||
The nd4j and libnd4j code base can be difficult to debug due to the lack of tools for tracing program execution.
|
||||
We have implemented function tracing using the -finstrument-functions flag provided by GCC to trace
|
||||
function calls and exits in the code base.
|
||||
This feature will help developers better understand the flow of the program and identify potential issues.
|
||||
|
||||
Function tracing can also work with CUDA, as long as GCC is the underlying compiler.
|
||||
|
||||
How to Enable Function Tracing
|
||||
To enable function tracing, follow these steps:
|
||||
|
||||
Build the code base with -Dlibnd4j.functrace=ON.
|
||||
Add the correct compiler flags to the javacpp plugin.
|
||||
Add the correct flags to the cmake build.
|
||||
Add a maven profile to each nd4j backend containing the correct compiler flags to work with -finstrument-functions.
|
||||
Build with Maven
|
||||
Here are the example Maven build commands for the CPU and CUDA backends, respectively:
|
||||
|
||||
For CPU backend:
|
||||
|
||||
```
|
||||
mvn -Dlibnd4j.functrace=ON -Pcpu clean install -DskipTests
|
||||
```
|
||||
For CUDA backend:
|
||||
|
||||
```
|
||||
mvn -Dlibnd4j.functrace=ON -Pcuda clean install -DskipTests
|
||||
```
|
||||
Make sure you have the necessary profiles configured in your pom.xml to enable tracing for the respective backend.
|
||||
|
||||
Implementation Details
|
||||
Compiler Flags
|
||||
We have used the following compiler flags to enable function tracing:
|
||||
|
||||
-Bsymbolic: Bind references to global symbols at link time, reducing the runtime overhead.
|
||||
-rdynamic: Export all dynamic symbols to the dynamic symbol table, making them available for backtracing.
|
||||
-fno-omit-frame-pointer: Do not omit the frame pointer, allowing for accurate backtraces.
|
||||
-fno-optimize-sibling-calls: Disable sibling call optimization to maintain the correct call stack.
|
||||
-finstrument-functions: Enable instrumentation of function entry and exit points.
|
||||
-g: Enable debugging information.
|
||||
-O0: Set the optimization level to zero for easier debugging.
|
||||
Setting the Output File
|
||||
We have implemented a method in each backend to set the output file for tracing results.
|
||||
|
||||
For the CPU backend, use the following code:
|
||||
|
||||
java
|
||||
```Copy code
|
||||
Nd4jCpu nd4jCpu = (Nd4jCpu) NativeOpsHolder.getInstance().getDeviceNativeOps();
|
||||
nd4jCpu.setInstrumentOut("profilerout.txt");
|
||||
```
|
||||
|
||||
|
||||
For the CUDA backend, use the following code:
|
||||
|
||||
java
|
||||
```
|
||||
Nd4jCuda nd4jCuda = (Nd4jCuda) NativeOpsHolder.getInstance().getDeviceNativeOps();
|
||||
nd4jCuda.setInstrumentOut("profilerout.txt");
|
||||
```
|
||||
These calls set the appropriate file to use for each backend. Note that we don't put this in NativeOps (the parent backend agnostic interface for this) because this normally should not be included in any builds due to overhead.
|
||||
|
||||
LD_PRELOAD and LD_DEBUG
|
||||
To ensure the correct implementation of the enter/exit functions is used, LD_PRELOAD is utilized to preload the libnd4j binary generated during the build. The built-in libc implementation of these functions is a no-op, so preloading the libnd4j binary is necessary.
|
||||
|
||||
LD_DEBUG can be used to verify that the correct implementation is being used by showing the symbols being loaded and their origin.
|
||||
|
||||
Sample Log Output
|
||||
bash
|
||||
Copy code
|
||||
g long> > >::end() (/home/agibsonccc/Documents/GitHub/deeplearning4j/libnd4j/blasbuild/cpu/blas/libnd4
|
||||
@@ -0,0 +1,63 @@
|
||||
# Implement ND4J Log Analyzer for Operation Execution Analysis
|
||||
|
||||
## Status
|
||||
**Proposed**
|
||||
|
||||
Proposed by: Adam Gibson(2024-08-02)
|
||||
|
||||
## Context
|
||||
|
||||
DeepLearning4J applications rely heavily on ND4J operations for neural network computations. As the library evolves, there's a need to identify regressions between different versions and analyze the execution patterns and performance metrics of ND4J operations. Currently, there's no standardized tool for recording, storing, and analyzing these operations in detail.
|
||||
|
||||
## Proposal
|
||||
|
||||
Implement an ND4J Log Analyzer as a Java agent with the following key features:
|
||||
|
||||
1. Record ND4J operation executions in real-time and store them in an H2 database.
|
||||
2. Index the specified DeepLearning4J codebase for reference.
|
||||
3. Provide utilities for querying and analyzing recorded operations:
|
||||
- StackTraceCodeFinder for locating source code lines
|
||||
- JsonComparisonReport for comparing operation logs between different runs or versions
|
||||
- JsonReport for exporting operation logs to JSON format
|
||||
4. Allow injection into running DeepLearning4J applications as a Java agent.
|
||||
|
||||
The analyzer will use two main configuration options:
|
||||
- `sourceCodeIndexerPath`: Path to the DeepLearning4J codebase to index.
|
||||
- `javaagent`: Path to the compiled ND4J Log Analyzer JAR file.
|
||||
|
||||
Data will be stored in two main tables:
|
||||
1. `OpLogEvent`: For storing ND4J operation executions.
|
||||
2. `SourceCodeLine`: For storing indexed source code information.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Enables detailed analysis of ND4J operations across different versions.
|
||||
* Provides a standardized method for recording and storing operation executions.
|
||||
* Allows for easy identification of regressions or unexpected changes in behavior.
|
||||
* Supports both real-time analysis and post-execution comparisons.
|
||||
* Integrates seamlessly with existing DeepLearning4J applications through Java agent injection.
|
||||
|
||||
### Disadvantages
|
||||
|
||||
* Adds computational overhead to the running application due to real-time logging.
|
||||
* Requires additional storage for the H2 database and generated reports.
|
||||
* May require updates to maintain compatibility with future versions of DeepLearning4J and ND4J.
|
||||
* Users need to learn how to use and interpret the new analysis tools.
|
||||
|
||||
### Risks
|
||||
|
||||
* Potential for performance impact on production systems if not used carefully.
|
||||
* Possibility of generating large amounts of data that need to be managed and stored securely.
|
||||
* Risk of false positives in regression detection due to non-deterministic behaviors in some ND4J operations.
|
||||
|
||||
## Action Items
|
||||
|
||||
1. Develop the core Java agent for ND4J operation interception and logging.
|
||||
2. Implement the H2 database schema and data storage mechanisms.
|
||||
3. Create utilities for source code indexing and stack trace analysis.
|
||||
4. Develop tools for JSON report generation and comparison.
|
||||
5. Write comprehensive documentation and usage guides.
|
||||
6. Conduct thorough testing with various DeepLearning4J applications and versions.
|
||||
7. Create a release plan and migration guide for existing DeepLearning4J users.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Refactor nd4j to Centralize Offset Storage and Introduce OpaqueNDArray
|
||||
|
||||
## Status
|
||||
|
||||
**Proposed**
|
||||
|
||||
Proposed by: Adam Gibson Oct 24,2024
|
||||
|
||||
## Context
|
||||
|
||||
The current nd4j codebase stores offsets in multiple locations, including opaque data buffers and data buffers. This scattered approach leads to inconsistencies, potential bugs, and difficulties in maintenance. Additionally, the current method of passing NDArray components from Java to C++ involves manually unpacking various elements, which is error-prone and cumbersome.
|
||||
|
||||
## Proposal
|
||||
|
||||
We propose to refactor the nd4j codebase to centralize offset storage within NDArrays and introduce a new OpaqueNDArray type for improved Java-C++ interoperability. The key features of this proposal include:
|
||||
|
||||
1. Moving all offset information into NDArray objects, removing them from opaque data buffers and data buffers.
|
||||
2. Introducing an OpaqueNDArray type in C++, which is an alias for NDArray*.
|
||||
3. Updating the Java-C++ interop layer to use OpaqueNDArray for passing NDArray information.
|
||||
4. Refactoring existing code to use the new centralized offset storage and OpaqueNDArray.
|
||||
5. Updating documentation and coding standards to reflect these changes.
|
||||
|
||||
Example of the proposed change:
|
||||
|
||||
// Before
|
||||
void someOperation(void* dataBuffer, sd::LongType* shapeBuffer, sd::LongType offset, ...) {
|
||||
// Manual unpacking and offset handling
|
||||
}
|
||||
|
||||
// After
|
||||
typedef NDArray* OpaqueNDArray;
|
||||
|
||||
void someOperation(OpaqueNDArray array) {
|
||||
// Access all necessary information through the NDArray object
|
||||
sd::LongType offset = array->offset();
|
||||
// ...
|
||||
}
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* Improves code consistency and reduces the risk of offset-related bugs.
|
||||
* Simplifies the Java-C++ interop by encapsulating NDArray information.
|
||||
* Reduces the likelihood of errors from manual unpacking of NDArray components.
|
||||
* Makes the codebase more maintainable and easier to understand.
|
||||
|
||||
### Disadvantages
|
||||
|
||||
* Requires a significant refactoring effort across the nd4j codebase.
|
||||
* May introduce temporary bugs during the transition if not done carefully.
|
||||
* Could potentially impact performance if not optimized properly.
|
||||
* Might require updates to external code that interacts with nd4j.
|
||||
|
||||
### Risks
|
||||
|
||||
* Risk of introducing bugs during the refactoring process, especially in complex operations.
|
||||
* Potential for decreased performance if the new offset access methods are not efficiently implemented.
|
||||
* May cause confusion for developers who are accustomed to the current system.
|
||||
* Could potentially break existing code that relies on the current offset storage mechanism.
|
||||
|
||||
## Action Items
|
||||
|
||||
1. Develop a comprehensive guide for implementing and using the new offset storage system and OpaqueNDArray.
|
||||
2. Create a set of unit tests to verify the correctness of offset handling and OpaqueNDArray usage.
|
||||
3. Update the team's coding standards to include guidelines on using the new offset storage and OpaqueNDArray.
|
||||
4. Conduct a pilot implementation on a small, isolated part of the codebase.
|
||||
5. Schedule the refactoring process, prioritizing the most frequently used operations.
|
||||
6. Update all relevant documentation, including comments in the code and API documentation.
|
||||
7. Implement benchmarks to compare performance before and after the changes.
|
||||
8. Conduct thorough code reviews and testing for each refactored section.
|
||||
9. Plan for a grace period to allow developers to familiarize themselves with the new system.
|
||||
10. Monitor and address any issues or feedback arising from the new offset storage and OpaqueNDArray usage.
|
||||
11. Consider creating a static analysis tool to ensure consistent usage of the new system across the codebase.
|
||||
12. Update any build scripts or configuration files that may be affected by the changes.
|
||||
13. Prepare a migration guide for users of nd4j to update their code to work with the new system.
|
||||
@@ -0,0 +1,320 @@
|
||||
# Publish a Smaller Artifact with Limited Type Support
|
||||
|
||||
## Status
|
||||
|
||||
**Proposed**
|
||||
|
||||
Proposed by: [Adam Gibson] Oct 22, 2024
|
||||
|
||||
## Context
|
||||
|
||||
The current C++ library published via Java Maven supports multi-type arithmetic, achieved through extensive template usage in C++ and built using CMake. While this approach provides flexibility by accommodating various data types, it results in a higher binary size. For many users, the full spectrum of type support may be unnecessary, leading to increased storage requirements and longer download times. To address these concerns, there is consideration to publish a smaller artifact that supports only specific types, thereby reducing the binary size.
|
||||
|
||||
## Proposal
|
||||
|
||||
We propose to publish a smaller Maven artifact of the C++ library that supports a limited set of data types. This specialized artifact will cater to users who do not require multi-type arithmetic, offering a more lightweight alternative. The key aspects of this proposal include:
|
||||
|
||||
1. **Create a Limited Type Support Artifact:**
|
||||
- Develop a separate build configuration using CMake that includes only the necessary type specializations.
|
||||
- Ensure that the limited artifact excludes type support beyond the specified types to minimize binary size.
|
||||
|
||||
2. **Maintain the Existing Multi-Type Artifact:**
|
||||
- Continue to offer the current multi-type arithmetic artifact for users who need comprehensive type support.
|
||||
|
||||
3. **Clear Naming and Versioning:**
|
||||
- Use distinct naming conventions (e.g., `mylib-core`, `mylib-lite`) to differentiate between the full and limited artifacts.
|
||||
- Align versioning strategies to ensure compatibility and ease of maintenance.
|
||||
|
||||
4. **Update Documentation and Support Materials:**
|
||||
- Clearly document the differences between the full and limited artifacts.
|
||||
- Provide guidelines on selecting the appropriate artifact based on user needs.
|
||||
|
||||
5. **Implement Testing for Both Artifacts:**
|
||||
- Develop separate test suites to validate the functionality and performance of each artifact.
|
||||
- Ensure that the limited artifact maintains the core functionality required by its target users.
|
||||
|
||||
### Example Test
|
||||
|
||||
As a sample test to ensure the limited artifact functions correctly, consider the following parameterized test:
|
||||
|
||||
```java
|
||||
@ParameterizedTest
|
||||
@MethodSource("org.nd4j.linalg.BaseNd4jTestWithBackends#configs")
|
||||
public void testMixedDataTypeViews(Nd4jBackend backend) {
|
||||
INDArray arrFloat = Nd4j.arange(24).reshape(4, 6).castTo(DataType.FLOAT);
|
||||
INDArray arrDouble = Nd4j.arange(24).reshape(4, 6).castTo(DataType.DOUBLE);
|
||||
INDArray arrLong = Nd4j.arange(24).reshape(4, 6).castTo(DataType.LONG);
|
||||
|
||||
INDArray viewFloat = arrFloat.get(NDArrayIndex.interval(1, 3), NDArrayIndex.interval(2, 5));
|
||||
INDArray viewDouble = arrDouble.get(NDArrayIndex.interval(1, 3), NDArrayIndex.interval(2, 5));
|
||||
INDArray viewLong = arrLong.get(NDArrayIndex.interval(1, 3), NDArrayIndex.interval(2, 5));
|
||||
|
||||
assertEquals(8.0f, viewFloat.getFloat(0, 0), 1e-5);
|
||||
assertEquals(16.0f, viewFloat.getFloat(1, 2), 1e-5);
|
||||
assertEquals(8.0, viewDouble.getDouble(0, 0), 1e-5);
|
||||
assertEquals(16.0, viewDouble.getDouble(1, 2), 1e-5);
|
||||
assertEquals(8L, viewLong.getLong(0, 0));
|
||||
assertEquals(16L, viewLong.getLong(1, 2));
|
||||
}
|
||||
```
|
||||
```cpp
|
||||
Key Macros for Type Promotion Approach
|
||||
To manage type promotion effectively within the limited artifact, the following macros and templates are utilized:
|
||||
|
||||
|
||||
/*
|
||||
* Type Ranking System:
|
||||
* type_rank template and its specializations assign an integer rank to each supported type.
|
||||
* This ranking helps in determining the "promoted" type when combining different types.
|
||||
* Type Promotion Traits:
|
||||
* promote_type and promote_type3 templates determine the promoted type between two or three types based on their ranks.
|
||||
* Type Name System:
|
||||
* type_name template and its specializations provide a string representation for each supported type.
|
||||
* Helper Functions and Macros:
|
||||
* promote function template converts a value to the promoted type.
|
||||
* Macros like INSTANTIATE_PROMOTE and CALLBACK_INSTANTIATE_PROMOTE help in instantiating the promote function for different type combinations.
|
||||
* PROMOTE_ARGS macro handles function arguments correctly.
|
||||
*/
|
||||
|
||||
// Type ranking system
|
||||
template<typename T> struct type_rank;
|
||||
|
||||
#if defined(HAS_BOOL)
|
||||
template<> struct type_rank<bool> : std::integral_constant<int, 0> {};
|
||||
#endif
|
||||
|
||||
#if defined(HAS_INT8)
|
||||
template<> struct type_rank<int8_t> : std::integral_constant<int, 1> {};
|
||||
#endif
|
||||
|
||||
#if defined(HAS_UINT8)
|
||||
template<> struct type_rank<uint8_t> : std::integral_constant<int, 1> {};
|
||||
#endif
|
||||
|
||||
#if defined(HAS_INT16)
|
||||
template<> struct type_rank<int16_t> : std::integral_constant<int, 2> {};
|
||||
#endif
|
||||
|
||||
#if defined(HAS_UINT16)
|
||||
template<> struct type_rank<uint16_t> : std::integral_constant<int, 2> {};
|
||||
#endif
|
||||
|
||||
#if defined(HAS_INT32)
|
||||
template<> struct type_rank<int32_t> : std::integral_constant<int, 3> {};
|
||||
#endif
|
||||
|
||||
#if defined(HAS_UINT32)
|
||||
template<> struct type_rank<uint32_t> : std::integral_constant<int, 3> {};
|
||||
#endif
|
||||
|
||||
template<> struct type_rank<int64_t> : std::integral_constant<int, 4> {};
|
||||
template<> struct type_rank<long long int> : std::integral_constant<int, 4> {};
|
||||
template<> struct type_rank<uint64_t> : std::integral_constant<int, 4> {};
|
||||
|
||||
#if defined(HAS_FLOAT16)
|
||||
template<> struct type_rank<float16> : std::integral_constant<int, 5> {};
|
||||
#endif
|
||||
|
||||
#if defined(HAS_BFLOAT16)
|
||||
template<> struct type_rank<bfloat16> : std::integral_constant<int, 5> {};
|
||||
#endif
|
||||
|
||||
#if defined(HAS_FLOAT32)
|
||||
template<> struct type_rank<float> : std::integral_constant<int, 6> {};
|
||||
#endif
|
||||
|
||||
#if defined(HAS_DOUBLE)
|
||||
template<> struct type_rank<double> : std::integral_constant<int, 7> {};
|
||||
#endif
|
||||
|
||||
// promote_type trait
|
||||
template<typename T1, typename T2>
|
||||
struct promote_type {
|
||||
using type = typename std::conditional<
|
||||
(type_rank<T1>::value >= type_rank<T2>::value),
|
||||
T1,
|
||||
T2
|
||||
>::type;
|
||||
};
|
||||
|
||||
// promote function template
|
||||
template <typename Type1, typename Type2, typename ValueType>
|
||||
typename promote_type<Type1, Type2>::type promote(ValueType value) {
|
||||
return static_cast<typename promote_type<Type1, Type2>::type>(value);
|
||||
}
|
||||
|
||||
// promote_type3 trait for three types
|
||||
template<typename T1, typename T2, typename T3>
|
||||
struct promote_type3 {
|
||||
using type = typename promote_type<
|
||||
typename promote_type<T1, T2>::type,
|
||||
T3
|
||||
>::type;
|
||||
};
|
||||
|
||||
// Primary template for type_name - undefined to trigger a compile-time error for unsupported types
|
||||
template<typename T>
|
||||
struct type_name;
|
||||
|
||||
#if defined(HAS_BOOL)
|
||||
template<> struct type_name<bool> { static const char* get() { return "bool"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_INT8)
|
||||
template<> struct type_name<int8_t> { static const char* get() { return "int8_t"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_UINT8)
|
||||
template<> struct type_name<uint8_t> { static const char* get() { return "uint8_t"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_INT16)
|
||||
template<> struct type_name<int16_t> { static const char* get() { return "int16_t"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_UINT16)
|
||||
template<> struct type_name<uint16_t> { static const char* get() { return "uint16_t"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_INT32)
|
||||
template<> struct type_name<int32_t> { static const char* get() { return "int32_t"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_UINT32)
|
||||
template<> struct type_name<uint32_t> { static const char* get() { return "uint32_t"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_INT64)
|
||||
template<> struct type_name<int64_t> { static const char* get() { return "int64_t"; } };
|
||||
template<> struct type_name<long long int> { static const char* get() { return "long long int"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_UINT64)
|
||||
template<> struct type_name<uint64_t> { static const char* get() { return "uint64_t"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_FLOAT16)
|
||||
template<> struct type_name<float16> { static const char* get() { return "float16"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_BFLOAT16)
|
||||
template<> struct type_name<bfloat16> { static const char* get() { return "bfloat16"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_FLOAT32)
|
||||
template<> struct type_name<float> { static const char* get() { return "float"; } };
|
||||
#endif
|
||||
|
||||
#if defined(HAS_DOUBLE)
|
||||
template<> struct type_name<double> { static const char* get() { return "double"; } };
|
||||
#endif
|
||||
|
||||
// Helper function to get type name
|
||||
template<typename T>
|
||||
const char* get_type_name() {
|
||||
return type_name<T>::get();
|
||||
}
|
||||
|
||||
// Macro to instantiate the promote function
|
||||
#define INSTANTIATE_PROMOTE(a1, b1, FUNC_NAME, ARGS) \
|
||||
template promote_type<GET_SECOND(a1), GET_SECOND(b1)>::type \
|
||||
promote<GET_SECOND(a1), GET_SECOND(b1), GET_SECOND(a1)>(GET_SECOND(a1));
|
||||
|
||||
// Callback macro
|
||||
#define CALLBACK_INSTANTIATE_PROMOTE(a1, b1, FUNC_NAME, ARGS) \
|
||||
INSTANTIATE_PROMOTE(a1, b1, FUNC_NAME, ARGS)
|
||||
|
||||
// Macro to define functions with advanced type promotion and debugging
|
||||
#define SD_PROMOTE_FUNC(FUNC_NAME, BODY) \
|
||||
template<typename T, typename U = T, typename Z = T> \
|
||||
Z FUNC_NAME(T val1, U val2) { \
|
||||
using calc_type = typename promote_type3<T, U, Z>::type; \
|
||||
calc_type promoted_val1 = static_cast<calc_type>(val1); \
|
||||
calc_type promoted_val2 = static_cast<calc_type>(val2); \
|
||||
calc_type result = BODY; \
|
||||
return static_cast<Z>(result); \
|
||||
}
|
||||
|
||||
template <typename T, typename Z>
|
||||
SD_HOST_DEVICE SD_INLINE Z sd_abs(T value);
|
||||
|
||||
template <typename T, typename Z>
|
||||
SD_HOST_DEVICE SD_INLINE Z sd_eq(T value, T value2, double eps) {
|
||||
return sd_abs<T, Z>(value - value2) <= eps;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
SD_HOST_DEVICE SD_INLINE void sd_swap(T& val1, T& val2);
|
||||
|
||||
SD_PROMOTE_FUNC(sd_max, (promoted_val1 > promoted_val2 ? promoted_val1 : promoted_val2))
|
||||
|
||||
SD_PROMOTE_FUNC(sd_min, (promoted_val1 < promoted_val2 ? promoted_val1 : promoted_val2))
|
||||
|
||||
SD_PROMOTE_FUNC(sd_add, (promoted_val1 + promoted_val2))
|
||||
SD_PROMOTE_FUNC(sd_subtract, (promoted_val1 - promoted_val2))
|
||||
SD_PROMOTE_FUNC(sd_multiply, (promoted_val1 * promoted_val2))
|
||||
SD_PROMOTE_FUNC(sd_divide, (promoted_val1 / promoted_val2))
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
#### Reduced Binary Size
|
||||
The smaller artifact consumes less storage and reduces download times, making it suitable for environments with limited resources or slow network connections.
|
||||
|
||||
#### Faster Compilation and Build Times
|
||||
Limiting the supported types decreases the complexity of the codebase, leading to quicker compilation and faster build processes.
|
||||
|
||||
#### Simpler Maintenance
|
||||
A streamlined codebase with fewer type specializations simplifies maintenance, allowing for easier bug fixes and feature enhancements.
|
||||
|
||||
#### Specific Target Audience
|
||||
Tailoring the artifact to specific user needs ensures optimal performance and usability for those requiring only certain data types.
|
||||
|
||||
### Disadvantages
|
||||
|
||||
#### Fragmentation
|
||||
Offering multiple artifacts can lead to user confusion regarding which version to use, complicating documentation and support.
|
||||
|
||||
#### Increased Maintenance Overhead
|
||||
Maintaining separate artifacts requires additional effort to ensure consistency and compatibility across versions.
|
||||
|
||||
#### User Flexibility
|
||||
Users may need to switch to the larger artifact in the future if their requirements evolve, potentially leading to compatibility issues.
|
||||
|
||||
#### Inconsistent Performance
|
||||
Differences in optimization between artifacts may result in varying performance characteristics, causing confusion among users.
|
||||
|
||||
#### Dependency Management Complexity
|
||||
Managing dependencies for multiple artifacts increases the risk of conflicts and integration issues within user projects.
|
||||
|
||||
### Risks
|
||||
|
||||
#### Introduction of Bugs
|
||||
Refactoring to create a limited artifact may inadvertently introduce bugs, especially if the transition is not meticulously managed.
|
||||
|
||||
#### Performance Impacts
|
||||
If the limited artifact is not properly optimized, it could underperform compared to expectations.
|
||||
|
||||
#### Developer Confusion
|
||||
Developers accustomed to the multi-type system may find the limited artifact restrictive, leading to potential misuse or frustration.
|
||||
|
||||
#### Breaking Existing Code
|
||||
Users relying on the full type support may experience disruptions if they transition to the limited artifact without proper migration.
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
Publishing a smaller artifact with limited type support offers significant benefits
|
||||
in terms of reduced binary size, faster build times, and simplified maintenance.
|
||||
|
||||
|
||||
However, it also introduces challenges related to fragmentation, increased maintenance
|
||||
overhead, and potential user confusion. By carefully planning the implementation,
|
||||
maintaining clear documentation, and providing robust support, the advantages can be
|
||||
leveraged while mitigating the disadvantages.
|
||||
|
||||
This strategic approach ensures that the library remains flexible and user-friendly,
|
||||
catering to a broader range of use cases
|
||||
without compromising on performance or usability.
|
||||
@@ -0,0 +1,232 @@
|
||||
# ADR-0031: Handling Type Combinations and Template Instantiations with Macros
|
||||
|
||||
## Status
|
||||
|
||||
**Proposed**
|
||||
|
||||
Proposed by: Adam Gibson Oct 22, 2024
|
||||
|
||||
## Context
|
||||
|
||||
In the current C++ library published via Java Maven, multi-type arithmetic is supported through extensive use of templates and built using CMake. While this approach offers flexibility by accommodating various data types, it leads to an increased binary size. Many users do not require the full spectrum of type support, resulting in unnecessary storage consumption and longer download times. To optimize performance and reduce the binary footprint, there is a need to streamline type combinations and template instantiations.
|
||||
|
||||
## Decision
|
||||
|
||||
To efficiently manage type combinations and template instantiations within the limited artifact, a series of preprocessor macros are employed. These macros automate the retrieval and processing of type lists, enabling systematic instantiation of template functions based on various type combinations. Below is a detailed explanation of the key macros and their functionalities:
|
||||
|
||||
### GET Macro
|
||||
|
||||
```cpp
|
||||
#define GET(n, list) CAT(GET_, n) list
|
||||
|
||||
#define GET_0(t1, ...) t1
|
||||
#define GET_1(t1, t2, ...) t2
|
||||
#define GET_2(t1, t2, t3, ...) t3
|
||||
#define GET_3(t1, t2, t3, t4, ...) t4
|
||||
#define GET_4(t1, t2, t3, t4, t5, ...) t5
|
||||
#define GET_5(t1, t2, t3, t4, t5, t6, ...) t6
|
||||
#define GET_6(t1, t2, t3, t4, t5, t6, t7, ...) t7
|
||||
#define GET_7(t1, t2, t3, t4, t5, t6, t7, t8, ...) t8
|
||||
#define GET_8(t1, t2, t3, t4, t5, t6, t7, t8, t9, ...) t9
|
||||
#define GET_9(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, ...) t10
|
||||
#define GET_10(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, ...) t11
|
||||
#define GET_11(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, ...) t12
|
||||
#define GET_12(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, ...) t13
|
||||
#define GET_13(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, ...) t14
|
||||
#define GET_14(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, ...) t15
|
||||
#define GET_15(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, ...) t16
|
||||
Explanation:
|
||||
|
||||
The GET macro retrieves the nth element from a provided list by concatenating GET_ with the index n and applying it to the list.
|
||||
Each GET_n macro is defined to extract the nth element from a variadic list of parameters.
|
||||
GET_FIRST and GET_SECOND Macros
|
||||
```cpp
|
||||
#define GET_FIRST(tuple) GET_FIRST_IMPL tuple
|
||||
#define GET_FIRST_IMPL(a, b) a
|
||||
|
||||
#define GET_SECOND(tuple) GET_SECOND_IMPL tuple
|
||||
#define GET_SECOND_IMPL(a, b) b
|
||||
```
|
||||
Explanation:
|
||||
|
||||
GET_FIRST and GET_SECOND macros extract the first and second elements from a tuple, respectively.
|
||||
They expand the tuple and apply the corresponding implementation macros to retrieve the desired element.
|
||||
PROCESS_COMBINATION and CALLBACK_PROCESS_COMBINATION Macros
|
||||
```cpp
|
||||
#define PROCESS_COMBINATION(a1, b1, a2, b2, FUNC_NAME, ARGS) \
|
||||
std::cout << "(" << a1 << ", " << b1 << ", " << a2 << ", " << b2 << ")\n";
|
||||
|
||||
#define CALLBACK_PROCESS_COMBINATION(outer, inner, FUNC_NAME, ARGS) \
|
||||
PROCESS_COMBINATION(GET_FIRST(outer), GET_SECOND(outer), GET_FIRST(inner), GET_SECOND(inner), FUNC_NAME, ARGS)
|
||||
```
|
||||
|
||||
|
||||
Explanation:
|
||||
|
||||
PROCESS_COMBINATION defines how to process a combination of types. In this example, it simply prints the combination.
|
||||
CALLBACK_PROCESS_COMBINATION retrieves elements from the outer and inner lists and passes them to PROCESS_COMBINATION.
|
||||
INNER_LOOP Macros
|
||||
```cpp
|
||||
|
||||
#define INNER_LOOP_1(OUTER_ELEMENT, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
CALLBACK(OUTER_ELEMENT, GET(0, INNER_LIST), FUNC_NAME, ARGS)
|
||||
|
||||
#define INNER_LOOP_2(OUTER_ELEMENT, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
INNER_LOOP_1(OUTER_ELEMENT, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
CALLBACK(OUTER_ELEMENT, GET(1, INNER_LIST), FUNC_NAME, ARGS)
|
||||
|
||||
#define INNER_LOOP_3(OUTER_ELEMENT, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
INNER_LOOP_2(OUTER_ELEMENT, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
CALLBACK(OUTER_ELEMENT, GET(2, INNER_LIST), FUNC_NAME, ARGS)
|
||||
|
||||
#define INNER_LOOP_4(OUTER_ELEMENT, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
INNER_LOOP_3(OUTER_ELEMENT, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
CALLBACK(OUTER_ELEMENT, GET(3, INNER_LIST), FUNC_NAME, ARGS)
|
||||
|
||||
#define INNER_LOOP_5(OUTER_ELEMENT, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
INNER_LOOP_4(OUTER_ELEMENT, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
CALLBACK(OUTER_ELEMENT, GET(4, INNER_LIST), FUNC_NAME, ARGS)
|
||||
```
|
||||
|
||||
|
||||
// ... Similarly up to INNER_LOOP_16
|
||||
Explanation:
|
||||
|
||||
INNER_LOOP_n macros iterate over the INNER_LIST up to the nth element.
|
||||
Each iteration applies the CALLBACK macro to process the current combination.
|
||||
OUTER_LOOP Macros
|
||||
```cpp
|
||||
#define OUTER_LOOP_1(OUTER_LIST, INNER_LIST, INNER_SIZE, CALLBACK, FUNC_NAME, ARGS) \
|
||||
CAT(INNER_LOOP_, INNER_SIZE)(GET(0, OUTER_LIST), INNER_LIST, CALLBACK, FUNC_NAME, ARGS)
|
||||
|
||||
#define OUTER_LOOP_2(OUTER_LIST, INNER_LIST, INNER_SIZE, CALLBACK, FUNC_NAME, ARGS) \
|
||||
OUTER_LOOP_1(OUTER_LIST, INNER_LIST, INNER_SIZE, CALLBACK, FUNC_NAME, ARGS) \
|
||||
CAT(INNER_LOOP_, INNER_SIZE)(GET(1, OUTER_LIST), INNER_LIST, CALLBACK, FUNC_NAME, ARGS)
|
||||
|
||||
#define OUTER_LOOP_3(OUTER_LIST, INNER_LIST, INNER_SIZE, CALLBACK, FUNC_NAME, ARGS) \
|
||||
OUTER_LOOP_2(OUTER_LIST, INNER_LIST, INNER_SIZE, CALLBACK, FUNC_NAME, ARGS) \
|
||||
CAT(INNER_LOOP_, INNER_SIZE)(GET(2, OUTER_LIST), INNER_LIST, CALLBACK, FUNC_NAME, ARGS)
|
||||
```
|
||||
// ... Similarly up to OUTER_LOOP_16
|
||||
Explanation:
|
||||
|
||||
OUTER_LOOP_n macros iterate over the OUTER_LIST up to the nth element.
|
||||
For each element in the OUTER_LIST, they invoke the corresponding INNER_LOOP_n macro to process combinations with the INNER_LIST.
|
||||
ITERATE_COMBINATIONS Macro
|
||||
```cpp
|
||||
#define ITERATE_COMBINATIONS(OUTER_LIST, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
CAT(OUTER_LOOP_, PP_NARGS(EXPAND OUTER_LIST))(OUTER_LIST, INNER_LIST, PP_NARGS(EXPAND INNER_LIST), CALLBACK, FUNC_NAME, ARGS)
|
||||
```
|
||||
Explanation:
|
||||
|
||||
ITERATE_COMBINATIONS initiates the nested iteration over OUTER_LIST and INNER_LIST.
|
||||
It determines the number of elements in the OUTER_LIST and INNER_LIST using PP_NARGS (a macro to count arguments) and dispatches to the appropriate OUTER_LOOP_n macro.
|
||||
Template Instantiation Macros
|
||||
|
||||
```cpp
|
||||
|
||||
#define INSTANT_PROCESS_COMBINATION(a1, b1, FUNC_NAME, ARGS) \
|
||||
template void FUNC_NAME<GET_SECOND(a1), GET_SECOND(b1)>ARGS;
|
||||
|
||||
#define INSTANT_PROCESS_COMBINATION_3(a1, b1, c1, FUNC_NAME, ARGS) \
|
||||
template void FUNC_NAME<GET_SECOND(a1), GET_SECOND(b1), GET_SECOND(c1)>ARGS;
|
||||
|
||||
#define INSTANT_PROCESS_COMBINATION_CLASS(a1, b1, FUNC_NAME, ARGS) \
|
||||
template class FUNC_NAME<GET_SECOND(a1), GET_SECOND(b1)>ARGS;
|
||||
|
||||
#define INSTANT_PROCESS_COMBINATION_CLASS_3(a1, b1, c1, FUNC_NAME, ARGS) \
|
||||
extern template class FUNC_NAME<GET_SECOND(a1), GET_SECOND(b1), GET_SECOND(c1)>ARGS;
|
||||
```
|
||||
|
||||
Explanation:
|
||||
|
||||
These macros instantiate template functions with specific type combinations extracted from type lists.
|
||||
INSTANT_PROCESS_COMBINATION handles two-type combinations, while INSTANT_PROCESS_COMBINATION_3 handles three-type combinations.
|
||||
Similarly, class templates can be instantiated or declared extern using the corresponding macros.
|
||||
ITERATE_COMBINATIONS_3 Macro
|
||||
cpp
|
||||
Copy code
|
||||
#define ITERATE_COMBINATIONS_3(OUTER_LIST, MIDDLE_LIST, INNER_LIST, CALLBACK, FUNC_NAME, ARGS) \
|
||||
CAT(OUTER_LOOP_, CAT(PP_NARGS(EXPAND OUTER_LIST), _3))(OUTER_LIST, MIDDLE_LIST, INNER_LIST, PP_NARGS(EXPAND MIDDLE_LIST), PP_NARGS(EXPAND INNER_LIST), CALLBACK, FUNC_NAME, ARGS)
|
||||
Explanation:
|
||||
|
||||
ITERATE_COMBINATIONS_3 iterates over three type lists (SD_COMMON_TYPES) and applies the INSTANT_PROCESS_COMBINATION_3 macro to each combination.
|
||||
This results in the instantiation of the PairWiseTransform::exec function for each combination of types.
|
||||
Usage Example
|
||||
The following example demonstrates how the macros are used to instantiate template functions based on combinations of types:
|
||||
|
||||
```cpp
|
||||
/*
|
||||
*
|
||||
*
|
||||
ITERATE_COMBINATIONS_3: This macro iterates over three lists of data types (SD_COMMON_TYPES) and applies the INSTANT_PROCESS_COMBINATION_3 macro to each combination. This results in the instantiation of the PairWiseTransform::exec function for each combination of data types.
|
||||
ITERATE_COMBINATIONS: This macro iterates over two lists of data types (SD_COMMON_TYPES) and applies the CALLBACK_INSTANTIATE_PROMOTE macro to each combination. This is likely used for promoting data types.
|
||||
Function Instantiation:
|
||||
The PairWiseTransform::exec function is instantiated for various combinations of data types. The function signature includes parameters for operation number (opNum), input arrays (x, y), their shape information (xShapeInfo, yShapeInfo), output array (z), its shape information (zShapeInfo), extra parameters (extraParams), and the range of elements to process (start, stop).
|
||||
*/
|
||||
ITERATE_COMBINATIONS_3(
|
||||
(SD_COMMON_TYPES),
|
||||
(SD_COMMON_TYPES),
|
||||
(SD_COMMON_TYPES),
|
||||
INSTANT_PROCESS_COMBINATION_3,
|
||||
functions::pairwise_transforms::PairWiseTransform,
|
||||
::exec(int opNum, const void *x, const sd::LongType *xShapeInfo, const void *y,
|
||||
const sd::LongType *yShapeInfo, void *z, const sd::LongType *zShapeInfo,
|
||||
void *extraParams, sd::LongType start, sd::LongType stop)
|
||||
)
|
||||
|
||||
ITERATE_COMBINATIONS(
|
||||
(SD_COMMON_TYPES),
|
||||
(SD_COMMON_TYPES),
|
||||
CALLBACK_INSTANTIATE_PROMOTE,
|
||||
promote,
|
||||
;
|
||||
)
|
||||
```
|
||||
|
||||
Explanation:
|
||||
|
||||
ITERATE_COMBINATIONS_3 iterates over three type lists (SD_COMMON_TYPES) and instantiates the PairWiseTransform::exec function for each combination of types.
|
||||
ITERATE_COMBINATIONS iterates over two type lists and applies the CALLBACK_INSTANTIATE_PROMOTE macro to handle type promotion.
|
||||
Summary
|
||||
The macros defined above provide a systematic approach to handling multiple type combinations and template instantiations. By automating the retrieval and processing of type lists, the codebase ensures that all necessary template instances are generated without manual intervention. This not only reduces the potential for errors but also streamlines the maintenance and scalability of the library.
|
||||
|
||||
Benefits:
|
||||
|
||||
Automation: Reduces the need for repetitive code by automating template instantiations.
|
||||
Scalability: Easily handles a large number of type combinations without increasing code complexity.
|
||||
Maintainability: Simplifies updates and maintenance by centralizing type handling logic within macros.
|
||||
Considerations:
|
||||
|
||||
Complexity: Macros can be difficult to debug and understand, especially for those unfamiliar with advanced preprocessor techniques.
|
||||
Compilation Time: Extensive use of templates and macros may lead to longer compilation times.
|
||||
By incorporating these macros into the limited artifact, the library achieves a balance between flexibility and efficiency, ensuring that only necessary type combinations are included, thereby reducing binary size and optimizing performance.
|
||||
|
||||
# Consequences
|
||||
|
||||
## Advantages
|
||||
|
||||
### Flexibility in Compile-Time Code Generation
|
||||
- **Automated Template Instantiation:** The combination macros enable the automatic generation of multiple template instantiations during compile time. This reduces the need for repetitive manual code, ensuring that all necessary type combinations are systematically covered.
|
||||
- **Dynamic Type Support:** By leveraging macros, the library can easily support new data types without significant changes to the core codebase. Adding or modifying type combinations becomes a matter of updating macro definitions, enhancing the library's adaptability.
|
||||
- **Consistent Code Patterns:** Macros ensure that template instantiations follow a uniform pattern, minimizing discrepancies and maintaining consistency across different type combinations. This uniformity simplifies the integration of new functionalities and type supports.
|
||||
|
||||
## Disadvantages
|
||||
|
||||
### Increased Code Complexity
|
||||
- **Complex Macro Definitions:** The use of advanced preprocessor macros introduces a layer of complexity that can be challenging to understand and manage. Developers unfamiliar with intricate macro techniques may find the codebase harder to navigate and modify.
|
||||
- **Obfuscated Code Flow:** Macros can obscure the actual code being generated, making it difficult to trace the flow of template instantiations. This obscurity can complicate debugging efforts and hinder the comprehension of how different type combinations are handled.
|
||||
- **Maintenance Challenges:** Updating or extending macros to accommodate new type combinations requires careful adjustments to prevent introducing bugs. The intricate nature of macro-based code generation demands meticulous attention, increasing the maintenance overhead.
|
||||
- **Limited Tooling Support:** Many development tools and IDEs offer limited support for macro-heavy code, reducing the effectiveness of features like code completion, refactoring, and error highlighting. This limitation can slow down development and increase the likelihood of unnoticed issues.
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
Publishing a smaller artifact with limited type support offers significant benefits,
|
||||
including reduced binary size, faster build times, and simplified maintenance.
|
||||
However, it also brings challenges such as fragmentation, increased maintenance
|
||||
overhead, and potential user confusion. By carefully planning the implementation,
|
||||
maintaining clear documentation, and providing robust support, the advantages can
|
||||
be maximized while mitigating the disadvantages. This strategic approach ensures
|
||||
that the library remains flexible and user-friendly, catering to a broader range
|
||||
of use cases without compromising on performance or usability.
|
||||
@@ -0,0 +1,196 @@
|
||||
# C++ Print Debugging Utilities
|
||||
|
||||
## Status
|
||||
|
||||
Implemented
|
||||
|
||||
Proposed by: Adam Gibson (20-11-2024)
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
## Context
|
||||
|
||||
We need a way to provide debugging utilities for C++ code in the nd4j library. These utilities help in identifying issues such as out-of-bounds crashes and unexpected behavior in mathematical operations. The goal is to have a set of tools that can be easily enabled or disabled via configuration flags.
|
||||
|
||||
## Decision
|
||||
|
||||
We implement three distinct debugging utilities that can be controlled through configuration flags:
|
||||
|
||||
1. Print Indices - For tracking loop execution and array access
|
||||
2. Print Math - For debugging mathematical operations
|
||||
3. Preprocessor Output - For debugging macro behavior
|
||||
|
||||
Each utility serves a specific debugging purpose and can be independently enabled or disabled through both Maven and CMake configuration.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Maven Configuration (pom.xml)
|
||||
```xml
|
||||
<!-- Print Indices Configuration -->
|
||||
<libnd4j.printindices>OFF</libnd4j.printindices>
|
||||
|
||||
<!-- Print Math Configuration -->
|
||||
<libnd4j.printmath>OFF</libnd4j.printmath>
|
||||
|
||||
<!-- Preprocessor Configuration -->
|
||||
<libnd4j.preprocess>OFF</libnd4j.preprocess>
|
||||
```
|
||||
|
||||
### Build System Integration
|
||||
The utilities are integrated into the build system through a shell script that configures CMake. The script:
|
||||
|
||||
1. Echoes configuration status:
|
||||
```bash
|
||||
echo PRINT_INDICES = "$PRINT_INDICES"
|
||||
echo PRINT_MATH = "$PRINT_MATH"
|
||||
echo PREPROCESS = "$PREPROCESS"
|
||||
```
|
||||
|
||||
2. Passes configuration to CMake:
|
||||
```bash
|
||||
eval "$CMAKE_COMMAND" \
|
||||
-DPRINT_MATH="$PRINT_MATH" \
|
||||
-DPRINT_INDICES="$PRINT_INDICES" \
|
||||
# ... other CMake configurations
|
||||
```
|
||||
|
||||
3. Special handling for preprocessing:
|
||||
```bash
|
||||
if [ "$PREPROCESS" == "ON" ]; then
|
||||
# Re-run CMake with preprocessing enabled
|
||||
eval "$CMAKE_COMMAND" \
|
||||
-DPRINT_MATH="$PRINT_MATH" \
|
||||
-DPRINT_INDICES="$PRINT_INDICES" \
|
||||
-DSD_PREPROCESS="$PREPROCESS" \
|
||||
# ... other configurations
|
||||
echo "Running preprocessing step..."
|
||||
exit 0
|
||||
fi
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Print Indices Utility
|
||||
|
||||
#### Purpose
|
||||
Tracks loop iterations and array access patterns to help identify out-of-bounds issues and iteration-related bugs.
|
||||
|
||||
#### Build Configuration
|
||||
1. Set through Maven property `libnd4j.printindices`
|
||||
2. Passed to CMake as `PRINT_INDICES`
|
||||
3. Defines `PRINT_INDICES` macro in C++ code
|
||||
|
||||
#### Implementation
|
||||
```cpp
|
||||
#if defined(PRINT_INDICES)
|
||||
printf("i: %lld xEws %lld ReduceBoolFunction<X, Z>::execScalar\n", i, xEws);
|
||||
#endif
|
||||
```
|
||||
|
||||
#### Example Usage
|
||||
```cpp
|
||||
else {
|
||||
for (auto i = start; i < stop; i++) {
|
||||
#if defined(PRINT_INDICES)
|
||||
printf("i: %lld xEws %lld ReduceBoolFunction<X, Z>::execScalar\n", i, xEws);
|
||||
#endif
|
||||
intermediate[thread_id] = OpType::update(
|
||||
intermediate[thread_id],
|
||||
OpType::op(x[i * xEws], extraParams),
|
||||
extraParams
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Print Math Utility
|
||||
|
||||
#### Purpose
|
||||
Provides detailed tracking of mathematical operations, including input values, outputs, and execution flow.
|
||||
|
||||
#### Build Configuration
|
||||
1. Set through Maven property `libnd4j.printmath`
|
||||
2. Passed to CMake as `PRINT_MATH`
|
||||
3. Defines `SD_GCC_FUNCTRACE` macro in C++ code when enabled
|
||||
|
||||
#### Implementation
|
||||
```cpp
|
||||
template <>
|
||||
SD_INLINE SD_HOST void sd_print_math2<uint16_t>(char* func_name, uint16_t input1, uint16_t input2, uint16_t output) {
|
||||
#if defined(SD_GCC_FUNCTRACE)
|
||||
PRINT_IF_NECESSARY(func_name);
|
||||
#endif
|
||||
printf("%s: input1 = %d, input2 = %d, output = %d\n", func_name, input1, input2, output);
|
||||
fflush(stdout);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Preprocessor Output Utility
|
||||
|
||||
#### Purpose
|
||||
Generates preprocessed source files to help debug macro-related issues and understand macro expansion.
|
||||
|
||||
#### Build Configuration
|
||||
1. Set through Maven property `libnd4j.preprocess`
|
||||
2. Passed to CMake as `SD_PREPROCESS`
|
||||
3. Triggers special build flow in shell script when enabled
|
||||
|
||||
#### Build Process
|
||||
When preprocessing is enabled:
|
||||
1. Maven sets `libnd4j.preprocess=ON`
|
||||
2. Shell script detects `PREPROCESS=ON`
|
||||
3. CMake is run with `-DSD_PREPROCESS=ON`
|
||||
4. Build exits after preprocessing step
|
||||
5. Preprocessed files are generated in the build directory
|
||||
|
||||
#### Implementation
|
||||
```cmake
|
||||
if("${SD_PREPROCESS}" STREQUAL "ON")
|
||||
message("Preprocessing enabled ${CMAKE_BINARY_DIR}")
|
||||
include_directories(${CMAKE_BINARY_DIR}/../../include)
|
||||
list(REMOVE_DUPLICATES ALL_SOURCES)
|
||||
# Generate preprocessed files
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Print Indices
|
||||
#### Advantages
|
||||
- Easily identifies loop boundary issues
|
||||
- Helps debug array access patterns
|
||||
- Minimal code overhead
|
||||
|
||||
#### Drawbacks
|
||||
- Can generate large amounts of output
|
||||
- Performance impact when enabled
|
||||
|
||||
### Print Math
|
||||
#### Advantages
|
||||
- Detailed visibility into mathematical operations
|
||||
- Type-safe debugging
|
||||
- Stack trace integration
|
||||
|
||||
#### Drawbacks
|
||||
- Increased binary size
|
||||
- Runtime overhead when enabled
|
||||
- Additional complexity in template code
|
||||
|
||||
### Preprocessor Output
|
||||
#### Advantages
|
||||
- Clear visibility into macro expansion
|
||||
- Helps debug complex macro interactions
|
||||
- Useful for template debugging
|
||||
- Early exit prevents unnecessary compilation steps
|
||||
|
||||
#### Drawbacks
|
||||
- Increases build complexity
|
||||
- Additional disk space requirements
|
||||
- Requires separate build run for preprocessing
|
||||
- Cannot be combined with normal build flow
|
||||
|
||||
## References
|
||||
- platformmath.h
|
||||
- templatemath.h
|
||||
- CMakeLists.txt configuration
|
||||
- pom.xml configuration
|
||||
- build-dl4j-natives.sh script
|
||||
- ReduceBoolFunction implementation
|
||||
@@ -0,0 +1,170 @@
|
||||
# ADR 0033: Shape Buffer Trie Implementation
|
||||
|
||||
## Status
|
||||
|
||||
Implemented
|
||||
|
||||
Proposed by: Adam Gibson (19-12-2024)
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
## Context
|
||||
The libnd4j library requires efficient storage and lookup of shape
|
||||
information for neural network operations. Shape information is
|
||||
usually calculated multilple times per operation and can be expensive
|
||||
to maintain.
|
||||
One goal is to reduce the overhead by getting rid of ShapeDescriptors
|
||||
which were unnecessary extra allocations rather than just using only the shape
|
||||
buffers.
|
||||
This was all stored in a ShapeDescriptor-based cache with an unordered map,
|
||||
|
||||
The primary challenges include:
|
||||
|
||||
1. Frequent shape buffer allocations and deallocations during neural
|
||||
network operations
|
||||
2. Need for fast shape lookup during computation
|
||||
3. Memory management of redundant shape information
|
||||
4. Thread safety requirements for parallel execution
|
||||
5. Overhead from ShapeDescriptor creation for cache lookups
|
||||
6. Memory overhead from unordered map storage
|
||||
|
||||
## Decision
|
||||
We implement a shape buffer trie data structure (`DirectShapeTrie`) to manage and
|
||||
cache shape information, replacing the previous unordered map implementation.
|
||||
The trie structure is chosen for the following characteristics:
|
||||
|
||||
### Key Components
|
||||
- A trie node structure containing:
|
||||
- Shape buffer pointer
|
||||
- Child node pointers
|
||||
- Reference counting mechanism
|
||||
- [Striped thread safety](https://www.baeldung.com/java-lock-stripping) using an array of mutexes
|
||||
- Direct memory management of shape buffers
|
||||
- Sequential shape information exploitation similar to word tries
|
||||
|
||||
### Implementation Details
|
||||
1. The trie stores shape buffers based on their content as sequential paths
|
||||
2. Each unique shape path in the trie represents a unique shape configuration
|
||||
3. Reference counting is used to manage memory lifecycle
|
||||
4. Thread safety is ensured through striped mutex locks
|
||||
5. Direct memory allocation is used instead of standard containers
|
||||
6. Removed ShapeDescriptor creation requirement for lookups
|
||||
7. Shape values are stored sequentially in the trie, similar to characters in a word trie
|
||||
|
||||
### Visual Example
|
||||
```
|
||||
Root
|
||||
├── 2 (rank)
|
||||
│ ├── 3,4 (shape values)
|
||||
│ │ └── [ptr: shape_buffer_1]
|
||||
│ └── 5,6 (shape values)
|
||||
│ └── [ptr: shape_buffer_2]
|
||||
└── 3 (rank)
|
||||
├── 2,3,4 (shape values)
|
||||
│ │ └── [ptr: shape_buffer_3]
|
||||
└── 4,5,6 (shape values)
|
||||
└── [ptr: shape_buffer_4]
|
||||
```
|
||||
|
||||
In this example:
|
||||
- Each level represents a component of the shape
|
||||
- First level: rank of the array
|
||||
- Subsequent levels: actual shape values
|
||||
- Leaf nodes contain pointers to the actual shape buffers
|
||||
- Multiple shapes can share common prefixes, saving memory
|
||||
|
||||
## Thread Safety Implementation
|
||||
|
||||
The shape buffer cache implements striped locking using an array of mutexes:
|
||||
```cpp
|
||||
mutable std::array<MUTEX_TYPE, NUM_STRIPES> _mutexes;
|
||||
```
|
||||
|
||||
This design provides:
|
||||
1. Reduced contention through multiple lock stripes
|
||||
2. Better concurrency than a single global mutex
|
||||
3. Lower memory overhead than per-node locking
|
||||
4. Const-correctness through mutable mutex array
|
||||
|
||||
The striping mechanism:
|
||||
1. Distributes shapes across multiple mutexes based on their characteristics
|
||||
2. Allows concurrent operations on shapes in different stripes
|
||||
3. Balances between fine-grained locking and implementation complexity
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
1. Memory Efficiency:
|
||||
- Eliminates redundant shape buffer storage
|
||||
- Automatic cleanup of unused shapes through reference counting
|
||||
- Shared shape buffers across operations
|
||||
- Removal of ShapeDescriptor allocation overhead
|
||||
- Better memory locality due to trie structure
|
||||
|
||||
2. Performance:
|
||||
- O(n) lookup time where n is the shape length
|
||||
- Efficient shape comparison through pointer equality
|
||||
- Reduced memory allocation overhead
|
||||
- No ShapeDescriptor creation cost for lookups
|
||||
- Sequential access patterns for shape values
|
||||
|
||||
3. Thread Safety:
|
||||
- Safe concurrent access through striped mutex protection
|
||||
- Atomic reference counting operations
|
||||
- Protected shape buffer lifecycle management
|
||||
|
||||
4. Concurrency:
|
||||
- Striped locking enables parallel access to different shape regions
|
||||
- Better scaling under high concurrency than single mutex
|
||||
- Maintains simplicity compared to per-node locking
|
||||
|
||||
### Disadvantages
|
||||
1. Implementation Complexity:
|
||||
- Manual memory management requires careful implementation
|
||||
- Reference counting edge cases need careful handling
|
||||
- Thread synchronization adds complexity
|
||||
- More complex trie traversal logic
|
||||
|
||||
2. Memory Overhead:
|
||||
- Trie structure itself introduces memory overhead
|
||||
- Additional pointers for trie navigation
|
||||
|
||||
3. Performance Trade-offs:
|
||||
- Stripe selection adds minor overhead
|
||||
- Multiple shapes might still hash to same stripe
|
||||
- Reference counting operations add CPU overhead
|
||||
- Fixed number of stripes limits maximum parallelism
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Memory Management
|
||||
```cpp
|
||||
sd::LongType* createBuffer(int length);
|
||||
void deleteBuffer(sd::LongType* buffer);
|
||||
```
|
||||
|
||||
### API Design
|
||||
```cpp
|
||||
sd::LongType* lookupBuffer(const sd::LongType* shape, int length);
|
||||
void registerBuffer(const sd::LongType* shape, int length);
|
||||
void decrementRef(const sd::LongType* buffer);
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. Hash Table Implementation (Previous Approach):
|
||||
- Pros: Simpler implementation, O(1) average lookup
|
||||
- Cons: More memory usage, ShapeDescriptor overhead, potential hash collisions
|
||||
- Remov
|
||||
2. Alternative Locking Strategies:
|
||||
- Single Global Mutex:
|
||||
- Pros: Simplest implementation
|
||||
- Cons: High contention under load
|
||||
- Per-Node Locking:
|
||||
- Pros: Maximum concurrency
|
||||
- Cons: High memory overhead, complex synchronization
|
||||
- Lock-Free Design:
|
||||
- Pros: No lock contention
|
||||
- Cons: Extremely complex implementation, harder to verify correctness
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# ADR 0034: FlatBuffers Modernization
|
||||
|
||||
## Status
|
||||
|
||||
Implemented
|
||||
|
||||
Proposed by: Assistant (14-04-2025)
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
## Context
|
||||
|
||||
The libnd4j library uses FlatBuffers for serialization of neural network graphs and related data structures. The current implementation uses FlatBuffers 1.12.0 syntax and conventions, particularly for handling sequences and arrays. With the upgrade to newer versions of FlatBuffers, we need to modernize our schema definitions and code generation.
|
||||
|
||||
The primary challenges include:
|
||||
|
||||
1. Migration from legacy Sequence/SequenceItem patterns to modern vector types
|
||||
2. Proper generation of Java files with correct package structures
|
||||
3. Integration with build system for consistent compilation
|
||||
4. Maintaining backward compatibility where possible
|
||||
5. Ensuring proper environment variable passing for code generation
|
||||
6. Proper schema namespace management
|
||||
|
||||
## Decision
|
||||
|
||||
We implement a modernized FlatBuffers integration that uses current vector syntax and build processes. This includes:
|
||||
|
||||
### Key Components
|
||||
- Schema files using modern vector syntax (`[Type]` instead of Sequence)
|
||||
- Direct build process integration in CMake
|
||||
- Explicit environment variable handling for flatc compiler
|
||||
- Namespace standardization across schema files
|
||||
|
||||
### Implementation Details
|
||||
1. Schemas use vector syntax for array types (e.g., `[FlatArray]` instead of `Sequence<FlatArray>`)
|
||||
2. CMake executes flatc compilation directly instead of using custom targets
|
||||
3. Build process ensures flatc is compiled before schema generation
|
||||
4. Java package structure matches expected nd4j-api layout
|
||||
5. Schema namespaces standardized to `graph` without `sd` prefix
|
||||
|
||||
### Schema Example
|
||||
```fbs
|
||||
namespace graph;
|
||||
|
||||
table SequenceItem {
|
||||
name:string;
|
||||
associated_variable:[FlatArray];
|
||||
}
|
||||
|
||||
table SequenceItemRoot {
|
||||
sequence_items:[SequenceItem];
|
||||
}
|
||||
|
||||
root_type SequenceItemRoot;
|
||||
```
|
||||
|
||||
### Build Integration
|
||||
```cmake
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E env "FLATC_PATH=${FLATC_EXECUTABLE}"
|
||||
bash ${CMAKE_CURRENT_SOURCE_DIR}/flatc-generate.sh
|
||||
RESULT_VARIABLE FLATC_RESULT
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
1. Modern FlatBuffers Integration:
|
||||
- Cleaner schema definitions
|
||||
- Better type safety through vector syntax
|
||||
- More maintainable code generation
|
||||
- Consistent with current FlatBuffers best practices
|
||||
|
||||
2. Build System Integration:
|
||||
- Reliable flatc compilation
|
||||
- Proper environment variable handling
|
||||
- Direct process execution instead of custom targets
|
||||
- Better error handling and reporting
|
||||
|
||||
3. Code Organization:
|
||||
- Correct Java package structure
|
||||
- Standardized namespaces
|
||||
- Clear separation of generated code
|
||||
- Better integration with existing nd4j structure
|
||||
|
||||
### Disadvantages
|
||||
1. Implementation Requirements:
|
||||
- Need to update existing schema files
|
||||
- Must ensure build process compatibility
|
||||
- Potential for temporary build issues during transition
|
||||
|
||||
2. Migration Effort:
|
||||
- Updates needed for existing code using legacy patterns
|
||||
- Need to verify all schema files
|
||||
- Testing required for all serialization paths
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Schema Location
|
||||
```
|
||||
/libnd4j/include/graph/scheme/*.fbs
|
||||
```
|
||||
|
||||
### Generated Code Location
|
||||
```
|
||||
/nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/java/org/nd4j/graph/
|
||||
```
|
||||
|
||||
### Build Process
|
||||
1. CMake configures build
|
||||
2. flatc compiler is built
|
||||
3. Schema generation script runs
|
||||
4. Java files are copied to appropriate location
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. Custom Target Approach:
|
||||
- Pros: More traditional CMake integration
|
||||
- Cons: Less direct control over process, harder to debug
|
||||
|
||||
2. Manual File Generation:
|
||||
- Pros: Simpler build process
|
||||
- Cons: Error-prone, harder to maintain
|
||||
|
||||
3. Separate Build Step:
|
||||
- Pros: Cleaner separation of concerns
|
||||
- Cons: More complex build process, potential for synchronization issues
|
||||
@@ -0,0 +1,325 @@
|
||||
# ADR 0035: SameDiff Unified Container Format
|
||||
|
||||
## Status
|
||||
|
||||
Implemented
|
||||
|
||||
Proposed by: Adam Gibson (15-04-2025)
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
The current SameDiff serialization relies on FlatBuffers for graph representation and handles large arrays (>2GB) using a chunking mechanism. However, this approach has several limitations:
|
||||
|
||||
1. **Single File Deployment**: Current format often requires multiple files when externalizing large arrays
|
||||
2. **Large Model Support**: Limited efficiency when dealing with very large models
|
||||
3. **Metadata Management**: Lack of standardized metadata for model tracking and versioning
|
||||
4. **Model Sharding**: Limited explicit support for sharding large models
|
||||
5. **Compatibility**: Each format change risks breaking backward compatibility
|
||||
|
||||
We need a more robust serialization format that addresses these challenges while maintaining compatibility with existing systems.
|
||||
|
||||
## Decision
|
||||
|
||||
We have implemented a unified container format for SameDiff that encapsulates both graph structure and arrays in a single file, with support for optional externalization and sharding when needed. This format maintains full backward compatibility with the original serialization approach.
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **Multi-Format Support**:
|
||||
- SDNB Format: Single-file internal format (.sdnb)
|
||||
- SDZ Format: ZIP-based container format (.sdz)
|
||||
- Sharded formats for both SDNB and SDZ
|
||||
|
||||
2. **SDNB Format**:
|
||||
- Section-based container with header, metadata, graph, and arrays
|
||||
- Efficient memory mapping for large arrays
|
||||
- Optimized for performance with direct I/O
|
||||
- Compatible with 32-bit FlatBuffers limitations
|
||||
|
||||
3. **SDZ Format**:
|
||||
- Standard ZIP archive containing internal .sdnb files
|
||||
- Compressed storage to reduce file size
|
||||
- Standard tools compatibility for inspection and extraction
|
||||
- Single file deployment for complex models
|
||||
- Simplicity of implementation using standard ZIP libraries
|
||||
|
||||
4. **Metadata Management**:
|
||||
- Standardized keys for common model attributes
|
||||
- Support for custom metadata
|
||||
- Versioning and provenance information
|
||||
- Extensible metadata system similar to GGUF (General GPU Unified Format)
|
||||
- Ability to add metadata later without reserializing model parameters
|
||||
|
||||
5. **Sharding Support**:
|
||||
- Explicit first-class support for model sharding in both formats
|
||||
- Smart distribution of variables across shards
|
||||
- Automatic shard count determination based on model size
|
||||
- Consistent naming convention for shards
|
||||
- Support for NDArrays of any size through intelligent sharding
|
||||
|
||||
6. **Backward Compatibility**:
|
||||
- Automatic format detection between SDNB and SDZ formats
|
||||
- Support for loading both internal and externalized original formats
|
||||
- Legacy model conversion utilities
|
||||
|
||||
### Implementation Details
|
||||
|
||||
1. **SDNB Format Structure**:
|
||||
```
|
||||
MAGIC_BYTES (4 bytes: "SDNB")
|
||||
VERSION (4 bytes)
|
||||
MANIFEST_OFFSET (8 bytes)
|
||||
MANIFEST_LENGTH (8 bytes)
|
||||
METADATA_OFFSET (8 bytes)
|
||||
[FLATBUFFER_GRAPH_DATA]
|
||||
[APPENDED_ARRAYS_DATA]
|
||||
[SERIALIZED_MANIFEST]
|
||||
```
|
||||
|
||||
2. **SDZ Format Structure**:
|
||||
```
|
||||
ZIP_HEADER
|
||||
[ENTRY: model.sdnb] # Graph structure shard
|
||||
[ENTRY: model.shard0-of-N.sdnb] # Alternative naming for graph shard
|
||||
[ENTRY: model.shard1-of-N.sdnb] # Variable shard 1
|
||||
[ENTRY: model.shard2-of-N.sdnb] # Variable shard 2
|
||||
...
|
||||
[ENTRY: model.shardM-of-N.sdnb] # Variable shard M
|
||||
ZIP_DIRECTORY
|
||||
ZIP_END
|
||||
```
|
||||
|
||||
3. **Sharding Strategy**:
|
||||
- Graph structure in shard 0
|
||||
- Variables distributed across remaining shards
|
||||
- Dynamic shard count calculation based on variable sizes
|
||||
- Maximum shard size limit of 1GB per shard
|
||||
- Smart variable grouping to minimize cross-shard dependencies
|
||||
|
||||
4. **API Design**:
|
||||
```java
|
||||
// SDNB Format API
|
||||
SameDiffSerializer.save(sameDiff, file, saveUpdaterState, metadata);
|
||||
SameDiffSerializer.saveAutoShard(sameDiff, baseFile, saveUpdaterState, metadata);
|
||||
SameDiffSerializer.saveSharded(sameDiff, baseFile, saveUpdaterState, estimatedShards, metadata);
|
||||
SameDiff model = SameDiffSerializer.load(file, loadUpdaterState);
|
||||
SameDiff model = SameDiffSerializer.loadSharded(baseFile, loadUpdaterState);
|
||||
|
||||
// SDZ Format API
|
||||
SDZSerializer.save(sameDiff, outputZipFile, saveUpdaterState, metadata);
|
||||
SameDiff model = SDZSerializer.load(modelZipFile, loadUpdaterState);
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### SDZ Format Details
|
||||
|
||||
The SDZ format addresses the need for single-file distribution of large models through the following implementation:
|
||||
|
||||
1. **ZIP Container**: The SDZ format uses a standard ZIP archive as its container, enabling compatibility with standard zip tools for inspection and extraction.
|
||||
|
||||
2. **Internal Structure**:
|
||||
- The ZIP archive contains one or more SDNB format files
|
||||
- The first file (shard0) contains the graph structure
|
||||
- Subsequent files contain variables distributed across shards
|
||||
- Consistent naming convention ensures proper loading sequence
|
||||
|
||||
3. **Sharding Implementation**:
|
||||
- `SDZSerializer.save()` internally calls `SameDiffSerializer.saveAutoShard()` to create SDNB files
|
||||
- These files are then compressed and packaged into the ZIP archive
|
||||
- Automatic cleanup of temporary files after ZIP creation
|
||||
- Distributed variable serialization across shards based on size
|
||||
|
||||
4. **Loading Process*``*:
|
||||
- `SDZSerializer.load()` extracts all SDNB files to a temporary directory
|
||||
- Loads shard 0 first to establish graph structure
|
||||
- Loads variable data from remaining shards
|
||||
- Ensures temporary directory cleanup
|
||||
- Returns fully reconstituted SameDiff instance
|
||||
|
||||
5. **ZIP Operations**:
|
||||
- Uses standard Java ZIP APIs for maximum compatibility
|
||||
- Implements efficient I/O with buffering for large file handling
|
||||
- Security measures against zip slip vulnerabilities
|
||||
- Validation of ZIP structure integrity
|
||||
|
||||
6. **Optimizations**:
|
||||
- Manifest-based array lookup for efficient loading
|
||||
- Smart buffer management to minimize memory pressure
|
||||
- Native byte order handling for cross-platform compatibility
|
||||
- Verification steps to validate loaded model integrity
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
The SDZ format balances compression benefits against performance requirements:
|
||||
|
||||
1. **Serialization Performance**:
|
||||
- Slight additional overhead for ZIP compression
|
||||
- Parallelized compression when possible
|
||||
- Progressive ZIP writing to avoid memory spikes
|
||||
|
||||
2. **Deserialization Performance**:
|
||||
- Sequential extraction for predictable memory usage
|
||||
- Lazy loading strategies for large variables
|
||||
- Efficient memory mapping for large arrays when possible
|
||||
- Verification during loading to ensure data integrity
|
||||
|
||||
3. **Storage Efficiency**:
|
||||
- Typically 30-50% size reduction through compression
|
||||
- Optimal balance between compression level and performance
|
||||
- Compression ratio varies based on parameter data patterns
|
||||
|
||||
## Trade-offs and Consequences
|
||||
|
||||
### Design Trade-offs
|
||||
|
||||
1. **FlatBuffers Compatibility vs. Unlimited Model Size**:
|
||||
- We maintain compatibility with 32-bit FlatBuffers for graph structure
|
||||
- We overcome FlatBuffers' 2GB size limitation through our sharding approach
|
||||
- This allows us to leverage FlatBuffers' efficiency for small graph structures while supporting NDArrays of any size
|
||||
|
||||
2. **Single File Format vs. Performance**:
|
||||
- We chose ZIP for its ubiquity, tooling support, and single-file deployment benefits
|
||||
- ZIP allows self-contained distribution while accepting some performance overhead during compression/decompression
|
||||
- This trades some loading speed for better deployment experience and reduced operational complexity
|
||||
|
||||
3. **Metadata Extensibility vs. Format Complexity**:
|
||||
- We implement an extensible metadata system similar to GGUF
|
||||
- This allows adding/updating metadata without reserializing the entire model
|
||||
- The increased format complexity is justified by the flexibility to evolve models over time
|
||||
|
||||
4. **Cross-Platform Support vs. Optimization**:
|
||||
- We prioritize cross-platform compatibility over platform-specific optimizations
|
||||
- This ensures models can be shared across environments but may not achieve maximum performance on specialized hardware
|
||||
|
||||
### Advantages
|
||||
|
||||
1. **Simplified Deployment**:
|
||||
- Single file deployment with SDZ format
|
||||
- Easier distribution and management
|
||||
- Reduced risk of missing files or shard mismatches
|
||||
|
||||
2. **Enhanced Model Storage**:
|
||||
- Support for NDArrays and models of any size
|
||||
- Efficient storage with ZIP compression
|
||||
- Selective loading of model components
|
||||
|
||||
3. **Better Metadata Management**:
|
||||
- Standardized tracking of model attributes
|
||||
- Version management for compatibility
|
||||
- Custom metadata for specific requirements
|
||||
- Post-training metadata additions without parameter reserializing
|
||||
|
||||
4. **First-Class Sharding**:
|
||||
- Explicit support for very large models
|
||||
- Intelligent variable distribution
|
||||
- Efficient loading of sharded models
|
||||
|
||||
5. **Complete Backward Compatibility**:
|
||||
- Seamless support for reading existing formats
|
||||
- Automatic format detection and handling
|
||||
- No disruption to existing workflows
|
||||
- Migration path for older models
|
||||
|
||||
### Disadvantages
|
||||
|
||||
1. **Implementation Complexity**:
|
||||
- More complex than previous FlatBuffers-only approach
|
||||
- Additional code paths for format handling
|
||||
- Need for comprehensive testing across formats
|
||||
|
||||
2. **Performance Considerations**:
|
||||
- Compression/decompression time with SDZ format
|
||||
- Temporary storage requirements during extraction
|
||||
- Slight overhead for small models
|
||||
|
||||
3. **Tool Ecosystem**:
|
||||
- Need for updates to existing tooling
|
||||
- Additional format documentation requirements
|
||||
- Migration guidance for existing models
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Format Detection Algorithm
|
||||
```java
|
||||
public static SameDiff load(File file, boolean loadUpdaterState) throws IOException {
|
||||
// Check if it's a ZIP file first (SDZ format)
|
||||
if (isZipFile(file)) {
|
||||
return SDZSerializer.load(file, loadUpdaterState);
|
||||
}
|
||||
|
||||
// Not a ZIP, check if it's a native SDNB file
|
||||
if (isValidSdnbFile(file)) {
|
||||
return SameDiffSerializer.load(file, loadUpdaterState);
|
||||
}
|
||||
|
||||
// Check if it's a base name for sharded files
|
||||
if (hasShardedFiles(file)) {
|
||||
return SameDiffSerializer.loadSharded(file, loadUpdaterState);
|
||||
}
|
||||
|
||||
// Unsupported format
|
||||
throw new UnsupportedOperationException("Unrecognized model format");
|
||||
}
|
||||
```
|
||||
|
||||
### SDZ Implementation
|
||||
```java
|
||||
public static void save(SameDiff sameDiff, File outputZipFile, boolean saveUpdaterState,
|
||||
Map<String, String> metadata) throws IOException {
|
||||
// Create temporary directory for SDNB files
|
||||
Path tempDir = Files.createTempDirectory("sdz-serializer-save-");
|
||||
|
||||
try {
|
||||
// Save using SDNB serializer to temporary directory
|
||||
File internalSavePath = new File(tempDir.toFile(), "model");
|
||||
SameDiffSerializer.saveAutoShard(sameDiff, internalSavePath, saveUpdaterState, metadata);
|
||||
|
||||
// Collect all files to add to ZIP
|
||||
List<File> filesToZip = new ArrayList<>();
|
||||
findAllFilesRecursively(tempDir.toFile(), filesToZip);
|
||||
|
||||
// Create ZIP archive
|
||||
createZipArchive(outputZipFile, filesToZip);
|
||||
} finally {
|
||||
// Clean up temporary directory
|
||||
FileUtils.deleteDirectory(tempDir.toFile());
|
||||
}
|
||||
}
|
||||
|
||||
public static SameDiff load(File modelZipFile, boolean loadUpdaterState) throws IOException {
|
||||
// Extract ZIP to temporary directory
|
||||
Path tempDir = Files.createTempDirectory("sdz-serializer-load-");
|
||||
|
||||
try {
|
||||
// Extract ZIP contents
|
||||
extractZip(modelZipFile, tempDir.toFile());
|
||||
|
||||
// Determine the path to load from
|
||||
File loadPath = determineLoadPath(tempDir.toFile());
|
||||
|
||||
// Load using SDNB serializer
|
||||
return SameDiffSerializer.load(loadPath, loadUpdaterState);
|
||||
} finally {
|
||||
// Clean up temporary directory
|
||||
FileUtils.deleteDirectory(tempDir.toFile());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Migration Guidelines
|
||||
|
||||
For existing users:
|
||||
|
||||
1. **Loading Existing Models**:
|
||||
- No changes needed, automatic format detection handles existing models
|
||||
|
||||
2. **Converting to SDZ Format**:
|
||||
- Use `SDZSerializer.save()` with existing SameDiff instances
|
||||
- Alternatively, load existing models and save in SDZ format
|
||||
|
||||
3. **When to Use Each Format**:
|
||||
- SDNB: For highest performance, particularly during training
|
||||
- SDZ: For deployment, storage efficiency, and single-file distribution
|
||||
- Sharded formats: For very large models exceeding memory limits
|
||||
@@ -0,0 +1,262 @@
|
||||
# ADR: Migrate Project Namespaces to org.eclipse.deeplearning4j using OpenRewrite
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
Proposed by: Adam Gibson (May 8, 2025)
|
||||
|
||||
Discussed with: Paul Dubs
|
||||
|
||||
## Context
|
||||
|
||||
The Deeplearning4j project ecosystem currently utilizes multiple root Java package namespaces, primarily `org.nd4j`, `org.deeplearning4j`, and `org.datavec`, along with existing code under `org.eclipse.deeplearning4j`, bindings to
|
||||
external libraries (like FlatBuffers, ONNX, TensorFlow, Python), and various `contrib` and `codegen` modules. This distributed namespace structure can make navigation, maintenance, and
|
||||
understanding component relationships more challenging than necessary.
|
||||
|
||||
The goal is to consolidate the core project codebase under a single, unified root namespace: `org.eclipse.deeplearning4j`. This aligns with the project's stewardship under the Eclipse Foundation and aims to create a clearer,
|
||||
more consistent, and maintainable structure. Given the project's large scale, an automated refactoring approach using [OpenRewrite](https://docs.openrewrite.org/reference/rewrite-maven-plugin) is optimal for feasibility. This ADR proposes the strategy and
|
||||
specific rules for this migration.
|
||||
|
||||
This is part of a 2 phase release plan where a milestone release that has the old package names is performed followed by the major renamespacing.
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
This ADR proposes using a comprehensive [OpenRewrite recipe](https://docs.openrewrite.org/reference/rewrite-maven-plugin) to automatically refactor the project's primary Java packages into the `org.eclipse.deeplearning4j` namespace. The refactoring follows a two-phase conceptual approach:
|
||||
|
||||
1. **Foundational Re-basing:** Map the main existing root packages (`org.nd4j`, `org.deeplearning4j`, `org.datavec`) to logical sub-packages under the new root (`.nd4j`, `.dl4jcore`, `.datavec`).
|
||||
2. **Architectural Refinement:** Apply more specific rules to achieve a clearer target structure, including:
|
||||
* Elevating the UI components to a top-level `org.eclipse.deeplearning4j.ui` package.
|
||||
* Consolidating ND4J backend-specific code (including former `jita` and `jcublas` into `.cuda`) under `org.eclipse.deeplearning4j.nd4j.backend.[type]`.
|
||||
* Structuring DataVec components functionally under `org.eclipse.deeplearning4j.datavec.*`.
|
||||
* Isolating runtime bindings for external libraries under `org.eclipse.deeplearning4j.bindings.*`.
|
||||
* Providing clear namespaces for `contrib` and `codegen` modules.
|
||||
* Establishing top-level `common` and `resources` packages (though full consolidation requires further steps).
|
||||
|
||||
The core of the proposal is the following OpenRewrite YAML recipe, which implements the automated parts of this strategy:
|
||||
|
||||
|
||||
|
||||
|
||||
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
|
||||
# OpenRewrite Recipe for Deeplearning4j Namespace Migration #
|
||||
# Target Root: org.eclipse.deeplearning4j #
|
||||
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
|
||||
type: org.openrewrite.Recipe
|
||||
name: org.eclipse.deeplearning4j.refactor.NamespaceMigration
|
||||
displayName: Migrate DL4J Project to org.eclipse.deeplearning4j Namespace
|
||||
description: Comprehensive refactoring of core DL4J, ND4J, and DataVec packages under the org.eclipse.deeplearning4j root, including backend consolidation and UI elevation.
|
||||
|
||||
recipeList:
|
||||
#--------------------------------------------------------------------------#
|
||||
# Step 1: Handle Specific Component Moves (More Specific Rules First) #
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
# --- UI Components -> o.e.d.ui.* ---
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.deeplearning4j.ui
|
||||
newPackageName: org.eclipse.deeplearning4j.ui
|
||||
recursive: true
|
||||
# Note: This rule assumes org.deeplearning4j.ui.* should be elevated.
|
||||
# It must run before the general org.deeplearning4j -> o.e.d.dl4jcore rule.
|
||||
|
||||
# --- ND4J Backends -> o.e.d.nd4j.backend.[type].* ---
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.linalg.jcublas # Maps to cuda backend
|
||||
newPackageName: org.eclipse.deeplearning4j.nd4j.backend.cuda
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.jita # JITA components also map to cuda backend
|
||||
newPackageName: org.eclipse.deeplearning4j.nd4j.backend.cuda # Target same new package
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.linalg.cpu.nativecpu # Maps to cpu backend
|
||||
newPackageName: org.eclipse.deeplearning4j.nd4j.backend.cpu
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.presets.cuda # CUDA presets
|
||||
newPackageName: org.eclipse.deeplearning4j.nd4j.backend.cuda.presets
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.presets.cpu # CPU presets
|
||||
newPackageName: org.eclipse.deeplearning4j.nd4j.backend.cpu.presets
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage: # Minimal backend?
|
||||
oldPackageName: org.nd4j.linalg.minimal
|
||||
newPackageName: org.eclipse.deeplearning4j.nd4j.backend.minimal
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.presets.minimal
|
||||
newPackageName: org.eclipse.deeplearning4j.nd4j.backend.minimal.presets
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage: # Common presets utils
|
||||
oldPackageName: org.nd4j.presets
|
||||
newPackageName: org.eclipse.deeplearning4j.nd4j.backend.common.presets
|
||||
recursive: true
|
||||
|
||||
# --- Native Ops / Bindings Facades ---
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.nativeblas # Common native facade classes like NativeOpsHolder
|
||||
newPackageName: org.eclipse.deeplearning4j.nd4j.nativeops.common
|
||||
recursive: true
|
||||
|
||||
# --- Runtime Bindings (Wrappers/Runners for external libs/runtimes) ---
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.onnxruntime # ONNX Runtime Runner
|
||||
newPackageName: org.eclipse.deeplearning4j.bindings.onnx.runtime
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.tensorflow.conversion # TF Runtime Runner/Converter
|
||||
newPackageName: org.eclipse.deeplearning4j.bindings.tensorflow.runtime
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.tensorflowlite # TF Lite Runner
|
||||
newPackageName: org.eclipse.deeplearning4j.bindings.tensorflowlite.runtime
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.tvm # TVM Runner
|
||||
newPackageName: org.eclipse.deeplearning4j.bindings.tvm.runtime
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.python4j # Python Bindings
|
||||
newPackageName: org.eclipse.deeplearning4j.bindings.python.api # Map core to .api
|
||||
recursive: true # Handles subpackages like .numpy unless more specific rules added
|
||||
|
||||
# --- Specific Contrib Modules (Using explicit contrib namespacing) ---
|
||||
# Note: fileMatcher might be needed in practice to limit these rules to specific module paths
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j # For classes directly in org.nd4j within nd4j-benchmark
|
||||
newPackageName: org.eclipse.deeplearning4j.contrib.benchmark.nd4j
|
||||
recursive: false # Avoid affecting subpackages handled below
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.bypass # Sub-package in nd4j-benchmark
|
||||
newPackageName: org.eclipse.deeplearning4j.contrib.benchmark.nd4j.bypass
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.memorypressure # Sub-package in nd4j-benchmark
|
||||
newPackageName: org.eclipse.deeplearning4j.contrib.benchmark.nd4j.memorypressure
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j # For classes directly in org.nd4j within version-updater
|
||||
newPackageName: org.eclipse.deeplearning4j.contrib.versionupdater
|
||||
recursive: false
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j.fileupdater # Sub-package in version-updater
|
||||
newPackageName: org.eclipse.deeplearning4j.contrib.versionupdater.fileupdater
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage: # For contrib blas-lapack-gen
|
||||
oldPackageName: org.deeplearning4j
|
||||
newPackageName: org.eclipse.deeplearning4j.contrib.blaslapackgen
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage: # For contrib nd4j-log-analyzer
|
||||
oldPackageName: org.nd4j.interceptor
|
||||
newPackageName: org.eclipse.deeplearning4j.contrib.nd4jloganalyzer.interceptor
|
||||
recursive: true
|
||||
|
||||
# --- Specific Codegen Modules ---
|
||||
# Note: fileMatcher might be needed in practice
|
||||
- org.openrewrite.java.ChangePackage: # For libnd4j-gen
|
||||
oldPackageName: org.nd4j.descriptor
|
||||
newPackageName: org.eclipse.deeplearning4j.codegen.descriptor
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage: # For op-codegen
|
||||
oldPackageName: org.nd4j.codegen
|
||||
newPackageName: org.eclipse.deeplearning4j.codegen.op
|
||||
recursive: true
|
||||
- org.openrewrite.java.ChangePackage: # For codegen blas-lapack-generator
|
||||
oldPackageName: org.deeplearning4j
|
||||
newPackageName: org.eclipse.deeplearning4j.codegen.blaslapack
|
||||
recursive: true
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Step 2: Apply General Re-basing Rules (Less Specific Rules Last) #
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.nd4j # General ND4J code not caught by specific rules above
|
||||
newPackageName: org.eclipse.deeplearning4j.nd4j
|
||||
recursive: true
|
||||
# This will map remaining org.nd4j.* like linalg.api.*, samediff.*, common.*, etc.
|
||||
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.deeplearning4j # General DL4J Core code (excluding UI already handled)
|
||||
newPackageName: org.eclipse.deeplearning4j.dl4jcore
|
||||
recursive: true
|
||||
# This will map nn.*, models.*, datasets.*, eval.*, nlp.*, etc.
|
||||
|
||||
- org.openrewrite.java.ChangePackage:
|
||||
oldPackageName: org.datavec # General DataVec code
|
||||
newPackageName: org.eclipse.deeplearning4j.datavec
|
||||
recursive: true
|
||||
# This will map api.*, transform.*, image.*, arrow.*, etc.
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Step 3: Exclusions / Manual Handling (Commented Out for Safety) #
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
# --- DO NOT AUTOMATICALLY RUN THESE for generated binding specs ---
|
||||
# --- Prefer changing generator configurations ---
|
||||
# - org.openrewrite.java.ChangePackage:
|
||||
# oldPackageName: com.google.flatbuffers
|
||||
# newPackageName: org.eclipse.deeplearning4j.bindings.flatbuffers.spec # Or .core
|
||||
# recursive: true
|
||||
# - org.openrewrite.java.ChangePackage:
|
||||
# oldPackageName: onnx
|
||||
# newPackageName: org.eclipse.deeplearning4j.bindings.onnx.spec
|
||||
# recursive: true
|
||||
# - org.openrewrite.java.ChangePackage:
|
||||
# oldPackageName: tensorflow # Includes tensorflow.framework, tensorflow.eager etc
|
||||
# newPackageName: org.eclipse.deeplearning4j.bindings.tensorflow.spec
|
||||
# recursive: true
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Step 4: Potential Cleanup (Run Separately After Migration) #
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
# - org.openrewrite.java.cleanup.OrganizeImports
|
||||
# - org.openrewrite.java.cleanup.RemoveUnusedImports
|
||||
# - org.openrewrite.java.format.AutoFormat
|
||||
|
||||
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
|
||||
# End of Recipe #
|
||||
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
### Advantages
|
||||
|
||||
* **Namespace Consistency:** Provides a single, unified root namespace (`org.eclipse.deeplearning4j`) for the entire project, aligning with Eclipse Foundation stewardship.
|
||||
* **Improved Structure:** The refined Phase 2 structure (explicit backends, UI, common components, bindings) enhances architectural clarity and modularity compared to the original scattered namespaces.
|
||||
* **Easier Navigation & Maintenance:** A consistent structure makes it easier for developers to find code, understand component relationships, and perform maintenance.
|
||||
* **Foundation for Future Work:** Creates a cleaner base for developing new features and potentially consolidating shared functionality (e.g., in common utilities, resource management).
|
||||
|
||||
### Disadvantages
|
||||
|
||||
* **Significant Initial Effort:** Running this large-scale refactoring requires careful setup, dry runs, review, and validation.
|
||||
* **Potential for Breakage:** Automated refactoring on this scale carries inherent risks. Build script issues, reflection usage, non-Java file references (like in XML or properties), and complex type resolution might lead to compilation errors or runtime issues requiring manual fixes.
|
||||
* **Testing Burden:** Requires executing the full test suite (unit, integration, platform-specific) to ensure correctness after refactoring. Test code itself will also be refactored and needs verification.
|
||||
* **External Bindings Complexity:** Handling generated code or bindings for libraries like FlatBuffers, ONNX, and TensorFlow requires careful consideration beyond simple package renaming; modifying generator configurations is preferred.
|
||||
* **Learning Curve:** Teams unfamiliar with OpenRewrite will need to learn its usage and recipe development, especially for any necessary follow-up complex refactorings.
|
||||
* **Merge Conflicts:** This constitutes a large change, potentially creating significant merge conflicts for ongoing feature branches. Coordination is essential.
|
||||
|
||||
### Technical details about the Namespace Refactoring
|
||||
|
||||
* **Tooling:** The proposal relies on OpenRewrite and its `org.openrewrite.java.ChangePackage` recipe.
|
||||
* **Recipe Structure:** The YAML recipe uses a `recipeList` and applies more specific package mapping rules *before* more general ones to handle structural changes like backend separation and UI elevation correctly.
|
||||
* **`ChangePackage` Limitations:** This recipe primarily uses `ChangePackage`. Achieving the full Phase 2 vision, especially consolidating classes from multiple old locations into a single new package (e.g., common utilities), may require additional recipes using `MoveClass`, `MovePackage`, or custom Java-based recipes after this initial migration.
|
||||
* **Exclusions:** Generated binding specifications (`com.google.flatbuffers`, `onnx.*`, `tensorflow.*`) are intentionally excluded from automated changes due to high risk. Runtime wrappers/runners for these *are* included in the refactoring.
|
||||
* **Process:** The recommended process involves: backup -> dry run -> review -> iterative recipe refinement (if needed) -> apply changes -> compile -> test -> manual code review -> commit.
|
||||
|
||||
## Discussion
|
||||
|
||||
The proposed structure aims for a balance between respecting the original component boundaries (ND4J, DL4J Core, DataVec) and creating a more modern, cohesive structure under the Eclipse Foundation namespace. Key decisions reflected in the proposal:
|
||||
|
||||
* Explicitly separating backend implementations (`.nd4j.backend.[type]`).
|
||||
* Elevating UI to a top-level component (`.ui`).
|
||||
* Designating clear homes for bindings (`.bindings`), contrib (`.contrib`), and codegen (`.codegen`).
|
||||
* Establishing placeholders for consolidated common code (`.common`) and resource management (`.resources`), acknowledging full consolidation is a subsequent step.
|
||||
* Using `.dl4jcore` to house the bulk of the original `org.deeplearning4j` code to distinguish it clearly within the new structure.
|
||||
|
||||
Further discussion within the development team is needed to confirm these target structures and plan the implementation, particularly the steps required beyond the initial automated recipe application for deeper Phase 2 refinements.
|
||||
Reference in New Issue
Block a user