Repository: deepjavalibrary/djl
Pull request #1709 (2022-06-13 23:42:31)

[docs] Readme updates for PyTorch 1.11
## Description ##

Update README to reflect PyTorch 1.11 build process



Patch:
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index dfbff184c..979496c54 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -44,7 +44,11 @@ jobs:
       - name: clone demos
         run: |
           cd docs
-          git clone https://github.com/aws-samples/djl-demo.git demos
+          git clone https://github.com/deepjavalibrary/djl-demo.git demos
+      - name: clone serving
+        run: |
+          cd docs
+          git clone https://github.com/deepjavalibrary/djl-serving.git serving
       - name: build docs
         run: |
           cd docs
diff --git a/.github/workflows/serving_publish.yml b/.github/workflows/serving_publish.yml
index 25bc341b3..81a5780b0 100644
--- a/.github/workflows/serving_publish.yml
+++ b/.github/workflows/serving_publish.yml
@@ -52,4 +52,15 @@ jobs:
           ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.ORG_GRADLE_PROJECT_ossrhUsername }}
           ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.ORG_GRADLE_PROJECT_ossrhPassword }}
           DJL_STAGING: ${{ github.event.inputs.repo-id }}
-
+      - name: Configure AWS Credentials
+        uses: aws-actions/configure-aws-credentials@v1
+        with:
+          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          aws-region: us-east-2
+      - name: Publish deb
+        run: |
+          cd serving
+          ./gradlew createDeb -P${{ github.event.inputs.mode }}
+          aws s3 cp build/distributions/*.deb s3://djl-ai/publish/djl-serving/
+          aws cloudfront create-invalidation --distribution-id E371VB8JQ6NRVY --paths "/djl-serving/*"
diff --git a/android/pytorch-native/README.md b/android/pytorch-native/README.md
index fd9cb8e0a..195cff62c 100644
--- a/android/pytorch-native/README.md
+++ b/android/pytorch-native/README.md
@@ -1,54 +1,66 @@
 
 # Pytorch Android Build Procedure
 
+This procedure is meant only for developers who want to build PyTorch native binaries from source, this is not intended for regular users.
+
+Follow this setup guide in order to run DJL apps on an Android. In order to successfully run the apps, you must install several Android items and build Android ABIs outlined below.
+
 ## Prerequisites
 
 ```
-# install needed python library
-pip install pyyaml
-
-# Don't run the following command if you don't have python3 installed
+# Run the following command (assume you have python3 installed already)
 export PYTHON=python3
+
+# install needed python libraries
+python3 -m pip install numpy ninja pyyaml mkl mkl-include setuptools cmake cffi typing_extensions
 ```
+## Only run the android-sdk install specific to your OS, then proceed to installing command line tools below
 
-## Linux (Ubuntu 20.04) android-sdk install
+This will install the android-sdk on your machine as well as python3. It sets the ANDROID_HOME environment variable for use throughout this setup guide, and gives you ownership to the ANDROID_HOME directory which we need later.
+
+### Linux (Ubuntu 20.04) android-sdk install
 
 ```
-sudo apt-get install android-sdk cmake
-export ANDROID_SDK_ROOT=/usr/lib/android-sdk
-sudo chown -R ubuntu:ubuntu $ANDROID_SDK_ROOT
-```
+# install python and Android sdk
+sudo apt-get install android-sdk python3
 
-## Mac android-sdk install
+# set Android home
+export ANDROID_HOME=/usr/lib/android-sdk
 
+# give user ownership of ANDROID_HOME path
+sudo chown -R ubuntu:ubuntu $ANDROID_HOME
 ```
-brew install android-sdk cmake
-export ANDROID_SDK_ROOT=/Users/$USER/Library/Android/sdk
-sudo chown -R $USER $ANDROID_SDK_ROOT 
+
+### Mac android-sdk install
+
 ```
+# install python and Android sdk
+brew install android-sdk
 
-<br>
-<br>
-<br>
-<br>
-<br>
-<br>
+# set Android home
+export ANDROID_HOME=/Users/$USER/Library/Android/sdk
+
+# give user ownership of ANDROID_HOME path
+sudo chown -R $USER $ANDROID_HOME 
+```
 
 ## Install Command line only tools
 
 Find latest command line only tools: [https://developer.android.com/studio#downloads](https://developer.android.com/studio#downloads:~:text=Command%20line%20tools%20only)
 
 ```
-mkdir -p $ANDROID_SDK_ROOT/cmdline-tools
-cd $ANDROID_SDK_ROOT/cmdline-tools
+# create directory for Android command line tools
+mkdir -p $ANDROID_HOME/cmdline-tools
+cd $ANDROID_HOME/cmdline-tools
 
 # From link above, set ANDROID_CLT equal to the name of Linux's latest SDK tools package release
 export ANDROID_CLT=commandlinetools-linux-8512546_latest.zip
 
+# Download Android command line tools and remove the zip file once finished unzipping
 curl -O https://dl.google.com/android/repository/${ANDROID_CLT}
-unzip $ANDROID_CLT
-rm $ANDROID_CLT
+unzip $ANDROID_CLT && rm $ANDROID_CLT
 
+# renames the command line tools directory that just was unzipped
 mv cmdline-tools tools
 ```
 
@@ -57,8 +69,9 @@ mv cmdline-tools tools
 See GitHub actions to ensure latest NDK_VERSION: [https://github.com/deepjavalibrary/djl/blob/master/.github/workflows/native_s3_pytorch_android.yml](https://github.com/deepjavalibrary/djl/blob/master/.github/workflows/native_s3_pytorch_android.yml)
 
 ```
-export NDK_VERSION=21.0.6113669
-echo "y" | sudo ${ANDROID_SDK_ROOT}/cmdline-tools/tools/bin/sdkmanager --install "ndk;${NDK_VERSION}"
+# set Android NDK version and install it
+export NDK_VERSION=21.1.6352462
+echo "y" | sudo ${ANDROID_HOME}/cmdline-tools/tools/bin/sdkmanager --install "ndk;${NDK_VERSION}"
 ```
 
 ## Build PyTorch Android native binary from source
@@ -68,23 +81,21 @@ See: [https://github.com/deepjavalibrary/djl/blob/master/.github/workflows/nativ
 ```
 # cd into whatever directory holds your djl directory
 export PYTORCH_VERSION=1.11.0
-export ANDROID_NDK=${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}
+export ANDROID_NDK=${ANDROID_HOME}/ndk/${NDK_VERSION}
 
+# clone PyTorch to local machine
 git clone https://github.com/pytorch/pytorch -b "v${PYTORCH_VERSION}" --recursive
 cd pytorch
 
 # need to build all four ABIs
-export ANDROID_ABI="armeabi-v7a with NEON"
-#export ANDROID_ABI=arm64-v8a
-#export ANDROID_ABI=x86
-#export ANDROID_ABI=x86_64
-BUILD_LITE_INTERPRETER=0 ./scripts/build_android.sh 
-
-cd build_android
-zip -r ~/${ANDROID_BI}_native.zip install/include lib
-cd ..
-# clean up pytorch repository, before build next ABI
-git clean -dffx .
+export BUILD_LITE_INTERPRETER=0
+./scripts/build_pytorch_android.sh arm64-v8a,armeabi-v7a,x86,x86_64
+
+# zip all four ABIs
+cd build_android_arm64-v8a && zip -r ~/arm64-v8a_native.zip install/include lib && cd ..
+cd build_android_armeabi-v7a && zip -r ~/armeabi-v7a_native.zip install/include lib && cd ..
+cd build_android_x86 && zip -r ~/x86_native.zip install/include lib && cd ..
+cd build_android_x86_64 && zip -r ~/x86_64_native.zip install/include lib && cd ..
 ```
 
 The zip files will be uploaded to S3 bucket in CI build
@@ -93,22 +104,23 @@ The zip files will be uploaded to S3 bucket in CI build
 
 See: [https://github.com/deepjavalibrary/djl/blob/master/.github/workflows/native_jni_s3_pytorch_android.yml](https://github.com/deepjavalibrary/djl/blob/master/.github/workflows/native_jni_s3_pytorch_android.yml)
 
-```
-export ANDROID_NDK=${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}
+This command unzips all the files we zipped in the previous code block. It puts them into the directories where the DJL build expects to find them when it compiles.
 
+```
 cd ../djl/engines/pytorch/pytorch-native
 
-# to avoid download pytorch native from S3, manually unzip pytoch native
-mkdir libtorch_android
-cd libtorch_android
+# to avoid download PyTorch native from S3, manually unzip PyTorch native
+mkdir libtorch_android && cd libtorch_android
+
+# make directory for each Android ABI and unzip files
 mkdir arm64-v8a armeabi-v7a x86 x86_64
 cd arm64-v8a && unzip ~/arm64-v8a_native.zip && mv install/include . && cd ..
 cd armeabi-v7a && unzip ~/armeabi-v7a_native.zip && mv install/include . && cd ..
 cd x86 && unzip ~/x86_native.zip && mv install/include . && cd ..
 cd x86_64 && unzip ~/x86_64_native.zip && mv install/include . && cd ..
 
+# switch back to PyTorch native directory to build with gradle
 cd ..
-
 ./gradlew compileAndroidJNI -Ppt_version=${PYTORCH_VERSION}
 ```
 
@@ -118,6 +130,8 @@ cd ..
 
 See: [ https://github.com/deepjavalibrary/djl/blob/master/.github/workflows/publish_android_packages.yml]( https://github.com/deepjavalibrary/djl/blob/master/.github/workflows/publish_android_packages.yml)
 
+The final command in this code block `./gradlew pTML` is optional. It stores a local copy of the DJL snapshot in your maven directory. If not done, then the app will pull the snapshot release of DJL from Sonatype. 
+
 ```
 # move into djl/android directory
 cd ../../../android 
@@ -131,7 +145,7 @@ cp -r ../engines/pytorch/pytorch-native/jnilib/0.18.0/android/* pytorch-native/j
 ./gradlew pTML
 ```
 
-`ai.djl.android:pytorch-native:0.18.0-SNAPSHOT`  will be published to mavenLocal (your local ~/.m2 folder)
+`ai.djl.android:pytorch-native:0.18.0-SNAPSHOT`  will be published to mavenLocal (your local ~/.m2 folder) if you ran `./gradlew pTML`
 
 ## Build PyTorch Android demo projects
 
@@ -141,6 +155,5 @@ From Android Studio, with an emulator turned on, run the following commands
 
 ```
 cd djl-demo/android/pytorch_android/style_transfer_cyclegan
-./gradlew build
 ./gradlew iD
 ```
diff --git a/api/src/main/java/ai/djl/nn/core/LinearCollection.java b/api/src/main/java/ai/djl/nn/core/LinearCollection.java
new file mode 100644
index 000000000..5d69fe1c4
--- /dev/null
+++ b/api/src/main/java/ai/djl/nn/core/LinearCollection.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.nn.core;
+
+import ai.djl.Device;
+import ai.djl.MalformedModelException;
+import ai.djl.ndarray.NDArray;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.types.Shape;
+import ai.djl.nn.AbstractBlock;
+import ai.djl.nn.Block;
+import ai.djl.nn.Parameter;
+import ai.djl.training.ParameterStore;
+import ai.djl.util.PairList;
+import ai.djl.util.Preconditions;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * A LinearCollection block applies \(m\) linear transformations \(Y_i = X_i W_i + b_i\) for each
+ * \(i \in [1, \ldots, m]\) and \(m = \prod_{j=1}^t s_j\). \(t\) is typically 1, so compared to a
+ * {@link Linear} block, the involved shapes have typically one split dimension added which
+ * separates the different linear transformations from each other. Another difference to a {@link
+ * Linear} block is that the weight is not transposed (to align with the internally used algebraic
+ * operation {@link NDArray#matMul(NDArray)} ). There is currently only a single batch dimension
+ * \(x_1\) supported.
+ *
+ * <p>It has the following shapes:
+ *
+ * <ul>
+ *   <li>input X: [x_1, s_1, s_2, …, s_t, input_dim]
+ *   <li>weight W: [s_1, s_2, …, s_t, input_dim, units]
+ *   <li>Bias b: [s_1, s_2, …, s_t, units]
+ *   <li>output Y: [x_1, s_1, s_2, …, s_t, units]
+ * </ul>
+ *
+ * <p>The LinearCollection block should be constructed using {@link LinearCollection.Builder}.
+ */
+public class LinearCollection extends AbstractBlock {
+
+    private static final byte VERSION = 1;
+
+    private long units;
+    private long inputFeatures;
+
+    private Shape inputShape;
+
+    private Parameter weight;
+    private Parameter bias;
+
+    private int[] shiftBatchAxis;
+
+    private int[] reverseShiftBatchAxis;
+
+    LinearCollection(Builder builder) {
+        super(VERSION);
+        units = builder.units;
+        weight =
+                addParameter(
+                        Parameter.builder()
+                                .setName("weight")
+                                .setType(Parameter.Type.WEIGHT)
+                                .build());
+        if (builder.bias) {
+            bias =
+                    addParameter(
+                            Parameter.builder()
+                                    .setName("bias")
+                                    .setType(Parameter.Type.BIAS)
+                                    .build());
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected NDList forwardInternal(
+            ParameterStore parameterStore,
+            NDList inputs,
+            boolean training,
+            PairList<String, Object> params) {
+        NDArray input = inputs.singletonOrThrow();
+        Device device = input.getDevice();
+        NDArray weightArr = parameterStore.getValue(weight, device, training);
+        NDArray biasArr = parameterStore.getValue(bias, device, training);
+        return linear(input, weightArr, biasArr);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Shape[] getOutputShapes(Shape[] inputs) {
+        return new Shape[] {inputs[0].slice(0, inputs[0].dimension() - 1).add(units)};
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public PairList<String, Shape> describeInput() {
+        return new PairList<>(
+                Collections.singletonList("linearInput"), Collections.singletonList(inputShape));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void beforeInitialize(Shape... inputShapes) {
+        super.beforeInitialize(inputShapes);
+        Preconditions.checkArgument(inputShapes.length == 1, "Linear block only support 1 input");
+        Shape input = inputShapes[0];
+        inputFeatures = input.slice(1, input.dimension()).size();
+        inputShape = input.slice(0, 1);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void prepare(Shape[] inputShapes) {
+        Shape input = inputShapes[0];
+        weight.setShape(input.slice(1, input.dimension()).add(units));
+        if (bias != null) {
+            bias.setShape(input.slice(1, input.dimension() - 1).add(units));
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void saveMetadata(DataOutputStream os) throws IOException {
+        os.writeLong(units);
+        os.writeLong(inputFeatures);
+        os.write(inputShape.getEncoded());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void loadMetadata(byte loadVersion, DataInputStream is)
+            throws IOException, MalformedModelException {
+        switch (loadVersion) {
+            case VERSION:
+                units = is.readLong();
+                inputFeatures = is.readLong();
+                break;
+            default:
+                throw new MalformedModelException("Unsupported encoding version: " + loadVersion);
+        }
+        inputShape = Shape.decode(is);
+    }
+
+    /**
+     * Applies linear transformations to the incoming data.
+     *
+     * @param input input X: [x_1, s_1, s_2, …, s_t, input_dim]
+     * @param weight weight W: [s_1, s_2, …, s_t, input_dim, units]
+     * @param bias bias b: [s_1, s_2, …, s_t, units]
+     * @return output Y: [x_1, s_1, s_2, …, s_t, units]
+     */
+    public NDList linear(NDArray input, NDArray weight, NDArray bias) {
+        if (shiftBatchAxis == null) {
+            // as the batch axis is the first axis in the shape of the input resp. output
+            // arrays, it needs to be shifted in order to bring the split axes in front resp. back
+            // again to be suitable for matMul;
+            // in case there is only one split axis, the transpose array (1,0,2) could be used for
+            // both shifts, but for the general case we calculate the transpose arrays here
+            int dim = input.getShape().dimension();
+            shiftBatchAxis = new int[dim];
+            reverseShiftBatchAxis = new int[dim];
+            for (int d = 0; d < dim - 2; d++) {
+                shiftBatchAxis[d] = d + 1;
+                reverseShiftBatchAxis[d + 1] = d;
+            }
+            shiftBatchAxis[dim - 1] = dim - 1;
+            reverseShiftBatchAxis[dim - 1] = dim - 1;
+            shiftBatchAxis[dim - 2] = 0;
+            reverseShiftBatchAxis[0] = dim - 2;
+        }
+        NDArray resultArr =
+                input.transpose(shiftBatchAxis).matMul(weight).transpose(reverseShiftBatchAxis);
+        if (bias != null) {
+            resultArr.addi(bias);
+        }
+        return new NDList(resultArr);
+    }
+
+    /**
+     * Creates a builder to build a {@code Linear}.
+     *
+     * @return a new builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** The Builder to construct a {@link LinearCollection} type of {@link Block}. */
+    public static final class Builder {
+
+        private long units;
+        private boolean bias = true;
+
+        Builder() {}
+
+        /**
+         * Sets the number of output channels.
+         *
+         * @param units the number of desired output channels
+         * @return this Builder
+         */
+        public Builder setUnits(long units) {
+            this.units = units;
+            return this;
+        }
+
+        /**
+         * Sets the optional parameter that indicates whether to include a bias vector with default
+         * value of true.
+         *
+         * @param bias whether to use a bias vector parameter
+         * @return this Builder
+         */
+        public Builder optBias(boolean bias) {
+            this.bias = bias;
+            return this;
+        }
+
+        /**
+         * Returns the constructed {@code Linear}.
+         *
+         * @return the constructed {@code Linear}
+         * @throws IllegalArgumentException if all required parameters (outChannels) have not been
+         *     set
+         */
+        public LinearCollection build() {
+            Preconditions.checkArgument(units > 0, "You must specify unit");
+            return new LinearCollection(this);
+        }
+    }
+}
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/MovieLens100k.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/MovieLens100k.java
new file mode 100644
index 000000000..e1d53c367
--- /dev/null
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/MovieLens100k.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.basicdataset.tabular;
+
+import ai.djl.Application;
+import ai.djl.basicdataset.BasicDatasets;
+import ai.djl.repository.Artifact;
+import ai.djl.repository.MRL;
+import ai.djl.repository.Repository;
+import ai.djl.util.Progress;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
+
+/** Movielens 100k movie reviews dataset from https://grouplens.org/datasets/movielens/100k/. */
+public final class MovieLens100k extends CsvDataset {
+
+    private static final String ARTIFACT_ID = "movielens-100k";
+    private static final String VERSION = "1.0";
+
+    private static final String[] USER_FEATURES = {
+        "user_id", "user_age", "user_gender", "user_occupation", "user_zipcode"
+    };
+    private static final String[] MOVIE_FEATURES = {
+        "movie_id",
+        "movie_title",
+        "movie_release_date",
+        "movie_video_release_date",
+        "imdb_url",
+        "unknown",
+        "action",
+        "adventure",
+        "animation",
+        "childrens",
+        "comedy",
+        "crime",
+        "documentary",
+        "drama",
+        "fantasy",
+        "film-noir",
+        "horror",
+        "musical",
+        "mystery",
+        "romance",
+        "sci-fi",
+        "thriller",
+        "war",
+        "western"
+    };
+
+    enum HeaderEnum {
+        user_id,
+        movie_id,
+        rating,
+        timestamp
+    }
+
+    private Usage usage;
+    private MRL mrl;
+    private boolean prepared;
+    private Map<String, Map<String, String>> userFeaturesMap;
+    private Map<String, Map<String, String>> movieFeaturesMap;
+
+    MovieLens100k(Builder builder) {
+        super(builder);
+        usage = builder.usage;
+        mrl = builder.getMrl();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected String getCell(long rowIndex, String featureName) {
+        CSVRecord record = csvRecords.get(Math.toIntExact(rowIndex));
+        if (HeaderEnum.rating.toString().equals(featureName)) {
+            return record.get(HeaderEnum.rating);
+        }
+        if (featureName.startsWith("user")) {
+            String userId = record.get(HeaderEnum.user_id);
+            return userFeaturesMap.get(userId).get(featureName);
+        }
+        String movieId = record.get(HeaderEnum.movie_id);
+        return movieFeaturesMap.get(movieId).get(featureName);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void prepare(Progress progress) throws IOException {
+        if (prepared) {
+            return;
+        }
+
+        Artifact artifact = mrl.getDefaultArtifact();
+        mrl.prepare(artifact, progress);
+
+        Path dir = mrl.getRepository().getResourceDirectory(artifact);
+        Path root = dir.resolve("ml-100k/ml-100k");
+
+        // The actual feature values to use for training/testing are stored in separate files
+        Path userFeaturesFile = root.resolve("u.user");
+        userFeaturesMap = prepareFeaturesMap(userFeaturesFile, USER_FEATURES);
+        Path movieFeaturesFile = root.resolve("u.item");
+        movieFeaturesMap = prepareFeaturesMap(movieFeaturesFile, MOVIE_FEATURES);
+
+        Path csvFile;
+        switch (usage) {
+            case TRAIN:
+                csvFile = root.resolve("ua.base");
+                break;
+            case TEST:
+                csvFile = root.resolve("ua.test");
+                break;
+            case VALIDATION:
+            default:
+                throw new UnsupportedOperationException("Validation data not available");
+        }
+
+        csvUrl = csvFile.toUri().toURL();
+        super.prepare(progress);
+        prepared = true;
+    }
+
+    private Map<String, Map<String, String>> prepareFeaturesMap(
+            Path featureFile, String[] featureNames) throws IOException {
+        URL featureFileUrl = featureFile.toUri().toURL();
+        CSVFormat format = CSVFormat.Builder.create(CSVFormat.newFormat('|')).build();
+        Reader reader =
+                new InputStreamReader(
+                        new BufferedInputStream(featureFileUrl.openStream()),
+                        StandardCharsets.UTF_8);
+        CSVParser csvParser = new CSVParser(reader, format);
+        List<CSVRecord> featureRecords = csvParser.getRecords();
+
+        Map<String, Map<String, String>> featuresMap = new ConcurrentHashMap<>();
+        for (CSVRecord record : featureRecords) {
+            Map<String, String> featureValues = new ConcurrentHashMap<>();
+            for (int i = 0; i < featureNames.length; i++) {
+                featureValues.put(featureNames[i], record.get(i));
+            }
+            featuresMap.put(record.get(0), featureValues);
+        }
+        return featuresMap;
+    }
+
+    /**
+     * Creates a builder to build a {@link MovieLens100k}.
+     *
+     * @return a new builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** A builder to construct a {@link MovieLens100k}. */
+    public static final class Builder extends CsvBuilder<Builder> {
+
+        Repository repository;
+        String groupId;
+        String artifactId;
+        Usage usage;
+        List<String> featureArray =
+                new ArrayList<>(
+                        Arrays.asList(
+                                "user_age",
+                                "user_gender",
+                                "user_occupation",
+                                "user_zipcode",
+                                "movie_title",
+                                "movie_genres"));
+
+        List<String> movieGenres =
+                new ArrayList<>(
+                        Arrays.asList(
+                                "unknown",
+                                "action",
+                                "adventure",
+                                "animation",
+                                "childrens",
+                                "comedy",
+                                "crime",
+                                "documentary",
+                                "drama",
+                                "fantasy",
+                                "film-noir",
+                                "horror",
+                                "musical",
+                                "mystery",
+                                "romance",
+                                "sci-fi",
+                                "thriller",
+                                "war",
+                                "western"));
+
+        /** Constructs a new builder. */
+        Builder() {
+            repository = BasicDatasets.REPOSITORY;
+            groupId = BasicDatasets.GROUP_ID;
+            artifactId = ARTIFACT_ID;
+            usage = Usage.TRAIN;
+            csvFormat = CSVFormat.TDF.builder().setHeader(HeaderEnum.class).setQuote(null).build();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Builder self() {
+            return this;
+        }
+
+        /**
+         * Sets the optional usage.
+         *
+         * @param usage the new usage
+         * @return this builder
+         */
+        public Builder optUsage(Usage usage) {
+            this.usage = usage;
+            return self();
+        }
+
+        /**
+         * Sets the optional repository.
+         *
+         * @param repository the repository
+         * @return this builder
+         */
+        public Builder optRepository(Repository repository) {
+            this.repository = repository;
+            return self();
+        }
+
+        /**
+         * Sets optional groupId.
+         *
+         * @param groupId the groupId}
+         * @return this builder
+         */
+        public Builder optGroupId(String groupId) {
+            this.groupId = groupId;
+            return self();
+        }
+
+        /**
+         * Sets the optional artifactId.
+         *
+         * @param artifactId the artifactId
+         * @return this builder
+         */
+        public Builder optArtifactId(String artifactId) {
+            if (artifactId.contains(":")) {
+                String[] tokens = artifactId.split(":");
+                groupId = tokens[0];
+                this.artifactId = tokens[1];
+            } else {
+                this.artifactId = artifactId;
+            }
+            return self();
+        }
+
+        /**
+         * Returns the available features of this dataset.
+         *
+         * @return a list of feature names
+         */
+        public List<String> getAvailableFeatures() {
+            return featureArray;
+        }
+
+        /**
+         * Adds a feature to the features set.
+         *
+         * @param name the name of the feature
+         * @return this builder
+         */
+        public Builder addFeature(String name) {
+            if (getAvailableFeatures().contains(name)) {
+                switch (name) {
+                    case "user_age":
+                        addNumericFeature(name);
+                        break;
+                    case "user_gender":
+                    case "user_occupation":
+                        addCategoricalFeature(name, true);
+                        break;
+                    case "user_zipcode":
+                    case "movie_title":
+                        addCategoricalFeature(name, false);
+                        break;
+                    case "movie_genres":
+                        movieGenres.forEach(genre -> addNumericFeature(genre));
+                        break;
+                    default:
+                        break;
+                }
+            } else {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Provided feature %s is not valid. Valid features are: %s",
+                                name, featureArray));
+            }
+            return self();
+        }
+
+        /**
+         * Builds the new {@link MovieLens100k}.
+         *
+         * @return the new {@link MovieLens100k}
+         */
+        @Override
+        public MovieLens100k build() {
+            if (features.isEmpty()) {
+                featureArray.forEach(feature -> addFeature(feature));
+            }
+            if (labels.isEmpty()) {
+                addNumericLabel("rating");
+            }
+            return new MovieLens100k(this);
+        }
+
+        MRL getMrl() {
+            return repository.dataset(Application.Tabular.ANY, groupId, artifactId, VERSION);
+        }
+    }
+}
diff --git a/docs/.gitignore b/docs/.gitignore
index cd3a6135e..81625f8f8 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -1,1 +1,2 @@
-demos
\ No newline at end of file
+demos
+serving
diff --git a/docs/dataset.md b/docs/dataset.md
index cb6d10778..7362673fa 100644
--- a/docs/dataset.md
+++ b/docs/dataset.md
@@ -71,6 +71,7 @@ This module contains the following datasets:
 
 - [Airfoil Self-Noise](https://javadoc.io/doc/ai.djl/basicdataset/latest/ai/djl/basicdataset/tabular/AirfoilRandomAccess.html) - A 6 feature dataset from NASA tests of airfoils
 - [Ames House Pricing](https://javadoc.io/doc/ai.djl/basicdataset/latest/ai/djl/basicdataset/tabular/AmesRandomAccess.html) - A 80 feature dataset to predict house prices
+- [Movielens 100k](https://javadoc.io/doc/ai.djl/basicdataset/latest/ai/djl/basicdataset/tabular/MovieLens100k.html) - A 6 feature dataset of movie ratings on 1682 movies from 943 users
 
 ### Time Series
 
diff --git a/docs/development/troubleshooting.md b/docs/development/troubleshooting.md
index c884bed9b..453f09669 100644
--- a/docs/development/troubleshooting.md
+++ b/docs/development/troubleshooting.md
@@ -232,7 +232,7 @@ No X11 DISPLAY variable was set, but this program performed an operation which r
 java.awt.HeadlessException:
 ```
 
-Follow the [steps here](https://github.com/aws-samples/d2l-java/blob/master/documentation/troubleshoot.md) to resolve it.
+Follow the [steps here](https://github.com/deepjavalibrary/d2l-java/blob/master/documentation/troubleshoot.md) to resolve it.
 
 
 ## 10. JVM crash
diff --git a/docs/interactive_tool.md b/docs/interactive_tool.md
index 683b56d05..ed102fedc 100644
--- a/docs/interactive_tool.md
+++ b/docs/interactive_tool.md
@@ -5,18 +5,18 @@ Without additional setup, you can easily run the tool kits online and export the
 
 ## [Interactive JShell](https://djl.ai/website/demo.html#jshell)
 
-![terminal](https://raw.githubusercontent.com/aws-samples/djl-demo/master/web-demo/interactive-console/img/terminal.gif)
+![terminal](https://raw.githubusercontent.com/deepjavalibrary/djl-demo/master/web-demo/interactive-console/img/terminal.gif)
 
 Interactive JShell is a modified version of [JShell](https://docs.oracle.com/javase/9/jshell/introduction-jshell.htm#JSHEL-GUID-630F27C8-1195-4989-9F6B-2C51D46F52C8) equipped with DJL features.
 You can use the existing Java features as well as DJL classes.
 To test out some functions in DJL, use this JShell to try methods defined in the [Javadoc](https://javadoc.io/doc/ai.djl/api/latest/ai/djl/ndarray/NDArray.html).
 We created useful operators to use in your deep learning applications.
-For more information about this JShell, see the [Interactive JShell and Block Runner for DJL](https://github.com/aws-samples/djl-demo/tree/master/web-demo/interactive-console#jshell) demo.
+For more information about this JShell, see the [Interactive JShell and Block Runner for DJL](https://github.com/deepjavalibrary/djl-demo/tree/master/web-demo/interactive-console#jshell) demo.
 
 
 ## [Block Runner](https://djl.ai/website/demo.html#block-runner)
 
-![block runner](https://raw.githubusercontent.com/aws-samples/djl-demo/master/web-demo/interactive-console/img/blockrunner.gif)
+![block runner](https://raw.githubusercontent.com/deepjavalibrary/djl-demo/master/web-demo/interactive-console/img/blockrunner.gif)
 
 Block Runner is designed to be simple. 
 It allows you to run Java code line by line without worrying about the class structures. As shown in the gif, you can simply craft some code and click the “Run” button to run it online. 
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 026ccb953..c54afb223 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -135,8 +135,18 @@ nav:
       - AWS S3 support: 'extensions/aws-ai/README.md'
       - fastText: 'extensions/fasttext/README.md'
       - Hadoop support: 'extensions/hadoop/README.md'
+      - Huggingface Tokenizers: 'extensions/tokenizers/README.md'
       - OpenCV: 'extensions/opencv/README.md'
       - SentencePiece: 'extensions/sentencepiece/README.md'
+      - Tablesaw: 'extensions/tablesaw/README.md'
+      - DJL Serving:
+          - 'docs/serving/README.md'
+          - 'docs/serving/serving/docs/workflows.md'
+          - 'docs/serving/serving/docs/configuration.md'
+          - 'docs/serving/serving/docs/inference_api.md'
+          - 'docs/serving/serving/docs/management_api.md'
+          - 'docs/serving/serving/docs/plugin_management.md'
+      - 'docs/serving/wlm/README.md'
   - Demos:
       - Demos: 'docs/demos/README.md'
       - AWS:
@@ -173,7 +183,7 @@ nav:
           - Footwear Classification: 'docs/demos/footwear_classification/README.md'
           - Live Object Detection: 'docs/demos/live-object-detection/README.md'
           - Pneumonia Detection: 'docs/demos/pneumonia-detection/README.md'
-          - MultiEngine on DJL: 'docs/demos/multi-engine/README.md'
+          - MultiEngine on DJL: 'docs/demos/development/multi-engine/README.md'
           - Interactive JShell and Block Runner for DJL: 'docs/demos/web-demo/interactive-console/README.md'
           - Malicious URL Detection: 'docs/demos/malicious-url-detector/README.md'
       - Extensions:
diff --git a/examples/README.md b/examples/README.md
index 9c5b293e1..b5c7a7c7e 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,7 +1,7 @@
 # Examples
 
 This module contains examples to demonstrate use of the Deep Java Library (DJL).
-You can find more examples from our [djl-demo github repo](https://github.com/aws-samples/djl-demo).
+You can find more examples from our [djl-demo github repo](https://github.com/deepjavalibrary/djl-demo).
 
 The following examples are included for training:
 
@@ -34,7 +34,7 @@ You may be able to find more translator examples in our engine specific model zo
 [PyTorch](https://github.com/deepjavalibrary/djl/tree/master/engines/pytorch/pytorch-model-zoo),
 and [TensorFlow](https://github.com/deepjavalibrary/djl/tree/master/engines/tensorflow/tensorflow-model-zoo).
 
-More examples and demos of applications featuring DJL are located in our [demo repository](https://github.com/aws-samples/djl-demo).
+More examples and demos of applications featuring DJL are located in our [demo repository](https://github.com/deepjavalibrary/djl-demo).
 
 ## Prerequisites
 
diff --git a/jupyter/load_pytorch_model.ipynb b/jupyter/load_pytorch_model.ipynb
index 13ceb96fb..3c843d400 100644
--- a/jupyter/load_pytorch_model.ipynb
+++ b/jupyter/load_pytorch_model.ipynb
@@ -106,7 +106,7 @@
     "...\n",
     "```\n",
     "\n",
-    "Then, we will use this pipeline to create the [`Translator`](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/translate/Translator.html)"
+    "Then, we will use this pipeline to create the [`Translator`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/translate/Translator.html)"
    ]
   },
   {
diff --git a/jupyter/mxnet/load_your_own_mxnet_bert.ipynb b/jupyter/mxnet/load_your_own_mxnet_bert.ipynb
index 338b7446f..b0327d395 100644
--- a/jupyter/mxnet/load_your_own_mxnet_bert.ipynb
+++ b/jupyter/mxnet/load_your_own_mxnet_bert.ipynb
@@ -6,7 +6,7 @@
    "source": [
     "# Load your own MXNet BERT model\n",
     "\n",
-    "In the previous [example](https://github.com/deepjavalibrary/djl/blob/master/jupyter/BERTQA.ipynb), you run BERT inference with the model from Model Zoo. You can also load the model on your own pre-trained BERT and use custom classes as the input and output.\n",
+    "In the previous [example](../BERTQA.ipynb), you run BERT inference with the model from Model Zoo. You can also load the model on your own pre-trained BERT and use custom classes as the input and output.\n",
     "\n",
     "In general, the MXNet BERT model requires these three inputs:\n",
     "\n",
@@ -116,11 +116,11 @@
     "![https://github.com/deepjavalibrary/djl/blob/master/examples/docs/img/workFlow.png?raw=true](https://github.com/deepjavalibrary/djl/blob/master/examples/docs/img/workFlow.png?raw=true)\n",
     "\n",
     "The red block (\"Images\") in the workflow is the input that DJL expects from you. The green block (\"Images\n",
-    "bounding box\") is the output that you expect. Because DJL does not know which input to expect and which output format that you prefer, DJL provides the `Translator` interface so you can define your own\n",
+    "bounding box\") is the output that you expect. Because DJL does not know which input to expect and which output format that you prefer, DJL provides the [`Translator`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/translate/Translator.html) interface so you can define your own\n",
     "input and output.\n",
     "\n",
     "The `Translator` interface encompasses the two white blocks: Pre-processing and Post-processing. The pre-processing\n",
-    "component converts the user-defined input objects into an NDList, so that the `Predictor` in DJL can understand the\n",
+    "component converts the user-defined input objects into an NDList, so that the [`Predictor`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html) in DJL can understand the\n",
     "input and make its prediction. Similarly, the post-processing block receives an NDList as the output from the\n",
     "`Predictor`. The post-processing block allows you to convert the output from the `Predictor` to the desired output\n",
     "format."
@@ -132,7 +132,7 @@
    "source": [
     "### Pre-processing\n",
     "\n",
-    "Now, you need to convert the sentences into tokens. We provide a powerful tool `BertTokenizer` that you can use to convert questions and answers into tokens, and batchify your sequence together. Once you have properly formatted tokens, you can use `Vocabulary` to map your token to BERT index.\n",
+    "Now, you need to convert the sentences into tokens. We provide a powerful tool [`BertTokenizer`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/modality/nlp/bert/BertTokenizer.html) that you can use to convert questions and answers into tokens, and batchify your sequence together. Once you have properly formatted tokens, you can use [`Vocabulary`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/modality/nlp/Vocabulary.html) to map your token to BERT index.\n",
     "\n",
     "The following code block demonstrates tokenizing the question and answer defined earlier into BERT-formatted tokens."
    ]
@@ -310,7 +310,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Armed with the needed knowledge, you can write an implementation of the `Translator` interface. `BertTranslator` uses the code snippets explained previously to implement the `processInput`method. For more information, see [`NDManager`](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/ndarray/NDManager.html).\n",
+    "Armed with the needed knowledge, you can write an implementation of the `Translator` interface. `BertTranslator` uses the code snippets explained previously to implement the `processInput`method. For more information, see [`NDManager`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/ndarray/NDManager.html).\n",
     "\n",
     "```\n",
     "manager.create(Number[] data, Shape)\n",
@@ -389,7 +389,7 @@
    "source": [
     "Congrats! You have created your first Translator! We have pre-filled the `processOutput()` function to process the `NDList` and return it in a desired format. `processInput()` and `processOutput()` offer the flexibility to get the predictions from the model in any format you desire. \n",
     "\n",
-    "With the Translator implemented, you need to bring up the predictor that uses your `Translator` to start making predictions. You can find the usage for `Predictor` in the [Predictor Javadoc](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/inference/Predictor.html). Create a translator and use the `question` and `resourceDocument` provided previously."
+    "With the Translator implemented, you need to bring up the predictor that uses your `Translator` to start making predictions. You can find the usage for `Predictor` in the [Predictor Javadoc](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html). Create a translator and use the `question` and `resourceDocument` provided previously."
    ]
   },
   {
diff --git a/jupyter/onnxruntime/machine_learning_with_ONNXRuntime.ipynb b/jupyter/onnxruntime/machine_learning_with_ONNXRuntime.ipynb
index 73611c2c8..885000e33 100644
--- a/jupyter/onnxruntime/machine_learning_with_ONNXRuntime.ipynb
+++ b/jupyter/onnxruntime/machine_learning_with_ONNXRuntime.ipynb
@@ -43,7 +43,7 @@
     "\n",
     "This tutorial requires the installation of Java Kernel. To install the Java Kernel, see the [README](https://github.com/deepjavalibrary/djl/blob/master/jupyter/README.md).\n",
     "\n",
-    "These are dependencies we will use. To enhance the NDArray operation capability, we are importing ONNX Runtime and PyTorch Engine at the same time. Please find more information [here](https://github.com/deepjavalibrary/djl/blob/master/docs/onnxruntime/hybrid_engine.md#hybrid-engine-for-onnx-runtime)."
+    "These are dependencies we will use. To enhance the NDArray operation capability, we are importing ONNX Runtime and PyTorch Engine at the same time. Please find more information [here](https://github.com/deepjavalibrary/djl/blob/master/docs/hybrid_engine.md)."
    ]
   },
   {
@@ -86,13 +86,13 @@
     "\n",
     "![https://github.com/deepjavalibrary/djl/blob/master/examples/docs/img/workFlow.png?raw=true](https://github.com/deepjavalibrary/djl/blob/master/examples/docs/img/workFlow.png?raw=true)\n",
     "\n",
-    "The `Translator` interface encompasses the two white blocks: Pre-processing and Post-processing. The pre-processing\n",
-    "component converts the user-defined input objects into an NDList, so that the `Predictor` in DJL can understand the\n",
+    "The [`Translator`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/translate/Translator.html) interface encompasses the two white blocks: Pre-processing and Post-processing. The pre-processing\n",
+    "component converts the user-defined input objects into an NDList, so that the [`Predictor`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html) in DJL can understand the\n",
     "input and make its prediction. Similarly, the post-processing block receives an NDList as the output from the\n",
     "`Predictor`. The post-processing block allows you to convert the output from the `Predictor` to the desired output\n",
     "format.\n",
     "\n",
-    "In our use case, we use a class namely `IrisFlower` as our input class type. We will use [`Classifications`](https://javadoc.io/doc/ai.djl/api/latest/ai/djl/modality/Classifications.html) as our output class type."
+    "In our use case, we use a class namely `IrisFlower` as our input class type. We will use [`Classifications`](https://javadoc.io/doc/ai.djl/api/0.17.0/ai/djl/modality/Classifications.html) as our output class type."
    ]
   },
   {
@@ -164,7 +164,7 @@
    "source": [
     "## Step 2 Prepare your model\n",
     "\n",
-    "We will load a pretrained sklearn model into DJL. We defined a [`ModelZoo`](https://javadoc.io/doc/ai.djl/api/latest/ai/djl/repository/zoo/ModelZoo.html) concept to allow user load model from varity of locations, such as remote URL, local files or DJL pretrained model zoo. We need to define `Criteria` class to help the modelzoo locate the model and attach translator. In this example, we download a compressed ONNX model from S3."
+    "We will load a pretrained sklearn model into DJL. We defined a [`ModelZoo`](https://javadoc.io/doc/ai.djl/api/0.17.0/ai/djl/repository/zoo/ModelZoo.html) concept to allow user load model from varity of locations, such as remote URL, local files or DJL pretrained model zoo. We need to define [`Criteria`](https://javadoc.io/doc/ai.djl/api/0.17.0/ai/djl/repository/zoo/Criteria.html) class to help the modelzoo locate the model and attach translator. In this example, we download a compressed ONNX model from S3."
    ]
   },
   {
diff --git a/jupyter/paddlepaddle/face_mask_detection_paddlepaddle.ipynb b/jupyter/paddlepaddle/face_mask_detection_paddlepaddle.ipynb
index f4a55b009..a20626bc9 100644
--- a/jupyter/paddlepaddle/face_mask_detection_paddlepaddle.ipynb
+++ b/jupyter/paddlepaddle/face_mask_detection_paddlepaddle.ipynb
@@ -69,7 +69,7 @@
     "- Resize: Shrink the image with a certain ratio to feed in\n",
     "- Normalize the image with a scale\n",
     "\n",
-    "Fortunatly, DJL offers a `Translator` interface that can help you with these processing. The rough Translator architecture looks like below:\n",
+    "Fortunatly, DJL offers a [`Translator`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/translate/Translator.html) interface that can help you with these processing. The rough Translator architecture looks like below:\n",
     "\n",
     "![](https://github.com/deepjavalibrary/djl/blob/master/examples/docs/img/workFlow.png?raw=true)\n",
     "\n",
@@ -127,7 +127,7 @@
     "\n",
     "### Postprocessing\n",
     "\n",
-    "For postprocessing, The output is in shape of (number_of_boxes, (class_id, probability, xmin, ymin, xmax, ymax)). We can store them into the prebuilt DJL `DetectedObjects` classes for further processing. Let's assume we have an inference output of ((1, 0.99, 0.2, 0.4, 0.5, 0.8)) and try to draw this box out."
+    "For postprocessing, The output is in shape of (number_of_boxes, (class_id, probability, xmin, ymin, xmax, ymax)). We can store them into the prebuilt DJL [`DetectedObjects`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/modality/cv/output/DetectedObjects.html) classes for further processing. Let's assume we have an inference output of ((1, 0.99, 0.2, 0.4, 0.5, 0.8)) and try to draw this box out."
    ]
   },
   {
@@ -207,7 +207,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "To run inference with this model, we need to load the model from Paddle model zoo. To load a model in DJL, you need to specify a `Crieteria`. `Crieteria` is used identify where to load the model and which `Translator` should apply to it. Then, all we need to do is to get a `Predictor` from the model and use it to do inference:"
+    "To run inference with this model, we need to load the model from Paddle model zoo. To load a model in DJL, you need to specify a [`Crieteria`](https://javadoc.io/doc/ai.djl/api/0.17.0/ai/djl/repository/zoo/Criteria.html). `Crieteria` is used identify where to load the model and which `Translator` should apply to it. Then, all we need to do is to get a [`Predictor`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html) from the model and use it to do inference:"
    ]
   },
   {
@@ -288,7 +288,7 @@
    "source": [
     "### Prepare Translator and load the model\n",
     "\n",
-    "For the face classification model, we can use DJL prebuilt `ImageClassificationTranslator` with a few transformation. This Translator brings a basic image translation process and can be extended with additional standard processing steps. So in our case, we don't have to create another `Translator` and just leverage on this prebuilt one."
+    "For the face classification model, we can use DJL prebuilt [`ImageClassificationTranslator`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/modality/cv/translator/ImageClassificationTranslator.html) with a few transformation. This Translator brings a basic image translation process and can be extended with additional standard processing steps. So in our case, we don't have to create another `Translator` and just leverage on this prebuilt one."
    ]
   },
   {
diff --git a/jupyter/pytorch/load_your_own_pytorch_bert.ipynb b/jupyter/pytorch/load_your_own_pytorch_bert.ipynb
index 9964662be..663572995 100644
--- a/jupyter/pytorch/load_your_own_pytorch_bert.ipynb
+++ b/jupyter/pytorch/load_your_own_pytorch_bert.ipynb
@@ -111,11 +111,11 @@
     "![https://github.com/deepjavalibrary/djl/blob/master/examples/docs/img/workFlow.png?raw=true](https://github.com/deepjavalibrary/djl/blob/master/examples/docs/img/workFlow.png?raw=true)\n",
     "\n",
     "The red block (\"Images\") in the workflow is the input that DJL expects from you. The green block (\"Images\n",
-    "bounding box\") is the output that you expect. Because DJL does not know which input to expect and which output format that you prefer, DJL provides the `Translator` interface so you can define your own\n",
+    "bounding box\") is the output that you expect. Because DJL does not know which input to expect and which output format that you prefer, DJL provides the [`Translator`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/translate/Translator.html) interface so you can define your own\n",
     "input and output.\n",
     "\n",
     "The `Translator` interface encompasses the two white blocks: Pre-processing and Post-processing. The pre-processing\n",
-    "component converts the user-defined input objects into an NDList, so that the `Predictor` in DJL can understand the\n",
+    "component converts the user-defined input objects into an NDList, so that the [`Predictor`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html) in DJL can understand the\n",
     "input and make its prediction. Similarly, the post-processing block receives an NDList as the output from the\n",
     "`Predictor`. The post-processing block allows you to convert the output from the `Predictor` to the desired output\n",
     "format."
@@ -127,7 +127,7 @@
    "source": [
     "### Pre-processing\n",
     "\n",
-    "Now, you need to convert the sentences into tokens. We provide a powerful tool `BertTokenizer` that you can use to convert questions and answers into tokens, and batchify your sequence together. Once you have properly formatted tokens, you can use `Vocabulary` to map your token to BERT index.\n",
+    "Now, you need to convert the sentences into tokens. We provide a powerful tool [`BertTokenizer`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/modality/nlp/bert/BertTokenizer.html) that you can use to convert questions and answers into tokens, and batchify your sequence together. Once you have properly formatted tokens, you can use [`Vocabulary`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/modality/nlp/Vocabulary.html) to map your token to BERT index.\n",
     "\n",
     "The following code block demonstrates tokenizing the question and answer defined earlier into BERT-formatted tokens."
    ]
@@ -272,7 +272,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Armed with the needed knowledge, you can write an implementation of the `Translator` interface. `BertTranslator` uses the code snippets explained previously to implement the `processInput`method. For more information, see [`NDManager`](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/ndarray/NDManager.html).\n",
+    "Armed with the needed knowledge, you can write an implementation of the `Translator` interface. `BertTranslator` uses the code snippets explained previously to implement the `processInput`method. For more information, see [`NDManager`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/ndarray/NDManager.html).\n",
     "\n",
     "```\n",
     "manager.create(Number[] data, Shape)\n",
@@ -345,7 +345,7 @@
    "source": [
     "Congrats! You have created your first Translator! We have pre-filled the `processOutput()` function to process the `NDList` and return it in a desired format. `processInput()` and `processOutput()` offer the flexibility to get the predictions from the model in any format you desire. \n",
     "\n",
-    "With the Translator implemented, you need to bring up the predictor that uses your `Translator` to start making predictions. You can find the usage for `Predictor` in the [Predictor Javadoc](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/inference/Predictor.html). Create a translator and use the `question` and `resourceDocument` provided previously."
+    "With the Translator implemented, you need to bring up the predictor that uses your `Translator` to start making predictions. You can find the usage for `Predictor` in the [Predictor Javadoc](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html). Create a translator and use the `question` and `resourceDocument` provided previously."
    ]
   },
   {
diff --git a/jupyter/tensorflow/pneumonia_detection.ipynb b/jupyter/tensorflow/pneumonia_detection.ipynb
index 97118bfee..d0683d365 100644
--- a/jupyter/tensorflow/pneumonia_detection.ipynb
+++ b/jupyter/tensorflow/pneumonia_detection.ipynb
@@ -145,7 +145,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "As you can see above, the translator resizes the image to 224x224 and normalizes the image by dividing by 255 before feeding it into the model. When doing inference, you need to follow the same pre-processing procedure as was used during training. In this case, we need to match the Keras training code. After running prediction, the model outputs probabilities of each class as an [NDArray](https://javadoc.io/doc/ai.djl/api/latest/index.html). We need to tell the predictor to translate it back to classes, namely “Normal” or \"Pneumonia\".\n",
+    "As you can see above, the translator resizes the image to 224x224 and normalizes the image by dividing by 255 before feeding it into the model. When doing inference, you need to follow the same pre-processing procedure as was used during training. In this case, we need to match the Keras training code. After running prediction, the model outputs probabilities of each class as an [NDArray](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/ndarray/NDArray.html). We need to tell the predictor to translate it back to classes, namely “Normal” or \"Pneumonia\".\n",
     "\n",
     "Until this point, all preparation work is done, we can start working on the prediction logic."
    ]
diff --git a/jupyter/tensorflow/rank_classification_using_BERT_on_Amazon_Review.ipynb b/jupyter/tensorflow/rank_classification_using_BERT_on_Amazon_Review.ipynb
index 075b8e9e8..90fbbb06b 100644
--- a/jupyter/tensorflow/rank_classification_using_BERT_on_Amazon_Review.ipynb
+++ b/jupyter/tensorflow/rank_classification_using_BERT_on_Amazon_Review.ipynb
@@ -120,15 +120,15 @@
     "Inference in deep learning is the process of predicting the output for a given input based on a pre-defined model.\n",
     "DJL abstracts away the whole process for ease of use. It can load the model, perform inference on the input, and provide output.\n",
     "\n",
-    "The `Translator` interface is used to: Pre-processing and Post-processing. The pre-processing\n",
-    "component converts the user-defined input objects into an NDList, so that the `Predictor` in DJL can understand the\n",
+    "The [`Translator`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/translate/Translator.html) interface is used to: Pre-processing and Post-processing. The pre-processing\n",
+    "component converts the user-defined input objects into an NDList, so that the [`Predictor`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html) in DJL can understand the\n",
     "input and make its prediction. Similarly, the post-processing block receives an NDList as the output from the\n",
     "`Predictor`. The post-processing block allows you to convert the output from the `Predictor` to the desired output\n",
     "format.\n",
     "\n",
     "### Pre-processing\n",
     "\n",
-    "Now, you need to convert the sentences into tokens. We provide a powerful tool `BertTokenizer` that you can use to convert questions and answers into tokens, and batchify your sequence together. Once you have properly formatted tokens, you can use `Vocabulary` to map your token to BERT index.\n",
+    "Now, you need to convert the sentences into tokens. We provide a powerful tool [`BertTokenizer`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/modality/nlp/bert/BertTokenizer.html) that you can use to convert questions and answers into tokens, and batchify your sequence together. Once you have properly formatted tokens, you can use [`Vocabulary`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/modality/nlp/Vocabulary.html) to map your token to BERT index.\n",
     "\n",
     "The following code block demonstrates tokenizing the question and answer defined earlier into BERT-formatted tokens.\n",
     "\n",
diff --git a/jupyter/tutorial/01_create_your_first_network.ipynb b/jupyter/tutorial/01_create_your_first_network.ipynb
index 55b62459d..2cfb35d32 100644
--- a/jupyter/tutorial/01_create_your_first_network.ipynb
+++ b/jupyter/tutorial/01_create_your_first_network.ipynb
@@ -53,7 +53,7 @@
     "\n",
     "## Application\n",
     "\n",
-    "The first thing to figure out when trying to build a neural network, like building most functions, is what your function signature is. What are your input types and output types? Because most models use relatively consistent signatures, we refer to them as [Applications](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/Application.html). Within the Applications interface, you can find a list of some of the more common model applications used in deep learning.\n",
+    "The first thing to figure out when trying to build a neural network, like building most functions, is what your function signature is. What are your input types and output types? Because most models use relatively consistent signatures, we refer to them as [Applications](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/Application.html). Within the Applications interface, you can find a list of some of the more common model applications used in deep learning.\n",
     "\n",
     "In this tutorial, we will focus on the image classification application. It is one of the most common first applications and has a significant history with deep learning. In image classification, the input is a single image and it is classified based on the main subject of the image into a number of different possible classes. The classes for the image depend on the specific data you are training with."
    ]
@@ -83,7 +83,7 @@
     "\n",
     "![Mnist Image](https://upload.wikimedia.org/wikipedia/commons/2/27/MnistExamples.png)\n",
     "\n",
-    "Once you understand your dataset, you should create an implementation of the [Dataset class](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/training/dataset/Dataset.html). In this case, we provide the MNIST dataset built-in to make it easy for you to use it.\n",
+    "Once you understand your dataset, you should create an implementation of the [Dataset class](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/dataset/Dataset.html). In this case, we provide the MNIST dataset built-in to make it easy for you to use it.\n",
     "\n",
     "## Multilayer Perceptron\n",
     "\n",
@@ -123,16 +123,16 @@
     "\n",
     "### NDArray\n",
     "\n",
-    "The core data type used for working with deep learning is the [NDArray](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/ndarray/NDArray.html). An NDArray represents a multidimensional, fixed-size homogeneous array. It has very similar behavior to the Numpy python package with the addition of efficient computing. We also have a helper class, the [NDList](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/ndarray/NDList.html) which is a list of NDArrays which can have different sizes and data types.\n",
+    "The core data type used for working with deep learning is the [NDArray](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/ndarray/NDArray.html). An NDArray represents a multidimensional, fixed-size homogeneous array. It has very similar behavior to the Numpy python package with the addition of efficient computing. We also have a helper class, the [NDList](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/ndarray/NDList.html) which is a list of NDArrays which can have different sizes and data types.\n",
     "\n",
     "### Block API\n",
     "\n",
-    "In DJL, [Blocks](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/nn/Block.html) serve a purpose similar to functions that convert an input `NDList` to an output `NDList`. They can represent single operations, parts of a neural network, and even the whole neural network. What makes blocks special is that they contain a number of parameters that are used in their function and are trained during deep learning. As these parameters are trained, the function represented by the blocks get more and more accurate.\n",
+    "In DJL, [Blocks](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/nn/Block.html) serve a purpose similar to functions that convert an input `NDList` to an output `NDList`. They can represent single operations, parts of a neural network, and even the whole neural network. What makes blocks special is that they contain a number of parameters that are used in their function and are trained during deep learning. As these parameters are trained, the function represented by the blocks get more and more accurate.\n",
     "\n",
     "When building these block functions, the easiest way is to use composition. Similar to how functions are built by calling other functions, blocks can be built by combining other blocks. We refer to the containing block as the parent and the sub-blocks as the children.\n",
     "\n",
     "\n",
-    "We provide several helpers to make it easy to build common block composition structures. For the MLP we will use the [SequentialBlock](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/nn/SequentialBlock.html), a container block whose children form a chain of blocks where each child block feeds its output to the next child block in a sequence.\n",
+    "We provide several helpers to make it easy to build common block composition structures. For the MLP we will use the [SequentialBlock](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/nn/SequentialBlock.html), a container block whose children form a chain of blocks where each child block feeds its output to the next child block in a sequence.\n",
     " "
    ]
   },
@@ -151,7 +151,7 @@
    "source": [
     "## Step 4: Add blocks to SequentialBlock\n",
     "\n",
-    "An MLP is organized into several layers. Each layer is composed of a [Linear Block](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/nn/core/Linear.html) and a non-linear activation function. If we just had two linear blocks in a row, it would be the same as a combined linear block ($f(x) = W_2(W_1x) = (W_2W_1)x = W_{combined}x$). An activation is used to intersperse between the linear blocks to allow them to represent non-linear functions. We will use the popular [ReLU](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/nn/Activation.html#reluBlock--) as our activation function.\n",
+    "An MLP is organized into several layers. Each layer is composed of a [Linear Block](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/nn/core/Linear.html) and a non-linear activation function. If we just had two linear blocks in a row, it would be the same as a combined linear block ($f(x) = W_2(W_1x) = (W_2W_1)x = W_{combined}x$). An activation is used to intersperse between the linear blocks to allow them to represent non-linear functions. We will use the popular [ReLU](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/nn/Activation.html#reluBlock()) as our activation function.\n",
     "\n",
     "The first layer and last layers have fixed sizes depending on your desired input and output size. However, you are free to choose the number and sizes of the middle layers in the network. We will create a smaller MLP with two middle layers that gradually decrease the size. Typically, you would experiment with different values to see what works the best on your data set."
    ]
diff --git a/jupyter/tutorial/02_train_your_first_model.ipynb b/jupyter/tutorial/02_train_your_first_model.ipynb
index e7d5972d8..74925719a 100644
--- a/jupyter/tutorial/02_train_your_first_model.ipynb
+++ b/jupyter/tutorial/02_train_your_first_model.ipynb
@@ -59,13 +59,13 @@
    "source": [
     "# Step 1: Prepare MNIST dataset for training\n",
     "\n",
-    "In order to train, you must create a [Dataset class](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/training/dataset/Dataset.html) to contain your training data. A dataset is a collection of sample input/output pairs for the function represented by your neural network. Each single input/output is represented by a [Record](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/training/dataset/Record.html). Each record could have multiple arrays of inputs or outputs such as an image question and answer dataset where the input is both an image and a question about the image while the output is the answer to the question.\n",
+    "In order to train, you must create a [Dataset class](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/dataset/Dataset.html) to contain your training data. A dataset is a collection of sample input/output pairs for the function represented by your neural network. Each single input/output is represented by a [Record](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/dataset/Record.html). Each record could have multiple arrays of inputs or outputs such as an image question and answer dataset where the input is both an image and a question about the image while the output is the answer to the question.\n",
     "\n",
-    "Because data learning is highly parallelizable, training is often done not with a single record at a time, but a [Batch](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/training/dataset/Batch.html). This can lead to significant performance gains, especially when working with images\n",
+    "Because data learning is highly parallelizable, training is often done not with a single record at a time, but a [Batch](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/dataset/Batch.html). This can lead to significant performance gains, especially when working with images\n",
     "\n",
     "## Sampler\n",
     "\n",
-    "Then, we must decide the parameters for loading data from the dataset. The only parameter we need for MNIST is the choice of [Sampler](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/training/dataset/Sampler.html). The sampler decides which and how many element from datasets are part of each batch when iterating through it. We will have it randomly shuffle the elements for the batch and use a batchSize of 32. The batchSize is usually the largest power of 2 that fits within memory."
+    "Then, we must decide the parameters for loading data from the dataset. The only parameter we need for MNIST is the choice of [Sampler](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/dataset/Sampler.html). The sampler decides which and how many element from datasets are part of each batch when iterating through it. We will have it randomly shuffle the elements for the batch and use a batchSize of 32. The batchSize is usually the largest power of 2 that fits within memory."
    ]
   },
   {
@@ -85,7 +85,7 @@
    "source": [
     "# Step 2: Create your Model\n",
     "\n",
-    "Next we will build a model. A [Model](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/Model.html) contains a neural network [Block](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/nn/Block.html) along with additional artifacts used for the training process. It possesses additional information about the inputs, outputs, shapes, and data types you will use. Generally, you will use the Model once you have fully completed your Block.\n",
+    "Next we will build a model. A [Model](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/Model.html) contains a neural network [Block](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/nn/Block.html) along with additional artifacts used for the training process. It possesses additional information about the inputs, outputs, shapes, and data types you will use. Generally, you will use the Model once you have fully completed your Block.\n",
     "\n",
     "In this part of the tutorial, we will use the built-in Multilayer Perceptron Block from the Model Zoo. To learn how to build it from scratch, see the previous tutorial: [Create Your First Network](01_create_your_first_network.ipynb).\n",
     "\n",
@@ -108,21 +108,21 @@
    "source": [
     "# Step 3: Create a Trainer\n",
     "\n",
-    "Now, you can create a [`Trainer`](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/training/Trainer.html) to train your model. The trainer is the main class to orchestrate the training process. Usually, they will be opened using a try-with-resources and closed after training is over.\n",
+    "Now, you can create a [`Trainer`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/Trainer.html) to train your model. The trainer is the main class to orchestrate the training process. Usually, they will be opened using a try-with-resources and closed after training is over.\n",
     "\n",
     "The trainer takes an existing model and attempts to optimize the parameters inside the model's Block to best match the dataset. Most optimization is based upon [Stochastic Gradient Descent](https://en.wikipedia.org/wiki/Stochastic_gradient_descent) (SGD).\n",
     "\n",
     "## Step 3.1: Setup your training configurations\n",
     "\n",
-    "Before you create your trainer, we we will need a [training configuration](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/training/DefaultTrainingConfig.html) that describes how to train your model.\n",
+    "Before you create your trainer, we we will need a [training configuration](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/DefaultTrainingConfig.html) that describes how to train your model.\n",
     "\n",
     "The following are a few common items you may need to configure your training:\n",
     "\n",
-    "* **REQUIRED** [`Loss`](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/training/loss/Loss.html) function: A loss function is used to measure how well our model matches the dataset. Because the lower value of the function is better, it's called the \"loss\" function. The Loss is the only required argument to the model\n",
-    "* [`Evaluator`](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/training/evaluator/Evaluator.html) function: An evaluator function is also used to measure how well our model matches the dataset. Unlike the loss, they are only there for people to look at and are not used for optimizing the model. Since many losses are not as intuitive, adding other evaluators such as Accuracy can help to understand how your model is doing. If you know of any useful evaluators, we recommend adding them.\n",
-    "* [`Training Listeners`](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/repository/zoo/ZooModel.html): The training listener adds additional functionality to the training process through a listener interface. This can include showing training progress, stopping early if training becomes undefined, or recording performance metrics. We offer several easy sets of [default listeners](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/repository/zoo/ZooModel.html).\n",
+    "* **REQUIRED** [`Loss`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/loss/Loss.html) function: A loss function is used to measure how well our model matches the dataset. Because the lower value of the function is better, it's called the \"loss\" function. The Loss is the only required argument to the model\n",
+    "* [`Evaluator`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/evaluator/Evaluator.html) function: An evaluator function is also used to measure how well our model matches the dataset. Unlike the loss, they are only there for people to look at and are not used for optimizing the model. Since many losses are not as intuitive, adding other evaluators such as Accuracy can help to understand how your model is doing. If you know of any useful evaluators, we recommend adding them.\n",
+    "* [`Training Listeners`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/listener/TrainingListener.html): The training listener adds additional functionality to the training process through a listener interface. This can include showing training progress, stopping early if training becomes undefined, or recording performance metrics. We offer several easy sets of [default listeners](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/listener/TrainingListener.Defaults.html).\n",
     "\n",
-    "You can also configure other options such as the Device, Initializer, and Optimizer. See [more details](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/training/TrainingConfig.html)."
+    "You can also configure other options such as the Device, Initializer, and Optimizer. See [more details](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/training/TrainingConfig.html)."
    ]
   },
   {
@@ -171,7 +171,7 @@
     "\n",
     "When training, it is usually organized into epochs where each epoch trains the model on each item in the dataset once. It is slightly faster than training randomly.\n",
     "\n",
-    "Then, we will use the EasyTrain to, as the name promises, make the training easy. If you want to see more details about how the training loop works, see [the EasyTrain class](https://github.com/deepjavalibrary/djl/blob/0.9/api/src/main/java/ai/djl/training/EasyTrain.java) or [read our Dive into Deep Learning book](https://d2l.djl.ai)."
+    "Then, we will use the EasyTrain to, as the name promises, make the training easy. If you want to see more details about how the training loop works, see [the EasyTrain class](https://github.com/deepjavalibrary/djl/blob/master/api/src/main/java/ai/djl/training/EasyTrain.java) or [read our Dive into Deep Learning book](https://d2l.djl.ai)."
    ]
   },
   {
diff --git a/jupyter/tutorial/03_image_classification_with_your_model.ipynb b/jupyter/tutorial/03_image_classification_with_your_model.ipynb
index 587aeea87..e77fb9503 100644
--- a/jupyter/tutorial/03_image_classification_with_your_model.ipynb
+++ b/jupyter/tutorial/03_image_classification_with_your_model.ipynb
@@ -100,7 +100,7 @@
     "\n",
     "## Step 3: Create a `Translator`\n",
     "\n",
-    "The [`Translator`](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/translate/Translator.html) is used to encapsulate the pre-processing and post-processing functionality of your application. The input to the processInput and processOutput should be single data items, not batches."
+    "The [`Translator`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/translate/Translator.html) is used to encapsulate the pre-processing and post-processing functionality of your application. The input to the processInput and processOutput should be single data items, not batches."
    ]
   },
   {
@@ -141,7 +141,7 @@
    "source": [
     "## Step 4: Create Predictor\n",
     "\n",
-    "Using the translator, we will create a new [`Predictor`](https://javadoc.io/static/ai.djl/api/0.17.0/index.html?ai/djl/inference/Predictor.html). The predictor is the main class to orchestrate the inference process. During inference, a trained model is used to predict values, often for production use cases. The predictor is NOT thread-safe, so if you want to do prediction in parallel, you should call newPredictor multiple times to create a predictor object for each thread."
+    "Using the translator, we will create a new [`Predictor`](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html). The predictor is the main class to orchestrate the inference process. During inference, a trained model is used to predict values, often for production use cases. The predictor is NOT thread-safe, so if you want to do prediction in parallel, you should call newPredictor multiple times to create a predictor object for each thread."
    ]
   },
   {
@@ -159,7 +159,7 @@
    "source": [
     "## Step 5: Run inference\n",
     "\n",
-    "With our predictor, we can simply call the [predict](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html#predict-I-) method to run inference. For better performance, you can also call [batchPredict](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html#batchPredict-java.util.List-) with a list of input items. Afterwards, the same predictor should be used for further inference calls. "
+    "With our predictor, we can simply call the [predict](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html#predict(I)) method to run inference. For better performance, you can also call [batchPredict](https://javadoc.io/static/ai.djl/api/0.17.0/ai/djl/inference/Predictor.html#batchPredict(java.util.List)) with a list of input items. Afterwards, the same predictor should be used for further inference calls. "
    ]
   },
   {
diff --git a/website/demo.html b/website/demo.html
index 2eb4b3d6f..ab93b7fa6 100644
--- a/website/demo.html
+++ b/website/demo.html
@@ -72,7 +72,7 @@
                     <h5 class="header col s12 light">Experience Java to solve your DL problem in a simple way.</h5>
                 </div>
                 <div class="row center">
-                    <a href="https://github.com/aws-samples/djl-demo"
+                    <a href="https://github.com/deepjavalibrary/djl-demo"
                     class="btn-large waves-effect waves-light light-blue accent-2">Checkout our Demos</a>
                     <a href="https://docs.djl.ai/docs/demos/index.html"
                     class="btn-large waves-effect waves-light light-blue accent-2">Documentation</a>
@@ -197,7 +197,7 @@
                         <h5 class="header col s12 light">Want to know more?</h5>
                     </div>
                     <div class="row center">
-                        <a href="https://github.com/aws-samples/djl-demo"
+                        <a href="https://github.com/deepjavalibrary/djl-demo"
                         class="btn-large waves-effect waves-light light-blue accent-2 pulse">Checkout our demo repository</a>
                     </div>
                     <div class="row center">


Test patch:
diff --git a/android/core/src/androidTest/java/ai/djl/android/core/ModelLoadingTest.java b/android/core/src/androidTest/java/ai/djl/android/core/ModelLoadingTest.java
new file mode 100644
index 000000000..1ae1b73a9
--- /dev/null
+++ b/android/core/src/androidTest/java/ai/djl/android/core/ModelLoadingTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.android.core;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import ai.djl.ModelException;
+import ai.djl.inference.Predictor;
+import ai.djl.modality.Classifications;
+import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.ImageFactory;
+import ai.djl.modality.cv.transform.ToTensor;
+import ai.djl.modality.cv.translator.ImageClassificationTranslator;
+import ai.djl.repository.zoo.Criteria;
+import ai.djl.repository.zoo.ModelZoo;
+import ai.djl.repository.zoo.ZooModel;
+import ai.djl.translate.TranslateException;
+
+/* Integration test to check if model and inference runs fine */
+public class ModelLoadingTest {
+
+    @Test
+    public void testModelLoading() throws IOException, ModelException, TranslateException {
+        ImageClassificationTranslator translator = ImageClassificationTranslator.builder()
+                .addTransform(new ToTensor())
+                .optFlag(Image.Flag.GRAYSCALE)
+                .optApplySoftmax(true)
+                .build();
+        Criteria<Image, Classifications> criteria =
+                Criteria.builder()
+                        .setTypes(Image.class, Classifications.class)
+                        .optModelUrls("https://djl-ai.s3.amazonaws.com/resources/demo/pytorch/doodle_mobilenet.zip")
+                        .optTranslator(translator)
+                        .build();
+        Image image = ImageFactory.getInstance().fromUrl("https://resources.djl.ai/images/0.png");
+        try (ZooModel<Image, Classifications> model = ModelZoo.loadModel(criteria);
+             Predictor<Image, Classifications> predictor = model.newPredictor()) {
+            Classifications result = predictor.predict(image);
+            Assert.assertEquals("dog", result.best().getClassName());
+        }
+    }
+}
diff --git a/basicdataset/src/test/java/ai/djl/basicdataset/MovieLens100kTest.java b/basicdataset/src/test/java/ai/djl/basicdataset/MovieLens100kTest.java
new file mode 100644
index 000000000..9560ee5e1
--- /dev/null
+++ b/basicdataset/src/test/java/ai/djl/basicdataset/MovieLens100kTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package ai.djl.basicdataset;
+
+import ai.djl.Model;
+import ai.djl.basicdataset.tabular.MovieLens100k;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.nn.Blocks;
+import ai.djl.nn.Parameter;
+import ai.djl.training.DefaultTrainingConfig;
+import ai.djl.training.Trainer;
+import ai.djl.training.TrainingConfig;
+import ai.djl.training.dataset.Batch;
+import ai.djl.training.dataset.Dataset;
+import ai.djl.training.dataset.Record;
+import ai.djl.training.initializer.Initializer;
+import ai.djl.training.loss.Loss;
+import ai.djl.translate.TranslateException;
+import java.io.IOException;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class MovieLens100kTest {
+
+    @Test
+    public void testMovieLens100kRemote() throws IOException, TranslateException {
+        TrainingConfig config =
+                new DefaultTrainingConfig(Loss.l2Loss())
+                        .optInitializer(Initializer.ONES, Parameter.Type.WEIGHT);
+
+        try (Model model = Model.newInstance("model")) {
+            model.setBlock(Blocks.identityBlock());
+
+            NDManager manager = model.getNDManager();
+
+            MovieLens100k movieLens100k =
+                    MovieLens100k.builder()
+                            .optUsage(Dataset.Usage.TEST)
+                            .addFeature("user_age")
+                            .addFeature("user_gender")
+                            .addFeature("user_occupation")
+                            .addFeature("movie_genres")
+                            .setSampling(32, true)
+                            .build();
+
+            movieLens100k.prepare();
+
+            long size = movieLens100k.size();
+            Assert.assertEquals(size, 9430);
+
+            Record record = movieLens100k.get(manager, 23);
+            NDList data = record.getData();
+            NDList labels = record.getLabels();
+
+            Assert.assertEquals(
+                    data.head().toFloatArray(),
+                    new float[] {
+                        23.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
+                        0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
+                        0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
+                        0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,
+                    });
+            Assert.assertEquals(labels.head().toFloatArray(), new float[] {5.0f});
+
+            try (Trainer trainer = model.newTrainer(config)) {
+                Batch batch = trainer.iterateDataset(movieLens100k).iterator().next();
+                Assert.assertEquals(batch.getData().size(), 1);
+                Assert.assertEquals(batch.getLabels().size(), 1);
+                batch.close();
+            }
+        }
+    }
+}
diff --git a/basicdataset/src/test/resources/mlrepo/dataset/tabular/ai/djl/basicdataset/movielens-100k/metadata.json b/basicdataset/src/test/resources/mlrepo/dataset/tabular/ai/djl/basicdataset/movielens-100k/metadata.json
new file mode 100644
index 000000000..2728d25cf
--- /dev/null
+++ b/basicdataset/src/test/resources/mlrepo/dataset/tabular/ai/djl/basicdataset/movielens-100k/metadata.json
@@ -0,0 +1,29 @@
+{
+  "metadataVersion": "0.2",
+  "resourceType": "dataset",
+  "application": "tabular",
+  "groupId": "ai.djl.basicdataset",
+  "artifactId": "movielens-100k",
+  "name": "movielens-100k",
+  "description": "Movie Ratings from 1000 Users on 1700 movies",
+  "website": "https://grouplens.org/datasets/movielens/100k/",
+  "licenses": {
+    "license": {
+      "name": "MovieLens License",
+      "url": "https://files.grouplens.org/datasets/movielens/ml-100k-README.txt"
+    }
+  },
+  "artifacts": [
+    {
+      "version": "1.0",
+      "snapshot": false,
+      "files": {
+        "ml-100k": {
+          "uri": "https://files.grouplens.org/datasets/movielens/ml-100k.zip",
+          "sha1Hash": "cd4dcac4241c8a4ad7badc7ca635da8a69dddb83",
+          "size": "4924029"
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/integration/src/main/java/ai/djl/integration/tests/nn/BlockCoreTest.java b/integration/src/main/java/ai/djl/integration/tests/nn/BlockCoreTest.java
index a30f8f382..83afde91f 100644
--- a/integration/src/main/java/ai/djl/integration/tests/nn/BlockCoreTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/nn/BlockCoreTest.java
@@ -21,6 +21,7 @@ import ai.djl.modality.nlp.embedding.TrainableWordEmbedding;
 import ai.djl.ndarray.NDArray;
 import ai.djl.ndarray.NDList;
 import ai.djl.ndarray.NDManager;
+import ai.djl.ndarray.types.DataType;
 import ai.djl.ndarray.types.LayoutType;
 import ai.djl.ndarray.types.Shape;
 import ai.djl.nn.Block;
@@ -34,6 +35,7 @@ import ai.djl.nn.convolutional.Conv2d;
 import ai.djl.nn.convolutional.Conv2dTranspose;
 import ai.djl.nn.convolutional.Conv3d;
 import ai.djl.nn.core.Linear;
+import ai.djl.nn.core.LinearCollection;
 import ai.djl.nn.norm.BatchNorm;
 import ai.djl.nn.norm.Dropout;
 import ai.djl.nn.norm.GhostBatchNorm;
@@ -57,6 +59,7 @@ import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.util.Arrays;
+import java.util.stream.IntStream;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -177,6 +180,165 @@ public class BlockCoreTest {
         }
     }
 
+    @Test
+    public void testLinearCollection() throws IOException, MalformedModelException {
+
+        // used to initialize both weight and bias
+        Initializer initializer =
+                (m, s, t) -> m.arange(0, s.size(), 1, DataType.FLOAT32).reshape(s);
+        TrainingConfig config =
+                new DefaultTrainingConfig(Loss.l2Loss())
+                        .optInitializer(initializer, Parameter.Type.WEIGHT)
+                        .optInitializer(initializer, Parameter.Type.BIAS);
+        int featureSize = 5;
+        int outSize = 7;
+        int batchSize = 11;
+        // number of linear transformations modeled by LinearCollection
+        int splitSize = 6;
+
+        // following ndarrays will be scoped to modelLinearCollection's ndmanager so they can be
+        // used later in the for-loop
+        NDArray expectedWeightForLinearCollection;
+        NDArray expectedBiasForLinearCollection;
+        NDArray expectedLabelsForLinearCollection;
+        NDArray dataForLinearCollection;
+
+        try (NDManager sharedManager = NDManager.newBaseManager()) {
+
+            // test expected output when using LinearCollection
+            try (Model model = Model.newInstance("model")) {
+                Block block = LinearCollection.builder().setUnits(outSize).build();
+                model.setBlock(block);
+
+                try (Trainer trainer = model.newTrainer(config)) {
+                    Shape inputShape = new Shape(batchSize, splitSize, featureSize);
+                    trainer.initialize(inputShape);
+
+                    NDManager manager = trainer.getManager();
+                    dataForLinearCollection =
+                            sharedManager.arange(inputShape.size()).reshape(inputShape);
+                    NDArray actualLabels =
+                            trainer.forward(new NDList(dataForLinearCollection)).singletonOrThrow();
+                    NDArray actualWeight = block.getParameters().get("weight").getArray();
+                    NDArray actualBias = block.getParameters().get("bias").getArray();
+                    expectedWeightForLinearCollection =
+                            initializer.initialize(
+                                    sharedManager,
+                                    new Shape(splitSize, featureSize, outSize),
+                                    DataType.FLOAT32);
+                    expectedBiasForLinearCollection =
+                            initializer.initialize(
+                                    sharedManager, new Shape(splitSize, outSize), DataType.FLOAT32);
+                    Assert.assertEquals(expectedWeightForLinearCollection, actualWeight);
+                    Assert.assertEquals(expectedBiasForLinearCollection, actualBias);
+                    expectedLabelsForLinearCollection =
+                            dataForLinearCollection
+                                    .transpose(1, 0, 2)
+                                    .matMul(expectedWeightForLinearCollection)
+                                    .transpose(1, 0, 2)
+                                    .add(expectedBiasForLinearCollection);
+                    Assert.assertEquals(expectedLabelsForLinearCollection, actualLabels);
+
+                    testEncode(manager, block);
+                }
+            }
+
+            // test expected output can be reproduced by using splitSize-many Linear blocks
+            for (int splitIndex : IntStream.range(0, splitSize).toArray()) {
+                try (Model model = Model.newInstance("model")) {
+                    Block block = Linear.builder().setUnits(outSize).build();
+                    model.setBlock(block);
+
+                    try (Trainer trainer = model.newTrainer(config)) {
+
+                        // copy initial weight/bias values from LinearCollection
+                        Shape inputShape = new Shape(batchSize, featureSize);
+                        block.setInitializer(
+                                (m, s, t) -> {
+                                    NDArray w =
+                                            expectedWeightForLinearCollection
+                                                    .get(splitIndex)
+                                                    .transpose();
+                                    w.attach(m);
+                                    return w;
+                                },
+                                Parameter.Type.WEIGHT);
+                        block.setInitializer(
+                                (m, s, t) -> {
+                                    NDArray w = expectedBiasForLinearCollection.get(splitIndex);
+                                    w.attach(m);
+                                    return w;
+                                },
+                                Parameter.Type.BIAS);
+                        trainer.initialize(inputShape);
+
+                        NDManager manager = trainer.getManager();
+                        NDArray data = dataForLinearCollection.transpose(1, 0, 2).get(splitIndex);
+                        NDArray actualLabels = trainer.forward(new NDList(data)).singletonOrThrow();
+                        NDArray actualWeight = block.getParameters().get("weight").getArray();
+                        NDArray actualBias = block.getParameters().get("bias").getArray();
+                        NDArray expectedWeight =
+                                expectedWeightForLinearCollection.get(splitIndex).transpose();
+                        NDArray expectedBias = expectedBiasForLinearCollection.get(splitIndex);
+                        Assert.assertEquals(expectedWeight, actualWeight);
+                        Assert.assertEquals(expectedBias, actualBias);
+                        NDArray expectedLabels =
+                                data.dot(expectedWeight.transpose()).add(expectedBias);
+                        Assert.assertEquals(expectedLabels, actualLabels);
+                        Assert.assertEquals(
+                                expectedLabelsForLinearCollection
+                                        .transpose(1, 0, 2)
+                                        .get(splitIndex),
+                                actualLabels);
+
+                        testEncode(manager, block);
+                    }
+                }
+            }
+
+            // test expected output when using multiple split axis
+            try (Model model = Model.newInstance("model")) {
+                Block block = LinearCollection.builder().setUnits(outSize).build();
+                model.setBlock(block);
+
+                try (Trainer trainer = model.newTrainer(config)) {
+                    long[] splitAxis = {3, 1, 2};
+                    Assert.assertEquals(splitSize, new Shape(splitAxis).size());
+                    Shape inputShape = new Shape(batchSize).add(splitAxis).add(featureSize);
+                    trainer.initialize(inputShape);
+
+                    NDManager manager = trainer.getManager();
+                    NDArray data = dataForLinearCollection.reshape(inputShape);
+                    NDArray actualLabels = trainer.forward(new NDList(data)).singletonOrThrow();
+                    NDArray actualWeight = block.getParameters().get("weight").getArray();
+                    NDArray actualBias = block.getParameters().get("bias").getArray();
+                    NDArray expectedWeight =
+                            initializer.initialize(
+                                    manager,
+                                    new Shape(splitAxis).add(featureSize, outSize),
+                                    DataType.FLOAT32);
+                    NDArray expectedBias =
+                            initializer.initialize(
+                                    manager, new Shape(splitAxis).add(outSize), DataType.FLOAT32);
+                    Assert.assertEquals(expectedWeight, actualWeight);
+                    Assert.assertEquals(expectedBias, actualBias);
+                    NDArray expectedLabels =
+                            data.transpose(1, 2, 3, 0, 4)
+                                    .matMul(expectedWeight)
+                                    .transpose(3, 0, 1, 2, 4)
+                                    .add(expectedBias);
+                    Assert.assertEquals(expectedLabels, actualLabels);
+                    Assert.assertEquals(
+                            expectedLabelsForLinearCollection.reshape(
+                                    new Shape(batchSize).add(splitAxis).add(outSize)),
+                            actualLabels);
+
+                    testEncode(manager, block);
+                }
+            }
+        }
+    }
+
     @SuppressWarnings("try")
     @Test
     public void testBatchNorm() throws IOException, MalformedModelException {