Repository: deepjavalibrary/djl
Pull request #1683 (2022-05-29 09:10:23)

Added "capped" state to NDManager
This change adds the capability to "cap" a NDManager instance.
In the capped state, it is not possible to add new resources to the manager (except for submanagers).
The model developer would choose to do so when he is sure that all resources (usually NDArray) that are supposed to be attached are attached and he expects no new resources to be added in the future, thus preventing unintentional addition of resources that could go undetected otherwise and could pose memory leaks.
Sub managers are by design still allowed to be attached and have an independent "cap" status. 


codecov-commenter: # [Codecov](https://codecov.io/gh/deepjavalibrary/djl/pull/1683?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) Report
> Merging [#1683](https://codecov.io/gh/deepjavalibrary/djl/pull/1683?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) (599f516) into [master](https://codecov.io/gh/deepjavalibrary/djl/commit/bb5073f7ffc33f1883621fc1783dbb01e70b418f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) (bb5073f) will **decrease** coverage by `1.40%`.
> The diff coverage is `63.21%`.

```diff
@@             Coverage Diff              @@
##             master    #1683      +/-   ##
============================================
- Coverage     72.08%   70.68%   -1.41%     
- Complexity     5126     5454     +328     
============================================
  Files           473      510      +37     
  Lines         21970    24030    +2060     
  Branches       2351     2617     +266     
============================================
+ Hits          15838    16986    +1148     
- Misses         4925     5733     +808     
- Partials       1207     1311     +104     
```


| [Impacted Files](https://codecov.io/gh/deepjavalibrary/djl/pull/1683?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) | Coverage Δ | |
|---|---|---|
| [api/src/main/java/ai/djl/modality/cv/Image.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L0ltYWdlLmphdmE=) | `69.23% <ø> (-4.11%)` | :arrow_down: |
| [...rc/main/java/ai/djl/modality/cv/output/Joints.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L291dHB1dC9Kb2ludHMuamF2YQ==) | `71.42% <ø> (ø)` | |
| [.../main/java/ai/djl/modality/cv/output/Landmark.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L291dHB1dC9MYW5kbWFyay5qYXZh) | `100.00% <ø> (ø)` | |
| [...main/java/ai/djl/modality/cv/output/Rectangle.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L291dHB1dC9SZWN0YW5nbGUuamF2YQ==) | `72.41% <ø> (ø)` | |
| [...i/djl/modality/cv/translator/BigGANTranslator.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvQmlnR0FOVHJhbnNsYXRvci5qYXZh) | `21.42% <ø> (-5.24%)` | :arrow_down: |
| [...odality/cv/translator/BigGANTranslatorFactory.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvQmlnR0FOVHJhbnNsYXRvckZhY3RvcnkuamF2YQ==) | `33.33% <0.00%> (+8.33%)` | :arrow_up: |
| [...nslator/InstanceSegmentationTranslatorFactory.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvSW5zdGFuY2VTZWdtZW50YXRpb25UcmFuc2xhdG9yRmFjdG9yeS5qYXZh) | `14.28% <0.00%> (-3.90%)` | :arrow_down: |
| [.../ai/djl/modality/cv/translator/YoloTranslator.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvWW9sb1RyYW5zbGF0b3IuamF2YQ==) | `8.33% <0.00%> (-0.50%)` | :arrow_down: |
| [.../modality/cv/translator/YoloTranslatorFactory.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvWW9sb1RyYW5zbGF0b3JGYWN0b3J5LmphdmE=) | `8.33% <0.00%> (-1.67%)` | :arrow_down: |
| [...i/djl/modality/cv/translator/YoloV5Translator.java](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary#diff-YXBpL3NyYy9tYWluL2phdmEvYWkvZGpsL21vZGFsaXR5L2N2L3RyYW5zbGF0b3IvWW9sb1Y1VHJhbnNsYXRvci5qYXZh) | `5.38% <0.00%> (-0.31%)` | :arrow_down: |
| ... and [251 more](https://codecov.io/gh/deepjavalibrary/djl/pull/1683/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/deepjavalibrary/djl/pull/1683?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/deepjavalibrary/djl/pull/1683?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary). Last update [3c4262d...599f516](https://codecov.io/gh/deepjavalibrary/djl/pull/1683?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deepjavalibrary).

patins1: I see two use cases for `NDManager.cap()`, once I call it after i set up the features/labels with a toplevel manager (a), and once i call it on the model's manager (b) just before I do the training (so after the model's parameters are created).  I would even argue that it would be useful that EasyTrain.fit() - as it is a standardized training loop - should call `NDManager.cap()` for both managers just at the start because EasyTrain.fit() is not supposed to attach any new resources to any of those two managers.  

My current pull request works only for PyTorch, so I was experimenting with MXNet as well and found one issue for both engines: the indexing operation used to assemble batches from the input features currently attaches the extracted data first to the manager (a) before being attached to the trainer's manager. This slows down the batching algorithm by some factor, I want to create a new pull request just addressing this issue. Also, I found some bugs in there that temporary attached resources are actually returned to a different original manager, I am gonna write a test to verify.
  
> @patins1 This is a great initiative. However, I don't think this easy to use. The biggest challenge is when to call `NDManager.cap()` and which `NDManager` instance is causing leak.

frankfliu: @patins1 
The leak can happen at different levels, Model level, ParameterStore level or Predictor level. Set `capped` won't detect leak at subNDManager. To detect memory leak, figure out which NDManager causing leaking is more important.

The solution I proposed is to set a global watermark which applies to all NDManagers, if any NDManager breach the threshold, we can through exception and call `BaseNDManager.debugDump()` to print out the entire resource tree. And we can quickly identify where the leak happen.
patins1: I fully understand the global watermark solution, I see only the problem in setting a watermark not too high so that memory leaks can go unnoticed and not too low that it actually prevents learning when e.g. the number of features increases. Therefore I prefer to use an analytical approach rather than a statistical/holistic approach. I have two other ideas that I would appreciate your feedback on:

1.  `BaseNDManager.resources` is typed as `ConcurrentHashMap`. How about using something similar to `WeakHashMap` (however, we need the values to be week and not the keys). Then the resources would be freed automatically when they are not anymore referenced (other than from `BaseNDManager.resources`)
2. In `EasyTrain.fit()`'s outer for-loop, we could count the total number of allocated `NDArray` after the first iteration, and after any subsequent iteration of the loop we assert that this number has not increased. This approach would share one feature with the watermark solution: determine the total number of `NDArray` instances. Maybe this can be solved by also storing a reference from parent managers to their children in order to recursively calculate the number.



Patch:
diff --git a/android/build.gradle b/android/build.gradle
index 88d1750bb..d469c4627 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -18,6 +18,10 @@ allprojects {
     repositories {
         google()
         mavenCentral()
+        mavenLocal()
+        maven {
+            url "https://oss.sonatype.org/content/repositories/snapshots/"
+        }
     }
 }
 
diff --git a/android/core/build.gradle b/android/core/build.gradle
index acf6c1021..4ab1e3783 100644
--- a/android/core/build.gradle
+++ b/android/core/build.gradle
@@ -27,12 +27,6 @@ android {
     }
 }
 
-repositories {
-    maven {
-        url "https://oss.sonatype.org/content/repositories/snapshots/"
-    }
-}
-
 signing {
     required(project.hasProperty("staging") || project.hasProperty("snapshot"))
     def signingKey = findProperty("signingKey")
diff --git a/android/core/src/main/java/ai/djl/android/core/BitmapImageFactory.java b/android/core/src/main/java/ai/djl/android/core/BitmapImageFactory.java
index 191fcfcd2..f670a4590 100644
--- a/android/core/src/main/java/ai/djl/android/core/BitmapImageFactory.java
+++ b/android/core/src/main/java/ai/djl/android/core/BitmapImageFactory.java
@@ -189,6 +189,13 @@ public class BitmapImageFactory extends ImageFactory {
             }
         }
 
+        /** {@inheritDoc} */
+        @Override
+        public List<BoundingBox> findBoundingBoxes() {
+            // TODO: Add grayscale conversion and use BoundFinder to implement
+            throw new UnsupportedOperationException("Not supported for BitMapImage");
+        }
+
         /** {@inheritDoc} */
         @Override
         public void drawBoundingBoxes(DetectedObjects detections) {
diff --git a/android/gradle.properties b/android/gradle.properties
index 591dba5ba..34e83a5c0 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -17,5 +17,5 @@ org.gradle.jvmargs=-Xmx1536m
 android.useAndroidX=true
 # Automatically convert third-party libraries to use AndroidX
 android.enableJetifier=true
-djl_version=0.17.0
+djl_version=0.18.0
 pytorch_version=1.11.0
diff --git a/android/pytorch-native/gradlew b/android/pytorch-native/gradlew
new file mode 120000
index 000000000..502f5a2d3
--- /dev/null
+++ b/android/pytorch-native/gradlew
@@ -0,0 +1,1 @@
+../gradlew
\ No newline at end of file
diff --git a/api/build.gradle b/api/build.gradle
index 94c2c2e7e..5f16a668e 100644
--- a/api/build.gradle
+++ b/api/build.gradle
@@ -8,7 +8,7 @@ dependencies {
         exclude group: "junit", module: "junit"
     }
     testImplementation "org.slf4j:slf4j-simple:${slf4j_version}"
-    testRuntimeOnly project(":engines:mxnet:mxnet-model-zoo")
+    testRuntimeOnly project(":engines:pytorch:pytorch-model-zoo")
 }
 
 javadoc {
diff --git a/api/src/main/java/ai/djl/ndarray/BaseNDManager.java b/api/src/main/java/ai/djl/ndarray/BaseNDManager.java
index 9bdc47591..3fcae1f36 100644
--- a/api/src/main/java/ai/djl/ndarray/BaseNDManager.java
+++ b/api/src/main/java/ai/djl/ndarray/BaseNDManager.java
@@ -47,6 +47,7 @@ public abstract class BaseNDManager implements NDManager {
     protected ConcurrentHashMap<String, AutoCloseable> resources;
     protected ConcurrentHashMap<String, TempResource> tempResources;
     protected AtomicBoolean closed = new AtomicBoolean(false);
+    protected AtomicBoolean capped = new AtomicBoolean(false);
 
     protected BaseNDManager(NDManager parent, Device device) {
         this.parent = parent;
@@ -235,6 +236,12 @@ public abstract class BaseNDManager implements NDManager {
         return !closed.get();
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void cap() {
+        this.capped.set(true);
+    }
+
     /** {@inheritDoc} */
     @Override
     public NDManager getParentManager() {
@@ -270,6 +277,15 @@ public abstract class BaseNDManager implements NDManager {
     /** {@inheritDoc} */
     @Override
     public synchronized void attachInternal(String resourceId, AutoCloseable resource) {
+        if (capped.get()) {
+            throw new IllegalStateException("NDManager is capped for addition of resources.");
+        }
+        attachUncappedInternal(resourceId, resource);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public synchronized void attachUncappedInternal(String resourceId, AutoCloseable resource) {
         if (closed.get()) {
             throw new IllegalStateException("NDManager has been closed already.");
         }
@@ -452,7 +468,7 @@ public abstract class BaseNDManager implements NDManager {
             try {
                 if (!detached) {
                     if (manager.isOpen()) {
-                        resource.attach(manager);
+                        resource.returnResource(manager);
                     } else {
                         resource.close();
                     }
diff --git a/api/src/main/java/ai/djl/ndarray/NDManager.java b/api/src/main/java/ai/djl/ndarray/NDManager.java
index 935f820fe..7dd8a19f6 100644
--- a/api/src/main/java/ai/djl/ndarray/NDManager.java
+++ b/api/src/main/java/ai/djl/ndarray/NDManager.java
@@ -1389,6 +1389,13 @@ public interface NDManager extends AutoCloseable {
      */
     boolean isOpen();
 
+    /**
+     * Caps this manager to prevent unintentional attachment of resources. This is useful to detect
+     * memory leaks at an early point in time. The attachment of sub managers is still allowed after
+     * this method has been called.
+     */
+    void cap();
+
     /**
      * Returns the parent {@code NDManager}.
      *
@@ -1434,6 +1441,20 @@ public interface NDManager extends AutoCloseable {
      */
     void attachInternal(String resourceId, AutoCloseable resource);
 
+    /**
+     * Attaches a resource to this {@code NDManager} circumventing any cap protection.
+     *
+     * <p>The attached resource will be closed when this {@code NDManager} is closed.
+     *
+     * <p>This attachment is internal. Many resources will internally track which manager they are
+     * attached to. In that case, you should call {@link NDResource#attach(NDManager)} instead and
+     * that should then call attachInternal.
+     *
+     * @param resourceId the unique resourceId
+     * @param resource the {@link AutoCloseable} resource to be attached
+     */
+    void attachUncappedInternal(String resourceId, AutoCloseable resource);
+
     /**
      * Temporarily attaches a resource to this {@code NDManager} to be returned when this is closed.
      *
diff --git a/api/src/main/java/ai/djl/ndarray/NDResource.java b/api/src/main/java/ai/djl/ndarray/NDResource.java
index 8033d0226..72d709c8d 100644
--- a/api/src/main/java/ai/djl/ndarray/NDResource.java
+++ b/api/src/main/java/ai/djl/ndarray/NDResource.java
@@ -31,6 +31,18 @@ public interface NDResource extends AutoCloseable {
      */
     void attach(NDManager manager);
 
+    /**
+     * Attaches this {@link NDResource} to the specified {@link NDManager} from which it was
+     * previously detached and temp-attached to the current manager of this resource. This is
+     * functionally equivalent to the attach method, however the cap-state disregarded when adding
+     * the resource back to the original manager.
+     *
+     * @param manager the {@link NDManager} to be attached to
+     */
+    default void returnResource(NDManager manager) {
+        attach(manager);
+    }
+
     /**
      * Temporarily attaches this {@link NDResource} to the specified {@link NDManager}.
      *
diff --git a/api/src/main/java/ai/djl/translate/ServingTranslatorFactory.java b/api/src/main/java/ai/djl/translate/ServingTranslatorFactory.java
index 54db7afe9..4eb3914c8 100644
--- a/api/src/main/java/ai/djl/translate/ServingTranslatorFactory.java
+++ b/api/src/main/java/ai/djl/translate/ServingTranslatorFactory.java
@@ -40,6 +40,7 @@ import java.util.Set;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import javax.tools.JavaCompiler;
 import javax.tools.ToolProvider;
 import org.slf4j.Logger;
@@ -81,7 +82,7 @@ public class ServingTranslatorFactory implements TranslatorFactory {
         Path libPath = modelDir.resolve("libs");
         if (!Files.isDirectory(libPath)) {
             libPath = modelDir.resolve("lib");
-            if (!Files.isDirectory(libPath)) {
+            if (!Files.isDirectory(libPath) && className == null) {
                 return loadDefaultTranslator(arguments);
             }
         }
@@ -98,10 +99,17 @@ public class ServingTranslatorFactory implements TranslatorFactory {
             Path classesDir = path.resolve("classes");
             compileJavaClass(classesDir);
 
-            List<Path> jarFiles =
-                    Files.list(path)
-                            .filter(p -> p.toString().endsWith(".jar"))
-                            .collect(Collectors.toList());
+            List<Path> jarFiles = new ArrayList<>();
+            if (Files.isDirectory(path)) {
+                try (Stream<Path> stream = Files.list(path)) {
+                    stream.forEach(
+                            p -> {
+                                if (p.toString().endsWith(".jar")) {
+                                    jarFiles.add(p);
+                                }
+                            });
+                }
+            }
             List<URL> urls = new ArrayList<>(jarFiles.size() + 1);
             urls.add(classesDir.toUri().toURL());
             for (Path p : jarFiles) {
@@ -136,10 +144,12 @@ public class ServingTranslatorFactory implements TranslatorFactory {
             logger.debug("Directory not exists: {}", dir);
             return null;
         }
-        Collection<Path> files =
-                Files.walk(dir)
-                        .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".class"))
-                        .collect(Collectors.toList());
+        Collection<Path> files;
+        try (Stream<Path> stream = Files.walk(dir)) {
+            files =
+                    stream.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".class"))
+                            .collect(Collectors.toList());
+        }
         for (Path file : files) {
             Path p = dir.relativize(file);
             String className = p.toString();
@@ -218,11 +228,13 @@ public class ServingTranslatorFactory implements TranslatorFactory {
                 logger.debug("Directory not exists: {}", dir);
                 return;
             }
-            String[] files =
-                    Files.walk(dir)
-                            .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".java"))
-                            .map(p -> p.toAbsolutePath().toString())
-                            .toArray(String[]::new);
+            String[] files;
+            try (Stream<Path> stream = Files.walk(dir)) {
+                files =
+                        stream.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".java"))
+                                .map(p -> p.toAbsolutePath().toString())
+                                .toArray(String[]::new);
+            }
             JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
             if (files.length > 0) {
                 compiler.run(null, null, null, files);
diff --git a/api/src/main/java/ai/djl/util/passthrough/PassthroughNDManager.java b/api/src/main/java/ai/djl/util/passthrough/PassthroughNDManager.java
index a82773685..d2c531b6b 100644
--- a/api/src/main/java/ai/djl/util/passthrough/PassthroughNDManager.java
+++ b/api/src/main/java/ai/djl/util/passthrough/PassthroughNDManager.java
@@ -176,6 +176,10 @@ public final class PassthroughNDManager implements NDManager {
         return true;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void cap() {}
+
     /** {@inheritDoc} */
     @Override
     public NDManager getParentManager() {
@@ -206,6 +210,12 @@ public final class PassthroughNDManager implements NDManager {
         throw new UnsupportedOperationException(UNSUPPORTED);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void attachUncappedInternal(String resourceId, AutoCloseable resource) {
+        throw new UnsupportedOperationException(UNSUPPORTED);
+    }
+
     /** {@inheritDoc} */
     @Override
     public void tempAttachInternal(
diff --git a/basicdataset/build.gradle b/basicdataset/build.gradle
index 132999c50..eac450c4d 100644
--- a/basicdataset/build.gradle
+++ b/basicdataset/build.gradle
@@ -10,7 +10,7 @@ dependencies {
     testImplementation "org.slf4j:slf4j-simple:${slf4j_version}"
 
     testImplementation project(":testing")
-    testImplementation project(":engines:mxnet:mxnet-engine")
+    testImplementation project(":engines:pytorch:pytorch-engine")
 }
 
 task syncS3(type: Exec) {
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/AirfoilRandomAccess.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/AirfoilRandomAccess.java
index bd4d6e209..4b68eb5a3 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/AirfoilRandomAccess.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/AirfoilRandomAccess.java
@@ -14,24 +14,16 @@ package ai.djl.basicdataset.tabular;
 
 import ai.djl.Application.Tabular;
 import ai.djl.basicdataset.BasicDatasets;
-import ai.djl.basicdataset.utils.Feature;
-import ai.djl.ndarray.NDList;
-import ai.djl.ndarray.NDManager;
-import ai.djl.ndarray.types.Shape;
+import ai.djl.basicdataset.tabular.utils.Feature;
 import ai.djl.repository.Artifact;
 import ai.djl.repository.MRL;
 import ai.djl.repository.Repository;
 import ai.djl.util.Progress;
 import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.FloatBuffer;
 import java.nio.file.Path;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import org.apache.commons.csv.CSVFormat;
-import org.apache.commons.csv.CSVRecord;
 
 /**
  * Airfoil Self-Noise Data Set from <a
@@ -52,10 +44,6 @@ public final class AirfoilRandomAccess extends CsvDataset {
     private Usage usage;
     private boolean prepared;
 
-    private boolean normalize;
-    private Map<String, Float> mean;
-    private Map<String, Float> std;
-
     /**
      * Creates an instance of {@code RandomAccessDataset} with the arguments in {@link Builder}.
      *
@@ -65,7 +53,6 @@ public final class AirfoilRandomAccess extends CsvDataset {
         super(builder);
         usage = builder.usage;
         mrl = builder.getMrl();
-        normalize = builder.normalize;
     }
 
     /** {@inheritDoc} */
@@ -93,20 +80,6 @@ public final class AirfoilRandomAccess extends CsvDataset {
 
         csvUrl = csvFile.toUri().toURL();
         super.prepare(progress);
-
-        if (normalize) {
-            mean = new HashMap<>();
-            std = new HashMap<>();
-
-            for (Feature feature : features) {
-                calculateMean(feature.getName());
-                calculateStd(feature.getName());
-            }
-            for (Feature feature : labels) {
-                calculateMean(feature.getName());
-                calculateStd(feature.getName());
-            }
-        }
         prepared = true;
     }
 
@@ -116,45 +89,6 @@ public final class AirfoilRandomAccess extends CsvDataset {
         return Arrays.asList(COLUMNS).subList(0, 5);
     }
 
-    /** {@inheritDoc} */
-    @Override
-    protected NDList toNDList(NDManager manager, CSVRecord record, List<Feature> selected) {
-        int length = selected.size();
-        ByteBuffer bb = manager.allocateDirect(length * 4);
-        FloatBuffer buf = bb.asFloatBuffer();
-        for (Feature feature : selected) {
-            String name = feature.getName();
-            float value = Float.parseFloat(record.get(name));
-            if (normalize) {
-                value = (value - mean.get(name)) / std.get(name);
-            }
-            buf.put(value);
-        }
-        buf.rewind();
-        return new NDList(manager.create(buf, new Shape(length)));
-    }
-
-    private void calculateMean(String column) {
-        float sum = 0;
-        long size = size();
-        for (int i = 0; i < size; ++i) {
-            CSVRecord record = csvRecords.get(i);
-            sum += Float.parseFloat(record.get(column));
-        }
-        mean.put(column, sum / size);
-    }
-
-    private void calculateStd(String column) {
-        float average = mean.get(column);
-        float sum = 0;
-        long size = size();
-        for (int i = 0; i < size; ++i) {
-            CSVRecord record = csvRecords.get(i);
-            sum += (float) Math.pow(Float.parseFloat(record.get(column)) - average, 2);
-        }
-        std.put(column, (float) Math.sqrt(sum / size));
-    }
-
     /**
      * Creates a builder to build a {@link AirfoilRandomAccess}.
      *
@@ -279,11 +213,11 @@ public final class AirfoilRandomAccess extends CsvDataset {
         public AirfoilRandomAccess build() {
             if (features.isEmpty()) {
                 for (int i = 0; i < 5; ++i) {
-                    addFeature(COLUMNS[i]);
+                    addNumericFeature(COLUMNS[i], normalize);
                 }
             }
             if (labels.isEmpty()) {
-                addNumericLabel("ssoundpres");
+                addNumericLabel("ssoundpres", normalize);
             }
             return new AirfoilRandomAccess(this);
         }
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/AmesRandomAccess.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/AmesRandomAccess.java
index e8a5b3694..538c0f95e 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/AmesRandomAccess.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/AmesRandomAccess.java
@@ -200,7 +200,7 @@ public class AmesRandomAccess extends CsvDataset {
             if (af.categorical.contains(name)) {
                 Map<String, Integer> map = af.featureToMap.get(name);
                 if (map == null) {
-                    return addCategoricalFeature(name);
+                    return addCategoricalFeature(name, onehotEncode);
                 }
                 return addCategoricalFeature(name, map, onehotEncode);
             }
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/CsvDataset.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/CsvDataset.java
index a09b0e82c..17e386c89 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/CsvDataset.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/CsvDataset.java
@@ -12,13 +12,6 @@
  */
 package ai.djl.basicdataset.tabular;
 
-import ai.djl.basicdataset.utils.DynamicBuffer;
-import ai.djl.basicdataset.utils.Feature;
-import ai.djl.ndarray.NDList;
-import ai.djl.ndarray.NDManager;
-import ai.djl.ndarray.types.Shape;
-import ai.djl.training.dataset.RandomAccessDataset;
-import ai.djl.training.dataset.Record;
 import ai.djl.util.Progress;
 import java.io.BufferedInputStream;
 import java.io.IOException;
@@ -27,43 +20,33 @@ import java.io.InputStreamReader;
 import java.io.Reader;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.nio.FloatBuffer;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.zip.GZIPInputStream;
 import org.apache.commons.csv.CSVFormat;
 import org.apache.commons.csv.CSVParser;
 import org.apache.commons.csv.CSVRecord;
 
 /** {@code CsvDataset} represents the dataset that stored in a .csv file. */
-public class CsvDataset extends RandomAccessDataset {
+public class CsvDataset extends TabularDataset {
 
     protected URL csvUrl;
     protected CSVFormat csvFormat;
-    protected List<Feature> features;
-    protected List<Feature> labels;
     protected List<CSVRecord> csvRecords;
 
     protected CsvDataset(CsvBuilder<?> builder) {
         super(builder);
         csvUrl = builder.csvUrl;
         csvFormat = builder.csvFormat;
-        features = builder.features;
-        labels = builder.labels;
     }
 
     /** {@inheritDoc} */
     @Override
-    public Record get(NDManager manager, long index) {
-        CSVRecord csvRecord = csvRecords.get(Math.toIntExact(index));
-        NDList data = toNDList(manager, csvRecord, features);
-        NDList label = toNDList(manager, csvRecord, labels);
-
-        return new Record(data, label);
+    protected String getCell(long rowIndex, String featureName) {
+        CSVRecord record = csvRecords.get(Math.toIntExact(rowIndex));
+        return record.get(featureName);
     }
 
     /** {@inheritDoc} */
@@ -79,6 +62,7 @@ public class CsvDataset extends RandomAccessDataset {
             CSVParser csvParser = new CSVParser(reader, csvFormat);
             csvRecords = csvParser.getRecords();
         }
+        prepareFeaturizers();
     }
 
     private InputStream getCsvStream() throws IOException {
@@ -109,29 +93,11 @@ public class CsvDataset extends RandomAccessDataset {
         return csvRecords.get(0).getParser().getHeaderNames();
     }
 
-    protected NDList toNDList(NDManager manager, CSVRecord record, List<Feature> selected) {
-        DynamicBuffer bb = new DynamicBuffer();
-        for (Feature feature : selected) {
-            String name = feature.getName();
-            String value = record.get(name);
-            feature.getFeaturizer().featurize(bb, value);
-        }
-        FloatBuffer buf = bb.getBuffer();
-        return new NDList(manager.create(buf, new Shape(bb.getLength())));
-    }
-
     /** Used to build a {@link CsvDataset}. */
-    public static class CsvBuilder<T extends CsvBuilder<T>> extends BaseBuilder<T> {
+    public static class CsvBuilder<T extends CsvBuilder<T>> extends TabularDataset.BaseBuilder<T> {
 
         protected URL csvUrl;
         protected CSVFormat csvFormat;
-        protected List<Feature> features;
-        protected List<Feature> labels;
-
-        protected CsvBuilder() {
-            features = new ArrayList<>();
-            labels = new ArrayList<>();
-        }
 
         /** {@inheritDoc} */
         @Override
@@ -181,111 +147,13 @@ public class CsvDataset extends RandomAccessDataset {
             return self();
         }
 
-        /**
-         * Adds the features to the feature set.
-         *
-         * @param features the features
-         * @return this builder
-         */
-        public T addFeature(Feature... features) {
-            Collections.addAll(this.features, features);
-            return self();
-        }
-
-        /**
-         * Adds a numeric feature to the feature set.
-         *
-         * @param name the feature name
-         * @return this builder
-         */
-        public T addNumericFeature(String name) {
-            features.add(new Feature(name, true));
-            return self();
-        }
-
-        /**
-         * Adds a categorical feature to the feature set.
-         *
-         * @param name the feature name
-         * @return this builder
-         */
-        public T addCategoricalFeature(String name) {
-            features.add(new Feature(name, false));
-            return self();
-        }
-
-        /**
-         * Adds a categorical feature to the feature set with specified mapping.
-         *
-         * @param name the feature name
-         * @param map a map contains categorical value maps to index
-         * @param onehotEncode true if use onehot encode
-         * @return this builder
-         */
-        public T addCategoricalFeature(
-                String name, Map<String, Integer> map, boolean onehotEncode) {
-            features.add(new Feature(name, map, onehotEncode));
-            return self();
-        }
-
-        /**
-         * Adds the features to the label set.
-         *
-         * @param labels the labels
-         * @return this builder
-         */
-        public T addLabel(Feature... labels) {
-            Collections.addAll(this.labels, labels);
-            return self();
-        }
-
-        /**
-         * Adds a number feature to the label set.
-         *
-         * @param name the label name
-         * @return this builder
-         */
-        public T addNumericLabel(String name) {
-            labels.add(new Feature(name, true));
-            return self();
-        }
-
-        /**
-         * Adds a categorical feature to the label set.
-         *
-         * @param name the feature name
-         * @return this builder
-         */
-        public T addCategoricalLabel(String name) {
-            labels.add(new Feature(name, true));
-            return self();
-        }
-
-        /**
-         * Adds a categorical feature to the feature set with specified mapping.
-         *
-         * @param name the feature name
-         * @param map a map contains categorical value maps to index
-         * @param onehotEncode true if use onehot encode
-         * @return this builder
-         */
-        public T addCategoricalLabel(String name, Map<String, Integer> map, boolean onehotEncode) {
-            labels.add(new Feature(name, map, onehotEncode));
-            return self();
-        }
-
         /**
          * Builds the new {@link CsvDataset}.
          *
          * @return the new {@link CsvDataset}
          */
         public CsvDataset build() {
-            if (features.isEmpty()) {
-                throw new IllegalArgumentException("Missing features.");
-            }
-            if (labels.isEmpty()) {
-                throw new IllegalArgumentException("Missing labels.");
-            }
+            validate();
             return new CsvDataset(this);
         }
     }
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/DailyDelhiClimate.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/DailyDelhiClimate.java
index 54cf4e244..6800f6468 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/DailyDelhiClimate.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/DailyDelhiClimate.java
@@ -14,25 +14,18 @@ package ai.djl.basicdataset.tabular;
 
 import ai.djl.Application.Tabular;
 import ai.djl.basicdataset.BasicDatasets;
-import ai.djl.basicdataset.utils.DynamicBuffer;
-import ai.djl.basicdataset.utils.Feature;
-import ai.djl.basicdataset.utils.Featurizer;
-import ai.djl.ndarray.NDList;
-import ai.djl.ndarray.NDManager;
+import ai.djl.basicdataset.tabular.utils.Feature;
+import ai.djl.basicdataset.tabular.utils.Featurizers;
 import ai.djl.repository.Artifact;
 import ai.djl.repository.MRL;
 import ai.djl.repository.Repository;
-import ai.djl.training.dataset.Record;
 import ai.djl.util.Progress;
 import java.io.IOException;
 import java.nio.file.Path;
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import org.apache.commons.csv.CSVFormat;
-import org.apache.commons.csv.CSVRecord;
 
 /**
  * Daily Delhi climate dataset from <a
@@ -58,16 +51,6 @@ public class DailyDelhiClimate extends CsvDataset {
         mrl = builder.getMrl();
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public Record get(NDManager manager, long index) {
-        CSVRecord csvRecord = csvRecords.get(Math.toIntExact(index));
-        NDList data = toNDList(manager, csvRecord, features);
-        NDList label = new NDList();
-
-        return new Record(data, label);
-    }
-
     /** {@inheritDoc} */
     @Override
     public void prepare(Progress progress) throws IOException {
@@ -202,24 +185,13 @@ public class DailyDelhiClimate extends CsvDataset {
          */
         public Builder addFeature(String name) {
             if ("date".equals(name)) {
-                return addDateFeature(name);
+                return addFeature(
+                        new Feature(name, Featurizers.getEpochDayFeaturizer("yyyy-MM-dd")));
             } else {
                 return addNumericFeature(name);
             }
         }
 
-        /**
-         * Add a new feature of date type. Since the dataset is a time series dataset, the date is
-         * an important feature in String form, and we can't just treat it as a simple categorical
-         * feature.
-         *
-         * @param name the name of the feature
-         * @return this builder
-         */
-        private Builder addDateFeature(String name) {
-            return addFeature(new Feature(name, new DateFeaturizer()));
-        }
-
         /**
          * Returns the available features of this dataset.
          *
@@ -248,23 +220,4 @@ public class DailyDelhiClimate extends CsvDataset {
             return repository.dataset(Tabular.ANY, groupId, artifactId, VERSION);
         }
     }
-
-    /** A featurizer implemented for feature of date type. */
-    public static final class DateFeaturizer implements Featurizer {
-
-        /**
-         * Featurize the feature of date type to epoch day (the number of days passed since
-         * 1970-01-01) and put it into float buffer, so that it can be used for future training in a
-         * simple way.
-         *
-         * @param buf the float buffer to be filled
-         * @param input the date string in the format {@code yyyy-MM-dd}
-         */
-        @Override
-        public void featurize(DynamicBuffer buf, String input) {
-            LocalDate ld = LocalDate.parse(input, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
-            long day = ld.toEpochDay();
-            buf.put(day);
-        }
-    }
 }
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/TabularDataset.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/TabularDataset.java
new file mode 100644
index 000000000..114b0033d
--- /dev/null
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/TabularDataset.java
@@ -0,0 +1,280 @@
+/*
+ * 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.basicdataset.tabular;
+
+import ai.djl.basicdataset.tabular.utils.DynamicBuffer;
+import ai.djl.basicdataset.tabular.utils.Feature;
+import ai.djl.basicdataset.tabular.utils.Featurizers;
+import ai.djl.basicdataset.tabular.utils.PreparedFeaturizer;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.ndarray.types.Shape;
+import ai.djl.training.dataset.RandomAccessDataset;
+import ai.djl.training.dataset.Record;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/** A abstract class for creating tabular datasets. */
+public abstract class TabularDataset extends RandomAccessDataset {
+
+    protected List<Feature> features;
+    protected List<Feature> labels;
+
+    /**
+     * Creates a new instance of {@link RandomAccessDataset} with the given necessary
+     * configurations.
+     *
+     * @param builder a builder with the necessary configurations
+     */
+    public TabularDataset(BaseBuilder<?> builder) {
+        super(builder);
+        features = builder.features;
+        labels = builder.labels;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Record get(NDManager manager, long index) {
+        NDList data = getRowFeatures(manager, index, features);
+
+        NDList label;
+        if (labels.isEmpty()) {
+            label = new NDList();
+        } else {
+            label = getRowFeatures(manager, index, labels);
+        }
+
+        return new Record(data, label);
+    }
+
+    /**
+     * Returns the designated features (either data or label features) from a row.
+     *
+     * @param manager the manager used to create the arrays
+     * @param index the index of the requested data item
+     * @param selected the features to pull from the row
+     * @return the features formatted as an {@link NDList}
+     */
+    public NDList getRowFeatures(NDManager manager, long index, List<Feature> selected) {
+        DynamicBuffer bb = new DynamicBuffer();
+        for (Feature feature : selected) {
+            String name = feature.getName();
+            String value = getCell(index, name);
+            feature.getFeaturizer().featurize(bb, value);
+        }
+        FloatBuffer buf = bb.getBuffer();
+        return new NDList(manager.create(buf, new Shape(bb.getLength())));
+    }
+
+    /** Prepares the {@link ai.djl.basicdataset.tabular.utils.PreparedFeaturizer}s. */
+    protected void prepareFeaturizers() {
+        int availableSize = Math.toIntExact(availableSize());
+        List<Feature> featuresToPrepare = new ArrayList<>(features.size() + labels.size());
+        featuresToPrepare.addAll(features);
+        featuresToPrepare.addAll(labels);
+        for (Feature feature : featuresToPrepare) {
+            if (feature.getFeaturizer() instanceof PreparedFeaturizer) {
+                PreparedFeaturizer featurizer = (PreparedFeaturizer) feature.getFeaturizer();
+                List<String> inputs = new ArrayList<>(Math.toIntExact(availableSize));
+                for (int i = 0; i < availableSize; i++) {
+                    inputs.add(getCell(i, feature.getName()));
+                }
+                featurizer.prepare(inputs);
+            }
+        }
+    }
+
+    /**
+     * Returns a cell in the dataset.
+     *
+     * @param rowIndex the row index or record index for the cell
+     * @param featureName the feature or column of the cell
+     * @return the value of the cell at that row and column
+     */
+    protected abstract String getCell(long rowIndex, String featureName);
+
+    /**
+     * Used to build a {@link TabularDataset}.
+     *
+     * @param <T> the builder type
+     */
+    public abstract static class BaseBuilder<T extends BaseBuilder<T>>
+            extends RandomAccessDataset.BaseBuilder<T> {
+
+        protected List<Feature> features;
+        protected List<Feature> labels;
+
+        protected BaseBuilder() {
+            features = new ArrayList<>();
+            labels = new ArrayList<>();
+        }
+
+        /**
+         * Adds the features to the feature set.
+         *
+         * @param features the features
+         * @return this builder
+         */
+        public T addFeature(Feature... features) {
+            Collections.addAll(this.features, features);
+            return self();
+        }
+
+        /**
+         * Adds a numeric feature to the feature set.
+         *
+         * @param name the feature name
+         * @return this builder
+         */
+        public T addNumericFeature(String name) {
+            features.add(new Feature(name, true));
+            return self();
+        }
+
+        /**
+         * Adds a numeric feature to the feature set.
+         *
+         * @param name the feature name
+         * @param normalize true to normalize the column
+         * @return this builder
+         */
+        public T addNumericFeature(String name, boolean normalize) {
+            features.add(new Feature(name, Featurizers.getNumericFeaturizer(normalize)));
+            return self();
+        }
+
+        /**
+         * Adds a categorical feature to the feature set.
+         *
+         * @param name the feature name
+         * @return this builder
+         */
+        public T addCategoricalFeature(String name) {
+            features.add(new Feature(name, false));
+            return self();
+        }
+
+        /**
+         * Adds a categorical feature to the feature set.
+         *
+         * @param name the feature name
+         * @param onehotEncode true to use onehot encode
+         * @return this builder
+         */
+        public T addCategoricalFeature(String name, boolean onehotEncode) {
+            features.add(new Feature(name, Featurizers.getStringFeaturizer(onehotEncode)));
+            return self();
+        }
+
+        /**
+         * Adds a categorical feature to the feature set with specified mapping.
+         *
+         * @param name the feature name
+         * @param map a map contains categorical value maps to index
+         * @param onehotEncode true to use onehot encode
+         * @return this builder
+         */
+        public T addCategoricalFeature(
+                String name, Map<String, Integer> map, boolean onehotEncode) {
+            features.add(new Feature(name, map, onehotEncode));
+            return self();
+        }
+
+        /**
+         * Adds the features to the label set.
+         *
+         * @param labels the labels
+         * @return this builder
+         */
+        public T addLabel(Feature... labels) {
+            Collections.addAll(this.labels, labels);
+            return self();
+        }
+
+        /**
+         * Adds a number feature to the label set.
+         *
+         * @param name the label name
+         * @return this builder
+         */
+        public T addNumericLabel(String name) {
+            labels.add(new Feature(name, true));
+            return self();
+        }
+
+        /**
+         * Adds a number feature to the label set.
+         *
+         * @param name the label name
+         * @param normalize true to normalize the column
+         * @return this builder
+         */
+        public T addNumericLabel(String name, boolean normalize) {
+            labels.add(new Feature(name, Featurizers.getNumericFeaturizer(normalize)));
+            return self();
+        }
+
+        /**
+         * Adds a categorical feature to the label set.
+         *
+         * @param name the feature name
+         * @return this builder
+         */
+        public T addCategoricalLabel(String name) {
+            labels.add(new Feature(name, false));
+            return self();
+        }
+
+        /**
+         * Adds a categorical feature to the label set.
+         *
+         * @param name the feature name
+         * @param onehotEncode true if use onehot encode
+         * @return this builder
+         */
+        public T addCategoricalLabel(String name, boolean onehotEncode) {
+            labels.add(new Feature(name, Featurizers.getStringFeaturizer(onehotEncode)));
+            return self();
+        }
+
+        /**
+         * Adds a categorical feature to the feature set with specified mapping.
+         *
+         * @param name the feature name
+         * @param map a map contains categorical value maps to index
+         * @param onehotEncode true if use onehot encode
+         * @return this builder
+         */
+        public T addCategoricalLabel(String name, Map<String, Integer> map, boolean onehotEncode) {
+            labels.add(new Feature(name, map, onehotEncode));
+            return self();
+        }
+
+        /**
+         * Validates the builder to ensure it is correct.
+         *
+         * @throws IllegalArgumentException if there is an error with the builder arguments
+         */
+        protected void validate() {
+            if (features.isEmpty()) {
+                throw new IllegalArgumentException("Missing features.");
+            }
+            if (labels.isEmpty()) {
+                throw new IllegalArgumentException("Missing labels.");
+            }
+        }
+    }
+}
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/utils/DynamicBuffer.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/DynamicBuffer.java
similarity index 97%
rename from basicdataset/src/main/java/ai/djl/basicdataset/utils/DynamicBuffer.java
rename to basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/DynamicBuffer.java
index 628ff608a..3ab3e9ed4 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/utils/DynamicBuffer.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/DynamicBuffer.java
@@ -10,7 +10,7 @@
  * 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.utils;
+package ai.djl.basicdataset.tabular.utils;
 
 import java.nio.FloatBuffer;
 
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/utils/Feature.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/Feature.java
similarity index 98%
rename from basicdataset/src/main/java/ai/djl/basicdataset/utils/Feature.java
rename to basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/Feature.java
index 46b91fd16..2d3a28205 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/utils/Feature.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/Feature.java
@@ -10,7 +10,7 @@
  * 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.utils;
+package ai.djl.basicdataset.tabular.utils;
 
 import java.util.Map;
 
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/utils/Featurizer.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/Featurizer.java
similarity index 95%
rename from basicdataset/src/main/java/ai/djl/basicdataset/utils/Featurizer.java
rename to basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/Featurizer.java
index a6c8d0517..215457a27 100644
--- a/basicdataset/src/main/java/ai/djl/basicdataset/utils/Featurizer.java
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/Featurizer.java
@@ -10,7 +10,7 @@
  * 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.utils;
+package ai.djl.basicdataset.tabular.utils;
 
 /** An interface that convert String to numeric data. */
 public interface Featurizer {
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/Featurizers.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/Featurizers.java
new file mode 100644
index 000000000..26a20d0d2
--- /dev/null
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/Featurizers.java
@@ -0,0 +1,245 @@
+/*
+ * 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.basicdataset.tabular.utils;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** A utility class provides helper functions to create {@link Featurizer}. */
+public final class Featurizers {
+
+    private static final Featurizer NUMERIC_FEATURIZER = new NumericFeaturizer();
+
+    private Featurizers() {}
+
+    /**
+     * Returns the default numeric {@link Featurizer}.
+     *
+     * @return the default numeric {@link Featurizer}
+     */
+    public static Featurizer getNumericFeaturizer() {
+        return getNumericFeaturizer(false);
+    }
+
+    /**
+     * Returns the default numeric {@link Featurizer}.
+     *
+     * @param normalize true to normalize (with mean and std) the values
+     * @return the default numeric {@link Featurizer}
+     */
+    public static Featurizer getNumericFeaturizer(boolean normalize) {
+        if (normalize) {
+            return new NormalizedNumericFeaturizer();
+        } else {
+            return NUMERIC_FEATURIZER;
+        }
+    }
+
+    /**
+     * Returns the default String {@link Featurizer}.
+     *
+     * @return the default String {@link Featurizer}
+     */
+    public static Featurizer getStringFeaturizer() {
+        return getStringFeaturizer(true);
+    }
+
+    /**
+     * Returns the default String {@link Featurizer}.
+     *
+     * @param onehotEncode true to use onehot encoding
+     * @return the default String {@link Featurizer}
+     */
+    public static Featurizer getStringFeaturizer(boolean onehotEncode) {
+        if (onehotEncode) {
+            return new PreparedOneHotStringFeaturizer();
+        } else {
+            return new StringFeaturizer();
+        }
+    }
+
+    /**
+     * Returns a new instance of String {@link Featurizer}.
+     *
+     * @param map a map contains categorical value maps to index
+     * @param onehotEncode true to use onehot encoding
+     * @return a new instance of String {@link Featurizer}
+     */
+    public static Featurizer getStringFeaturizer(Map<String, Integer> map, boolean onehotEncode) {
+        if (onehotEncode) {
+            return new OneHotStringFeaturizer(map);
+        } else {
+            return new StringFeaturizer(map);
+        }
+    }
+
+    /**
+     * Constructs an {@link EpochDayFeaturizer} for representing dates using the epoch day (number
+     * of days since 1970-01-01).
+     *
+     * @param datePattern the pattern that dates are found in the data table column
+     * @return a new instance of {@link EpochDayFeaturizer}
+     */
+    public static Featurizer getEpochDayFeaturizer(String datePattern) {
+        return new EpochDayFeaturizer(datePattern);
+    }
+
+    private static final class NumericFeaturizer implements Featurizer {
+
+        /** {@inheritDoc} */
+        @Override
+        public void featurize(DynamicBuffer buf, String input) {
+            buf.put(Float.parseFloat(input));
+        }
+    }
+
+    private static final class NormalizedNumericFeaturizer implements PreparedFeaturizer {
+
+        private float mean;
+        private float std;
+
+        /** {@inheritDoc} */
+        @Override
+        public void featurize(DynamicBuffer buf, String input) {
+            float value = (Float.parseFloat(input) - mean) / std;
+            buf.put(value);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void prepare(List<String> inputs) {
+            calculateMean(inputs);
+            calculateStd(inputs);
+        }
+
+        private void calculateMean(List<String> inputs) {
+            double sum = 0;
+            for (String input : inputs) {
+                sum += Float.parseFloat(input);
+            }
+            mean = (float) (sum / inputs.size());
+        }
+
+        private void calculateStd(List<String> inputs) {
+            double sum = 0;
+            for (String input : inputs) {
+                sum += Math.pow(Float.parseFloat(input) - mean, 2);
+            }
+            std = (float) Math.sqrt(sum / inputs.size());
+        }
+    }
+
+    private static class OneHotStringFeaturizer implements Featurizer {
+        protected Map<String, Integer> map;
+
+        public OneHotStringFeaturizer(Map<String, Integer> map) {
+            this.map = map;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void featurize(DynamicBuffer buf, String input) {
+            for (int i = 0; i < map.size(); ++i) {
+                buf.put(i == map.get(input) ? 1 : 0);
+            }
+        }
+    }
+
+    private static final class PreparedOneHotStringFeaturizer extends OneHotStringFeaturizer
+            implements PreparedFeaturizer {
+
+        public PreparedOneHotStringFeaturizer() {
+            super(null);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void prepare(List<String> inputs) {
+            map = new ConcurrentHashMap<>();
+            for (String input : inputs) {
+                if (!map.containsKey(input)) {
+                    map.put(input, map.size());
+                }
+            }
+        }
+    }
+
+    private static final class StringFeaturizer implements Featurizer {
+
+        private Map<String, Integer> map;
+        private boolean autoMap;
+
+        StringFeaturizer() {
+            this.map = new HashMap<>();
+            this.autoMap = true;
+        }
+
+        StringFeaturizer(Map<String, Integer> map) {
+            this.map = map;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void featurize(DynamicBuffer buf, String input) {
+            Integer index = map.get(input);
+            if (index != null) {
+                buf.put(index);
+                return;
+            }
+
+            if (!autoMap) {
+                throw new IllegalArgumentException("Value: " + input + " not found in the map.");
+            }
+            int value = map.size();
+            map.put(input, value);
+            buf.put(value);
+        }
+    }
+
+    /**
+     * A featurizer implemented for feature of date type using epoch day (number of days since
+     * 1970-01-01).
+     */
+    private static final class EpochDayFeaturizer implements Featurizer {
+
+        String datePattern;
+
+        /**
+         * Constructs a {@link EpochDayFeaturizer}.
+         *
+         * @param datePattern the pattern that dates are found in the data table column
+         */
+        EpochDayFeaturizer(String datePattern) {
+            this.datePattern = datePattern;
+        }
+
+        /**
+         * Featurize the feature of date type to epoch day (the number of days passed since
+         * 1970-01-01) and put it into float buffer, so that it can be used for future training in a
+         * simple way.
+         *
+         * @param buf the float buffer to be filled
+         * @param input the date string in the format {@code yyyy-MM-dd}
+         */
+        @Override
+        public void featurize(DynamicBuffer buf, String input) {
+            LocalDate ld = LocalDate.parse(input, DateTimeFormatter.ofPattern(datePattern));
+            long day = ld.toEpochDay();
+            buf.put(day);
+        }
+    }
+}
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/PreparedFeaturizer.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/PreparedFeaturizer.java
new file mode 100644
index 000000000..f5f3d33a5
--- /dev/null
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/PreparedFeaturizer.java
@@ -0,0 +1,26 @@
+/*
+ * 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.basicdataset.tabular.utils;
+
+import java.util.List;
+
+/** A {@link Featurizer} that must be prepared with the possible feature values before use. */
+public interface PreparedFeaturizer extends Featurizer {
+
+    /**
+     * Prepares the featurizer with the list of possible inputs.
+     *
+     * @param inputs the possible inputs
+     */
+    void prepare(List<String> inputs);
+}
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/package-info.java b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/package-info.java
new file mode 100644
index 000000000..d9f4b7b4a
--- /dev/null
+++ b/basicdataset/src/main/java/ai/djl/basicdataset/tabular/utils/package-info.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2021 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.
+ */
+
+/** Contains utilities used within datasets that are {@link ai.djl.Application.Tabular}. */
+package ai.djl.basicdataset.tabular.utils;
diff --git a/basicdataset/src/main/java/ai/djl/basicdataset/utils/Featurizers.java b/basicdataset/src/main/java/ai/djl/basicdataset/utils/Featurizers.java
deleted file mode 100644
index 1f0d76a96..000000000
--- a/basicdataset/src/main/java/ai/djl/basicdataset/utils/Featurizers.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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.basicdataset.utils;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/** A utility class provides helper functions to create {@link Featurizer}. */
-public final class Featurizers {
-
-    private static final Featurizer NUMERIC_FEATURIZER = new NumericFeaturizer();
-    private static final Featurizer STRING_FEATURIZER = new StringFeaturizer();
-
-    private Featurizers() {}
-
-    /**
-     * Returns the default numeric {@link Featurizer}.
-     *
-     * @return the default numeric {@link Featurizer}
-     */
-    public static Featurizer getNumericFeaturizer() {
-        return NUMERIC_FEATURIZER;
-    }
-
-    /**
-     * Returns the default String {@link Featurizer}.
-     *
-     * @return the default String {@link Featurizer}
-     */
-    public static Featurizer getStringFeaturizer() {
-        return STRING_FEATURIZER;
-    }
-
-    /**
-     * Returns a new instance of String {@link Featurizer}.
-     *
-     * @param map a map contains categorical value maps to index
-     * @param onehotEncode true if use onehot encode
-     * @return a new instance of String {@link Featurizer}
-     */
-    public static Featurizer getStringFeaturizer(Map<String, Integer> map, boolean onehotEncode) {
-        return new StringFeaturizer(map, onehotEncode);
-    }
-
-    private static final class NumericFeaturizer implements Featurizer {
-
-        /** {@inheritDoc} */
-        @Override
-        public void featurize(DynamicBuffer buf, String input) {
-            buf.put(Float.parseFloat(input));
-        }
-    }
-
-    private static final class StringFeaturizer implements Featurizer {
-
-        private Map<String, Integer> map;
-        private boolean onehotEncode;
-        private boolean autoMap;
-
-        StringFeaturizer() {
-            this.map = new HashMap<>();
-            this.autoMap = true;
-        }
-
-        StringFeaturizer(Map<String, Integer> map, boolean onehotEncode) {
-            this.map = map;
-            this.onehotEncode = onehotEncode;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public void featurize(DynamicBuffer buf, String input) {
-            if (onehotEncode) {
-                for (int i = 0; i < map.size(); ++i) {
-                    buf.put(i == map.get(input) ? 1 : 0);
-                }
-                return;
-            }
-
-            Integer index = map.get(input);
-            if (index != null) {
-                buf.put(index);
-                return;
-            }
-
-            if (!autoMap) {
-                throw new IllegalArgumentException("Value: " + input + " not found in the map.");
-            }
-            int value = map.size();
-            map.put(input, value);
-            buf.put(value);
-        }
-    }
-}
diff --git a/docs/development/how_to_use_dataset.md b/docs/development/how_to_use_dataset.md
index 8f6c3abd7..4ab75dcf5 100644
--- a/docs/development/how_to_use_dataset.md
+++ b/docs/development/how_to_use_dataset.md
@@ -23,9 +23,11 @@ If a helper is available, it can make it easier to implement the dataset then bu
 - [TextDataset](https://javadoc.io/doc/ai.djl/basicdataset/latest/ai/djl/basicdataset/nlp/TextDataset.html) - An abstract dataset for NLP where either the input or labels are text-based.
 - [TextData](https://javadoc.io/doc/ai.djl/basicdataset/latest/ai/djl/basicdataset/utils/TextData.html) - A utility for managing the text within a dataset
 
-### Other
+### Tabular
 
 - [CsvDataset](https://javadoc.io/doc/ai.djl/basicdataset/latest/ai/djl/basicdataset/tabular/CsvDataset.html) - An dataset for loading data from a .csv file
+- [TabularDataset](https://javadoc.io/doc/ai.djl/basicdataset/latest/ai/djl/basicdataset/tabular/TabularDataset.html) - An abstract dataset for loading tabular data with rows and feature columns
+- [TablesawDataset](https://javadoc.io/doc/ai.djl.tablesaw/tablesaw/latest/ai/djl/tablesaw/TablesawDataset.html) - An dataset for loading from [Tablesaw](https://jtablesaw.github.io/tablesaw/)
 
 ## Custom Datasets
 
diff --git a/engines/ml/xgboost/build.gradle b/engines/ml/xgboost/build.gradle
index 9d31691b7..d7b36ad03 100644
--- a/engines/ml/xgboost/build.gradle
+++ b/engines/ml/xgboost/build.gradle
@@ -1,5 +1,3 @@
-import groovy.xml.QName
-
 group "ai.djl.ml.xgboost"
 boolean isGpu = project.hasProperty("gpu")
 def XGB_FLAVOR = isGpu ? "-gpu" : ""
@@ -25,19 +23,18 @@ dependencies {
         exclude group: "org.scala-lang", module: "scala-reflect"
         exclude group: "org.scala-lang", module: "scala-library"
     }
-    if (isGpu) {
-        api "ai.rapids:cudf:${rapis_version}:cuda11"
-    }
 
     exclusion project(":api")
     exclusion "commons-logging:commons-logging:${commons_logging_version}"
-    exclusion "ai.rapids:cudf:${rapis_version}:cuda11"
     testImplementation(project(":testing"))
     testImplementation("org.testng:testng:${testng_version}") {
         exclude group: "junit", module: "junit"
     }
 
     testRuntimeOnly "org.slf4j:slf4j-simple:${slf4j_version}"
+    if (isGpu) {
+        testRuntimeOnly "ai.rapids:cudf:${rapis_version}:cuda11"
+    }
 }
 
 jar {
@@ -67,12 +64,20 @@ publishing {
                 url = "https://djl.ai/engines/ml/${project.name}"
 
                 withXml {
-                    Node pomNode = asNode()
+                    def pomNode = asNode()
                     pomNode.dependencies."*".findAll() {
                         it.artifactId.text().startsWith("xgboost")
                     }.each() {
                         it.parent().remove(it)
                     }
+                    if (isGpu) {
+                        def dep = pomNode.dependencies[0].appendNode("dependency")
+                        dep.appendNode("groupId", "ai.rapids")
+                        dep.appendNode("artifactId", "cudf")
+                        dep.appendNode("version", "${rapis_version}")
+                        dep.appendNode("classifier", "cuda11")
+                        dep.appendNode("scope", "compile")
+                    }
                 }
             }
         }
diff --git a/engines/mxnet/mxnet-engine/src/main/java/ai/djl/mxnet/engine/MxNDArray.java b/engines/mxnet/mxnet-engine/src/main/java/ai/djl/mxnet/engine/MxNDArray.java
index fc68b4b73..f217fa89d 100644
--- a/engines/mxnet/mxnet-engine/src/main/java/ai/djl/mxnet/engine/MxNDArray.java
+++ b/engines/mxnet/mxnet-engine/src/main/java/ai/djl/mxnet/engine/MxNDArray.java
@@ -163,6 +163,14 @@ public class MxNDArray extends NativeResource<Pointer> implements LazyNDArray {
         manager.attachInternal(getUid(), this);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void returnResource(NDManager manager) {
+        detach();
+        this.manager = (MxNDManager) manager;
+        manager.attachUncappedInternal(getUid(), this);
+    }
+
     /** {@inheritDoc} */
     @Override
     public void tempAttach(NDManager manager) {
diff --git a/engines/mxnet/mxnet-engine/src/main/java/ai/djl/mxnet/engine/MxNDManager.java b/engines/mxnet/mxnet-engine/src/main/java/ai/djl/mxnet/engine/MxNDManager.java
index 90b61955e..c8bff66d0 100644
--- a/engines/mxnet/mxnet-engine/src/main/java/ai/djl/mxnet/engine/MxNDManager.java
+++ b/engines/mxnet/mxnet-engine/src/main/java/ai/djl/mxnet/engine/MxNDManager.java
@@ -277,7 +277,7 @@ public class MxNDManager extends BaseNDManager {
     @Override
     public MxNDManager newSubManager(Device dev) {
         MxNDManager manager = new MxNDManager(this, dev, version);
-        attachInternal(manager.uid, manager);
+        attachUncappedInternal(manager.uid, manager);
         return manager;
     }
 
diff --git a/engines/onnxruntime/onnxruntime-engine/build.gradle b/engines/onnxruntime/onnxruntime-engine/build.gradle
index fadda0f1f..c961d5691 100644
--- a/engines/onnxruntime/onnxruntime-engine/build.gradle
+++ b/engines/onnxruntime/onnxruntime-engine/build.gradle
@@ -7,6 +7,7 @@ dependencies {
     testImplementation("org.testng:testng:${testng_version}") {
         exclude group: "junit", module: "junit"
     }
+    testImplementation(project(":testing"))
 
     testRuntimeOnly "org.slf4j:slf4j-simple:${slf4j_version}"
 }
diff --git a/engines/onnxruntime/onnxruntime-engine/src/main/java/ai/djl/onnxruntime/engine/OrtEngine.java b/engines/onnxruntime/onnxruntime-engine/src/main/java/ai/djl/onnxruntime/engine/OrtEngine.java
index 1013a34e2..66adeb4b6 100644
--- a/engines/onnxruntime/onnxruntime-engine/src/main/java/ai/djl/onnxruntime/engine/OrtEngine.java
+++ b/engines/onnxruntime/onnxruntime-engine/src/main/java/ai/djl/onnxruntime/engine/OrtEngine.java
@@ -78,7 +78,7 @@ public final class OrtEngine extends Engine {
     /** {@inheritDoc} */
     @Override
     public String getVersion() {
-        return "1.9.0";
+        return "1.11.0";
     }
 
     /** {@inheritDoc} */
diff --git a/engines/paddlepaddle/paddlepaddle-engine/build.gradle b/engines/paddlepaddle/paddlepaddle-engine/build.gradle
index ed93a1871..06b03aa02 100644
--- a/engines/paddlepaddle/paddlepaddle-engine/build.gradle
+++ b/engines/paddlepaddle/paddlepaddle-engine/build.gradle
@@ -7,6 +7,7 @@ dependencies {
         exclude group: "junit", module: "junit"
     }
     testImplementation "org.slf4j:slf4j-simple:${slf4j_version}"
+    testImplementation(project(":testing"))
 }
 
 compileJava.dependsOn(processResources)
diff --git a/engines/paddlepaddle/paddlepaddle-engine/src/main/java/ai/djl/paddlepaddle/engine/PpEngine.java b/engines/paddlepaddle/paddlepaddle-engine/src/main/java/ai/djl/paddlepaddle/engine/PpEngine.java
index 0879600bc..2bae30c4d 100644
--- a/engines/paddlepaddle/paddlepaddle-engine/src/main/java/ai/djl/paddlepaddle/engine/PpEngine.java
+++ b/engines/paddlepaddle/paddlepaddle-engine/src/main/java/ai/djl/paddlepaddle/engine/PpEngine.java
@@ -17,9 +17,11 @@ import ai.djl.Model;
 import ai.djl.engine.Engine;
 import ai.djl.ndarray.NDManager;
 import ai.djl.nn.SymbolBlock;
-import ai.djl.paddlepaddle.jni.JniUtils;
 import ai.djl.paddlepaddle.jni.LibUtils;
 import ai.djl.training.GradientCollector;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
 
 /**
  * The {@code PpEngine} is an implementation of the {@link Engine} based on the <a
@@ -36,11 +38,7 @@ public final class PpEngine extends Engine {
     private Engine alternativeEngine;
     private boolean initialized;
 
-    private String version;
-
-    private PpEngine() {
-        version = JniUtils.getVersion();
-    }
+    private PpEngine() {}
 
     static Engine newInstance() {
         LibUtils.loadLibrary();
@@ -76,7 +74,14 @@ public final class PpEngine extends Engine {
     /** {@inheritDoc} */
     @Override
     public String getVersion() {
-        return version;
+        try (InputStream is =
+                PpEngine.class.getResourceAsStream("/paddlepaddle-engine.properties")) {
+            Properties prop = new Properties();
+            prop.load(is);
+            return prop.getProperty("paddlepaddle_version");
+        } catch (IOException e) {
+            throw new AssertionError("Failed to load paddlapaddle-engine.properties", e);
+        }
     }
 
     /** {@inheritDoc} */
diff --git a/engines/paddlepaddle/paddlepaddle-engine/src/main/java/ai/djl/paddlepaddle/jni/JniUtils.java b/engines/paddlepaddle/paddlepaddle-engine/src/main/java/ai/djl/paddlepaddle/jni/JniUtils.java
index fe9974991..b19d3ea1c 100644
--- a/engines/paddlepaddle/paddlepaddle-engine/src/main/java/ai/djl/paddlepaddle/jni/JniUtils.java
+++ b/engines/paddlepaddle/paddlepaddle-engine/src/main/java/ai/djl/paddlepaddle/jni/JniUtils.java
@@ -141,8 +141,4 @@ public final class JniUtils {
     public static String[] getInputNames(PaddlePredictor predictor) {
         return PaddleLibrary.LIB.getInputNames(predictor.getHandle());
     }
-
-    public static String getVersion() {
-        return "2.0.2";
-    }
 }
diff --git a/engines/paddlepaddle/paddlepaddle-model-zoo/build.gradle b/engines/paddlepaddle/paddlepaddle-model-zoo/build.gradle
index e18f1e7ed..c23f27184 100644
--- a/engines/paddlepaddle/paddlepaddle-model-zoo/build.gradle
+++ b/engines/paddlepaddle/paddlepaddle-model-zoo/build.gradle
@@ -8,6 +8,7 @@ dependencies {
         exclude group: "junit", module: "junit"
     }
     testImplementation "org.slf4j:slf4j-simple:${slf4j_version}"
+    testImplementation(project(":testing"))
     testRuntimeOnly project(":engines:pytorch:pytorch-engine")
 }
 
diff --git a/engines/pytorch/pytorch-engine/src/main/java/ai/djl/pytorch/engine/PtNDArray.java b/engines/pytorch/pytorch-engine/src/main/java/ai/djl/pytorch/engine/PtNDArray.java
index 96d30d87c..ffabca47d 100644
--- a/engines/pytorch/pytorch-engine/src/main/java/ai/djl/pytorch/engine/PtNDArray.java
+++ b/engines/pytorch/pytorch-engine/src/main/java/ai/djl/pytorch/engine/PtNDArray.java
@@ -271,6 +271,14 @@ public class PtNDArray extends NativeResource<Long> implements NDArray {
         manager.attachInternal(getUid(), this);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void returnResource(NDManager manager) {
+        detach();
+        this.manager = (PtNDManager) manager;
+        manager.attachUncappedInternal(getUid(), this);
+    }
+
     /** {@inheritDoc} */
     @Override
     public void tempAttach(NDManager manager) {
diff --git a/engines/pytorch/pytorch-engine/src/main/java/ai/djl/pytorch/engine/PtNDManager.java b/engines/pytorch/pytorch-engine/src/main/java/ai/djl/pytorch/engine/PtNDManager.java
index 6959e11b4..3b439b788 100644
--- a/engines/pytorch/pytorch-engine/src/main/java/ai/djl/pytorch/engine/PtNDManager.java
+++ b/engines/pytorch/pytorch-engine/src/main/java/ai/djl/pytorch/engine/PtNDManager.java
@@ -160,7 +160,7 @@ public class PtNDManager extends BaseNDManager {
     @Override
     public PtNDManager newSubManager(Device device) {
         PtNDManager manager = new PtNDManager(this, device);
-        attachInternal(manager.uid, manager);
+        attachUncappedInternal(manager.uid, manager);
         return manager;
     }
 
diff --git a/engines/tensorflow/tensorflow-model-zoo/build.gradle b/engines/tensorflow/tensorflow-model-zoo/build.gradle
index 5753fdc73..02e10135b 100644
--- a/engines/tensorflow/tensorflow-model-zoo/build.gradle
+++ b/engines/tensorflow/tensorflow-model-zoo/build.gradle
@@ -3,6 +3,7 @@ group 'ai.djl.tensorflow'
 dependencies {
     api project(":engines:tensorflow:tensorflow-engine")
 
+    testImplementation(project(":testing"))
     testImplementation("org.testng:testng:${testng_version}") {
         exclude group: "junit", module: "junit"
     }
diff --git a/examples/src/main/java/ai/djl/examples/training/transferlearning/TrainAmazonReviewRanking.java b/examples/src/main/java/ai/djl/examples/training/transferlearning/TrainAmazonReviewRanking.java
index 462292c69..ca098e067 100644
--- a/examples/src/main/java/ai/djl/examples/training/transferlearning/TrainAmazonReviewRanking.java
+++ b/examples/src/main/java/ai/djl/examples/training/transferlearning/TrainAmazonReviewRanking.java
@@ -16,9 +16,9 @@ import ai.djl.Application;
 import ai.djl.Model;
 import ai.djl.ModelException;
 import ai.djl.basicdataset.tabular.CsvDataset;
-import ai.djl.basicdataset.utils.DynamicBuffer;
-import ai.djl.basicdataset.utils.Feature;
-import ai.djl.basicdataset.utils.Featurizer;
+import ai.djl.basicdataset.tabular.utils.DynamicBuffer;
+import ai.djl.basicdataset.tabular.utils.Feature;
+import ai.djl.basicdataset.tabular.utils.Featurizer;
 import ai.djl.engine.Engine;
 import ai.djl.examples.training.util.Arguments;
 import ai.djl.inference.Predictor;
diff --git a/extensions/audio/gradlew b/extensions/audio/gradlew
new file mode 120000
index 000000000..343e0d2ca
--- /dev/null
+++ b/extensions/audio/gradlew
@@ -0,0 +1,1 @@
+../../gradlew
\ No newline at end of file
diff --git a/extensions/audio/src/main/java/ai/djl/audio/dataset/SpeechRecognitionDataset.java b/extensions/audio/src/main/java/ai/djl/audio/dataset/SpeechRecognitionDataset.java
index 29eefefff..47038589c 100644
--- a/extensions/audio/src/main/java/ai/djl/audio/dataset/SpeechRecognitionDataset.java
+++ b/extensions/audio/src/main/java/ai/djl/audio/dataset/SpeechRecognitionDataset.java
@@ -51,7 +51,7 @@ public abstract class SpeechRecognitionDataset extends RandomAccessDataset {
     protected boolean prepared;
 
     /**
-     * Creates a new instance of {@link SpeechRecognitionDataset} with the given necessary
+     * Creates a new instance of {@code SpeechRecognitionDataset} with the given necessary
      * configurations.
      *
      * @param builder a builder with the necessary configurations
@@ -85,7 +85,7 @@ public abstract class SpeechRecognitionDataset extends RandomAccessDataset {
         sourceAudioData.setAudioPaths(audioPathList);
     }
 
-    /** Abstract AudioBuilder that helps build a {@link SpeechRecognitionDataset}. */
+    /** Abstract AudioBuilder that helps build a {@code SpeechRecognitionDataset}. */
     public abstract static class AudioBuilder<T extends AudioBuilder<T>> extends BaseBuilder<T> {
 
         protected AudioData.Configuration sourceConfiguration;
@@ -108,7 +108,7 @@ public abstract class SpeechRecognitionDataset extends RandomAccessDataset {
         }
 
         /**
-         * Sets the {@link AudioData.Configuration} to use for the source text data.
+         * Sets the {@code AudioData.Configuration} to use for the source text data.
          *
          * @param sourceConfiguration the {@link AudioData.Configuration}
          * @return this builder
@@ -119,9 +119,9 @@ public abstract class SpeechRecognitionDataset extends RandomAccessDataset {
         }
 
         /**
-         * Sets the {@link TextData.Configuration} to use for the target text data.
+         * Sets the {@code TextData.Configuration} to use for the target text data.
          *
-         * @param targetConfiguration the {@link TextData.Configuration}
+         * @param targetConfiguration the {@code TextData.Configuration}
          * @return this builder
          */
         public T setTargetConfiguration(TextData.Configuration targetConfiguration) {
diff --git a/extensions/audio/src/main/javadoc/overview.html b/extensions/audio/src/main/javadoc/overview.html
new file mode 100644
index 000000000..eb2385b7a
--- /dev/null
+++ b/extensions/audio/src/main/javadoc/overview.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+    <meta charset="UTF-8">
+</head>
+<body>
+<p>This document is the API specification for the Audio extension for DJL.</p>
+
+<p>The audio module contains classes that provide high performance audio processing for DJL.</p>
+</body>
+</html>
diff --git a/extensions/aws-ai/build.gradle b/extensions/aws-ai/build.gradle
index 360ede00f..e28f63b0a 100644
--- a/extensions/aws-ai/build.gradle
+++ b/extensions/aws-ai/build.gradle
@@ -9,7 +9,7 @@ dependencies {
     api "software.amazon.awssdk:sts"
     api project(":api")
 
-    testImplementation project(":engines:mxnet:mxnet-model-zoo")
+    testImplementation project(":engines:pytorch:pytorch-model-zoo")
 
     testImplementation("org.testng:testng:${testng_version}") {
         exclude group: "junit", module: "junit"
diff --git a/extensions/tablesaw/src/main/java/ai/djl/tablesaw/TablesawDataset.java b/extensions/tablesaw/src/main/java/ai/djl/tablesaw/TablesawDataset.java
index 874a4bfa3..fb2694d1c 100644
--- a/extensions/tablesaw/src/main/java/ai/djl/tablesaw/TablesawDataset.java
+++ b/extensions/tablesaw/src/main/java/ai/djl/tablesaw/TablesawDataset.java
@@ -12,46 +12,30 @@
  */
 package ai.djl.tablesaw;
 
-import ai.djl.basicdataset.utils.DynamicBuffer;
-import ai.djl.basicdataset.utils.Feature;
-import ai.djl.ndarray.NDList;
-import ai.djl.ndarray.NDManager;
-import ai.djl.ndarray.types.Shape;
-import ai.djl.training.dataset.RandomAccessDataset;
-import ai.djl.training.dataset.Record;
+import ai.djl.basicdataset.tabular.TabularDataset;
 import ai.djl.util.Progress;
-import java.nio.FloatBuffer;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import tech.tablesaw.api.Row;
 import tech.tablesaw.api.Table;
 import tech.tablesaw.io.ReadOptions;
 
 /** {@code TablesawDataset} represents the dataset that stored in a .csv file. */
-public class TablesawDataset extends RandomAccessDataset {
+public class TablesawDataset extends TabularDataset {
 
     protected ReadOptions readOptions;
-    protected List<Feature> features;
-    protected List<Feature> labels;
     protected Table table;
 
     protected TablesawDataset(TablesawBuilder<?> builder) {
         super(builder);
         readOptions = builder.readOptions;
-        features = builder.features;
-        labels = builder.labels;
     }
 
     /** {@inheritDoc} */
     @Override
-    public Record get(NDManager manager, long index) {
-        Row row = table.row(Math.toIntExact(index));
-        NDList data = toNDList(manager, row, features);
-        NDList label = toNDList(manager, row, labels);
-
-        return new Record(data, label);
+    protected String getCell(long rowIndex, String featureName) {
+        Row row = table.row(Math.toIntExact(rowIndex));
+        return row.getString(featureName);
     }
 
     /** {@inheritDoc} */
@@ -64,6 +48,7 @@ public class TablesawDataset extends RandomAccessDataset {
     @Override
     public void prepare(Progress progress) {
         table = Table.read().usingOptions(readOptions);
+        prepareFeaturizers();
     }
 
     /**
@@ -87,28 +72,11 @@ public class TablesawDataset extends RandomAccessDataset {
         return table.columnNames();
     }
 
-    protected NDList toNDList(NDManager manager, Row row, List<Feature> selected) {
-        DynamicBuffer bb = new DynamicBuffer();
-        for (Feature feature : selected) {
-            String name = feature.getName();
-            String value = row.getString(name);
-            feature.getFeaturizer().featurize(bb, value);
-        }
-        FloatBuffer buf = bb.getBuffer();
-        return new NDList(manager.create(buf, new Shape(bb.getLength())));
-    }
-
     /** Used to build a {@link TablesawDataset}. */
-    public static class TablesawBuilder<T extends TablesawBuilder<T>> extends BaseBuilder<T> {
+    public static class TablesawBuilder<T extends TablesawBuilder<T>>
+            extends TabularDataset.BaseBuilder<T> {
 
         protected ReadOptions readOptions;
-        protected List<Feature> features;
-        protected List<Feature> labels;
-
-        protected TablesawBuilder() {
-            features = new ArrayList<>();
-            labels = new ArrayList<>();
-        }
 
         /** {@inheritDoc} */
         @Override
@@ -128,111 +96,13 @@ public class TablesawDataset extends RandomAccessDataset {
             return self();
         }
 
-        /**
-         * Adds the features to the feature set.
-         *
-         * @param features the features
-         * @return this builder
-         */
-        public T addFeature(Feature... features) {
-            Collections.addAll(this.features, features);
-            return self();
-        }
-
-        /**
-         * Adds a numeric feature to the feature set.
-         *
-         * @param name the feature name
-         * @return this builder
-         */
-        public T addNumericFeature(String name) {
-            features.add(new Feature(name, true));
-            return self();
-        }
-
-        /**
-         * Adds a categorical feature to the feature set.
-         *
-         * @param name the feature name
-         * @return this builder
-         */
-        public T addCategoricalFeature(String name) {
-            features.add(new Feature(name, false));
-            return self();
-        }
-
-        /**
-         * Adds a categorical feature to the feature set with specified mapping.
-         *
-         * @param name the feature name
-         * @param map a map contains categorical value maps to index
-         * @param onehotEncode true if use onehot encode
-         * @return this builder
-         */
-        public T addCategoricalFeature(
-                String name, Map<String, Integer> map, boolean onehotEncode) {
-            features.add(new Feature(name, map, onehotEncode));
-            return self();
-        }
-
-        /**
-         * Adds the features to the label set.
-         *
-         * @param labels the labels
-         * @return this builder
-         */
-        public T addLabel(Feature... labels) {
-            Collections.addAll(this.labels, labels);
-            return self();
-        }
-
-        /**
-         * Adds a number feature to the label set.
-         *
-         * @param name the label name
-         * @return this builder
-         */
-        public T addNumericLabel(String name) {
-            labels.add(new Feature(name, true));
-            return self();
-        }
-
-        /**
-         * Adds a categorical feature to the label set.
-         *
-         * @param name the feature name
-         * @return this builder
-         */
-        public T addCategoricalLabel(String name) {
-            labels.add(new Feature(name, true));
-            return self();
-        }
-
-        /**
-         * Adds a categorical feature to the feature set with specified mapping.
-         *
-         * @param name the feature name
-         * @param map a map contains categorical value maps to index
-         * @param onehotEncode true if use onehot encode
-         * @return this builder
-         */
-        public T addCategoricalLabel(String name, Map<String, Integer> map, boolean onehotEncode) {
-            labels.add(new Feature(name, map, onehotEncode));
-            return self();
-        }
-
         /**
          * Builds the new {@link TablesawDataset}.
          *
          * @return the new {@link TablesawDataset}
          */
         public TablesawDataset build() {
-            if (features.isEmpty()) {
-                throw new IllegalArgumentException("Missing features.");
-            }
-            if (labels.isEmpty()) {
-                throw new IllegalArgumentException("Missing labels.");
-            }
+            validate();
             return new TablesawDataset(this);
         }
     }


Test patch:
diff --git a/api/src/test/java/ai/djl/repository/DjlRepositoryTest.java b/api/src/test/java/ai/djl/repository/DjlRepositoryTest.java
index 83b8e9507..0f39630a7 100644
--- a/api/src/test/java/ai/djl/repository/DjlRepositoryTest.java
+++ b/api/src/test/java/ai/djl/repository/DjlRepositoryTest.java
@@ -26,16 +26,16 @@ public class DjlRepositoryTest {
 
     @Test
     public void testResource() throws ModelNotFoundException, MalformedModelException, IOException {
-        Repository repo = Repository.newInstance("DJL", "djl://ai.djl.mxnet/mlp");
+        Repository repo = Repository.newInstance("DJL", "djl://ai.djl.pytorch/resnet");
         Assert.assertEquals(repo.getResources().size(), 1);
 
-        repo = Repository.newInstance("DJL", "djl://ai.djl.mxnet/resnet/0.0.1");
+        repo = Repository.newInstance("DJL", "djl://ai.djl.pytorch/resnet/0.0.1");
         Assert.assertEquals(repo.getResources().size(), 1);
 
-        repo = Repository.newInstance("DJL", "djl://ai.djl.mxnet/resnet/0.0.1/resnet18_v1");
+        repo = Repository.newInstance("DJL", "djl://ai.djl.pytorch/resnet/0.0.1/traced_resnet18");
         Assert.assertEquals(repo.getResources().size(), 1);
 
-        repo = Repository.newInstance("DJL", "djl://ai.djl.mxnet/fake/0.0.1");
+        repo = Repository.newInstance("DJL", "djl://ai.djl.pytorch/fake/0.0.1");
         Assert.assertEquals(repo.getResources().size(), 0);
 
         repo = Repository.newInstance("DJL", "djl://ai.djl.fake/mlp/0.0.1");
@@ -46,19 +46,19 @@ public class DjlRepositoryTest {
 
         Assert.expectThrows(
                 IllegalArgumentException.class,
-                () -> Repository.newInstance("DJL", "djl://ai.djl.mxnet"));
+                () -> Repository.newInstance("DJL", "djl://ai.djl.pytorch"));
 
         Assert.expectThrows(
                 IllegalArgumentException.class,
-                () -> Repository.newInstance("DJL", "djl://ai.djl.mxnet/"));
+                () -> Repository.newInstance("DJL", "djl://ai.djl.pytorch/"));
 
         Criteria<Image, Classifications> criteria =
                 Criteria.builder()
                         .setTypes(Image.class, Classifications.class)
-                        .optModelUrls("djl://ai.djl.mxnet/resnet/0.0.1/resnet18_v1")
+                        .optModelUrls("djl://ai.djl.pytorch/resnet/0.0.1/traced_resnet18")
                         .build();
         try (ZooModel<Image, Classifications> model = criteria.loadModel()) {
-            Assert.assertEquals(model.getName(), "resnet18_v1");
+            Assert.assertEquals(model.getName(), "traced_resnet18");
         }
     }
 }
diff --git a/basicdataset/src/test/java/ai/djl/basicdataset/AirfoilRandomAccessTest.java b/basicdataset/src/test/java/ai/djl/basicdataset/AirfoilRandomAccessTest.java
index dc8028c5c..63ebbbbc1 100644
--- a/basicdataset/src/test/java/ai/djl/basicdataset/AirfoilRandomAccessTest.java
+++ b/basicdataset/src/test/java/ai/djl/basicdataset/AirfoilRandomAccessTest.java
@@ -100,8 +100,8 @@ public class AirfoilRandomAccessTest {
 
         float epsilon = (float) 1e-4;
 
-        float[] expected = {-0.6603f, -1.1448f, 1.797f, 1.3109f, -0.6443f};
+        float[] expected = {-0.6620f, -1.1464f, 1.7993f, 1.3129f, -0.6448f};
         Assert.assertEquals(data.head().toFloatArray(), expected, epsilon);
-        Assert.assertEquals(labels.head().toFloatArray(), new float[] {0.1937f}, epsilon);
+        Assert.assertEquals(labels.head().toFloatArray(), new float[] {0.1979f}, epsilon);
     }
 }
diff --git a/basicdataset/src/test/java/ai/djl/basicdataset/AmazonReviewsTest.java b/basicdataset/src/test/java/ai/djl/basicdataset/AmazonReviewsTest.java
index dd9c31ee6..b5f6318ac 100644
--- a/basicdataset/src/test/java/ai/djl/basicdataset/AmazonReviewsTest.java
+++ b/basicdataset/src/test/java/ai/djl/basicdataset/AmazonReviewsTest.java
@@ -35,8 +35,9 @@ public class AmazonReviewsTest {
             dataset.prepare();
 
             Record record = dataset.get(manager, 0);
-            Assert.assertEquals(record.getData().get(0).getFloat(), 0);
-            Assert.assertEquals(record.getLabels().get(0).getFloat(), 4.0);
+            Assert.assertEquals(
+                    record.getData().singletonOrThrow().toFloatArray(), new float[] {1});
+            Assert.assertEquals(record.getLabels().singletonOrThrow().getFloat(), 4.0);
         }
     }
 }
diff --git a/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/engine/DlrNDManagerTest.java b/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/engine/DlrNDManagerTest.java
index a8a5ca727..58ab0447b 100644
--- a/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/engine/DlrNDManagerTest.java
+++ b/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/engine/DlrNDManagerTest.java
@@ -25,6 +25,7 @@ public class DlrNDManagerTest {
     @Test
     public void testNDArray() {
         TestRequirements.notWindows();
+        TestRequirements.notArm();
 
         try (DlrNDManager manager =
                 (DlrNDManager) DlrNDManager.getSystemManager().newSubManager()) {
diff --git a/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/integration/DlrTest.java b/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/integration/DlrTest.java
index 190deed84..78b77c118 100644
--- a/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/integration/DlrTest.java
+++ b/engines/dlr/dlr-engine/src/test/java/ai/djl/dlr/integration/DlrTest.java
@@ -37,6 +37,7 @@ public class DlrTest {
     @Test
     public void testDlr() throws ModelException, IOException, TranslateException {
         TestRequirements.notWindows();
+        TestRequirements.notArm();
 
         String os;
         if (System.getProperty("os.name").toLowerCase().startsWith("mac")) {
diff --git a/engines/ml/xgboost/src/test/java/ai/djl/ml/xgboost/XgbModelTest.java b/engines/ml/xgboost/src/test/java/ai/djl/ml/xgboost/XgbModelTest.java
index c29853e90..95d1ce8a6 100644
--- a/engines/ml/xgboost/src/test/java/ai/djl/ml/xgboost/XgbModelTest.java
+++ b/engines/ml/xgboost/src/test/java/ai/djl/ml/xgboost/XgbModelTest.java
@@ -40,6 +40,8 @@ public class XgbModelTest {
 
     @BeforeClass
     public void downloadXGBoostModel() throws IOException {
+        TestRequirements.notArm();
+
         Path modelDir = Paths.get("build/model");
         DownloadUtils.download(
                 "https://resources.djl.ai/test-models/xgboost/regression.json",
diff --git a/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxBackendOptimizationTest.java b/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxBackendOptimizationTest.java
index 8899c2bdb..0cb147dbb 100644
--- a/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxBackendOptimizationTest.java
+++ b/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxBackendOptimizationTest.java
@@ -15,12 +15,15 @@ package ai.djl.mxnet.integration;
 import ai.djl.mxnet.engine.MxNDManager;
 import ai.djl.mxnet.engine.Symbol;
 import ai.djl.ndarray.NDManager;
+import ai.djl.testing.TestRequirements;
 import org.testng.annotations.Test;
 
 public class MxBackendOptimizationTest {
 
     @Test
     public void testOptimizedFor() {
+        TestRequirements.notArm();
+
         // TODO: Add Customized plugin test
         try (MxNDManager manager = (MxNDManager) NDManager.newBaseManager()) {
             Symbol symbol =
diff --git a/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxCoverageTest.java b/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxCoverageTest.java
index 801695cf3..be1f5f4c6 100644
--- a/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxCoverageTest.java
+++ b/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxCoverageTest.java
@@ -14,6 +14,7 @@ package ai.djl.mxnet.integration;
 
 import ai.djl.mxnet.engine.MxEngine;
 import ai.djl.testing.CoverageUtils;
+import ai.djl.testing.TestRequirements;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import org.testng.annotations.Test;
@@ -22,6 +23,8 @@ public class MxCoverageTest {
 
     @Test
     public void test() throws IOException, ReflectiveOperationException, URISyntaxException {
+        TestRequirements.notArm();
+
         // mxnet-engine
         CoverageUtils.testGetterSetters(MxEngine.class);
     }
diff --git a/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxGradientCollectorIntegrationTest.java b/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxGradientCollectorIntegrationTest.java
index bd03e0396..6ceba5b52 100644
--- a/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxGradientCollectorIntegrationTest.java
+++ b/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxGradientCollectorIntegrationTest.java
@@ -21,6 +21,7 @@ import ai.djl.ndarray.types.Shape;
 import ai.djl.nn.Blocks;
 import ai.djl.nn.Parameter;
 import ai.djl.testing.Assertions;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.DefaultTrainingConfig;
 import ai.djl.training.GradientCollector;
 import ai.djl.training.Trainer;
@@ -33,6 +34,8 @@ public class MxGradientCollectorIntegrationTest {
 
     @Test
     public void testMxAutograd() {
+        TestRequirements.notArm();
+
         try (Model model = Model.newInstance("model");
                 NDManager manager = model.getNDManager()) {
             model.setBlock(Blocks.identityBlock());
diff --git a/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxParameterStoreTest.java b/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxParameterStoreTest.java
index 6fa7ef0a8..f4ef63406 100644
--- a/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxParameterStoreTest.java
+++ b/engines/mxnet/mxnet-engine/src/test/java/ai/djl/mxnet/integration/MxParameterStoreTest.java
@@ -20,6 +20,7 @@ import ai.djl.ndarray.NDArray;
 import ai.djl.ndarray.NDManager;
 import ai.djl.ndarray.types.Shape;
 import ai.djl.testing.Assertions;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.ParameterServer;
 import ai.djl.training.optimizer.Optimizer;
 import ai.djl.training.tracker.Tracker;
@@ -30,6 +31,8 @@ public class MxParameterStoreTest {
 
     @Test
     public void testParameterStore() {
+        TestRequirements.notArm();
+
         try (Model model = Model.newInstance("model")) {
             NDManager manager = model.getNDManager();
             int numGpus = manager.getEngine().getGpuCount();
diff --git a/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxModelZooCoverageTest.java b/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxModelZooCoverageTest.java
index 6bf4bd252..c62a3ce9e 100644
--- a/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxModelZooCoverageTest.java
+++ b/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxModelZooCoverageTest.java
@@ -14,6 +14,7 @@ package ai.djl.mxnet.integration;
 
 import ai.djl.mxnet.zoo.MxModelZoo;
 import ai.djl.testing.CoverageUtils;
+import ai.djl.testing.TestRequirements;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import org.testng.annotations.Test;
@@ -22,6 +23,8 @@ public class MxModelZooCoverageTest {
 
     @Test
     public void test() throws IOException, ReflectiveOperationException, URISyntaxException {
+        TestRequirements.notArm();
+
         // mxnet-model-zoo
         CoverageUtils.testGetterSetters(MxModelZoo.class);
     }
diff --git a/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxSymbolBlockTest.java b/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxSymbolBlockTest.java
index 204ad2e0b..04bec7424 100644
--- a/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxSymbolBlockTest.java
+++ b/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/MxSymbolBlockTest.java
@@ -12,7 +12,7 @@
  */
 package ai.djl.mxnet.integration;
 
-import ai.djl.MalformedModelException;
+import ai.djl.ModelException;
 import ai.djl.engine.Engine;
 import ai.djl.inference.Predictor;
 import ai.djl.modality.Classifications;
@@ -29,7 +29,6 @@ import ai.djl.nn.SequentialBlock;
 import ai.djl.nn.SymbolBlock;
 import ai.djl.nn.core.Linear;
 import ai.djl.repository.zoo.Criteria;
-import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
 import ai.djl.testing.Assertions;
 import ai.djl.testing.TestRequirements;
@@ -54,8 +53,9 @@ public class MxSymbolBlockTest {
 
     @Test
     public void testSymbolModelInputOutput()
-            throws IOException, ModelNotFoundException, MalformedModelException,
-                    TranslateException {
+            throws IOException, ModelException, TranslateException {
+        TestRequirements.notArm();
+
         Criteria<Image, Classifications> criteria =
                 Criteria.builder()
                         .setTypes(Image.class, Classifications.class)
@@ -72,7 +72,9 @@ public class MxSymbolBlockTest {
     }
 
     @Test
-    public void testForward() throws IOException, ModelNotFoundException, MalformedModelException {
+    public void testForward() throws IOException, ModelException {
+        TestRequirements.notArm();
+
         Criteria<Image, Classifications> criteria =
                 Criteria.builder()
                         .setTypes(Image.class, Classifications.class)
@@ -94,8 +96,9 @@ public class MxSymbolBlockTest {
     }
 
     @Test
-    public void trainWithNewParam()
-            throws IOException, ModelNotFoundException, MalformedModelException {
+    public void trainWithNewParam() throws IOException, ModelException {
+        TestRequirements.notArm();
+
         if ("MXNet".equals(Engine.getDefaultEngineName())) {
             // TODO: WARN The gradMeans (but not predictions or loss) changed during the upgrade
             // to MXNet 1.8. The issue affect only CPU, but GPU has not changed.
@@ -134,8 +137,9 @@ public class MxSymbolBlockTest {
     }
 
     @Test
-    public void trainWithExistParam()
-            throws IOException, ModelNotFoundException, MalformedModelException {
+    public void trainWithExistParam() throws IOException, ModelException {
+        TestRequirements.notArm();
+
         TrainingConfig config =
                 new DefaultTrainingConfig(Loss.softmaxCrossEntropyLoss())
                         .optInitializer(Initializer.ONES, Parameter.Type.WEIGHT);
@@ -167,8 +171,9 @@ public class MxSymbolBlockTest {
     }
 
     @Test
-    public void trainWithCustomLayer()
-            throws IOException, ModelNotFoundException, MalformedModelException {
+    public void trainWithCustomLayer() throws IOException, ModelException {
+        TestRequirements.notArm();
+
         TrainingConfig config =
                 new DefaultTrainingConfig(Loss.softmaxCrossEntropyLoss())
                         .optInitializer(Initializer.ONES, Parameter.Type.WEIGHT);
@@ -209,6 +214,8 @@ public class MxSymbolBlockTest {
     }
 
     private Pair<NDArray, NDArray> train(NDManager manager, Trainer trainer, Block block) {
+        TestRequirements.notArm();
+
         Shape inputShape = new Shape(10, 28 * 28);
         trainer.initialize(inputShape);
 
diff --git a/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/modality/nlp/WordEmbeddingTest.java b/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/modality/nlp/WordEmbeddingTest.java
index 3e99e7e1c..15c6d5dfb 100644
--- a/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/modality/nlp/WordEmbeddingTest.java
+++ b/engines/mxnet/mxnet-model-zoo/src/test/java/ai/djl/mxnet/integration/modality/nlp/WordEmbeddingTest.java
@@ -12,14 +12,14 @@
  */
 package ai.djl.mxnet.integration.modality.nlp;
 
-import ai.djl.MalformedModelException;
+import ai.djl.ModelException;
 import ai.djl.modality.nlp.embedding.EmbeddingException;
 import ai.djl.modality.nlp.embedding.ModelZooTextEmbedding;
 import ai.djl.ndarray.NDList;
 import ai.djl.ndarray.NDManager;
 import ai.djl.repository.zoo.Criteria;
-import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import java.io.IOException;
 import java.util.Collections;
 import org.testng.Assert;
@@ -28,9 +28,9 @@ import org.testng.annotations.Test;
 public class WordEmbeddingTest {
 
     @Test
-    public void testGlove()
-            throws IOException, ModelNotFoundException, MalformedModelException,
-                    EmbeddingException {
+    public void testGlove() throws IOException, ModelException, EmbeddingException {
+        TestRequirements.notArm();
+
         Criteria<String, NDList> criteria =
                 Criteria.builder()
                         .setTypes(String.class, NDList.class)
diff --git a/engines/onnxruntime/onnxruntime-engine/src/test/java/ai/djl/onnxruntime/engine/OrtTest.java b/engines/onnxruntime/onnxruntime-engine/src/test/java/ai/djl/onnxruntime/engine/OrtTest.java
index 6d6c709f0..6fe36e8d2 100644
--- a/engines/onnxruntime/onnxruntime-engine/src/test/java/ai/djl/onnxruntime/engine/OrtTest.java
+++ b/engines/onnxruntime/onnxruntime-engine/src/test/java/ai/djl/onnxruntime/engine/OrtTest.java
@@ -12,7 +12,6 @@
  */
 package ai.djl.onnxruntime.engine;
 
-import ai.djl.MalformedModelException;
 import ai.djl.Model;
 import ai.djl.ModelException;
 import ai.djl.inference.Predictor;
@@ -23,8 +22,8 @@ import ai.djl.ndarray.NDManager;
 import ai.djl.ndarray.types.Shape;
 import ai.djl.onnxruntime.zoo.tabular.softmax_regression.IrisFlower;
 import ai.djl.repository.zoo.Criteria;
-import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.io.InputStream;
@@ -38,6 +37,8 @@ public class OrtTest {
 
     @Test
     public void testOrt() throws TranslateException, ModelException, IOException {
+        TestRequirements.notArm();
+
         try {
             Criteria<IrisFlower, Classifications> criteria =
                     Criteria.builder()
@@ -86,6 +87,8 @@ public class OrtTest {
 
     @Test
     public void testNDArray() {
+        TestRequirements.notArm();
+
         try (NDManager manager = OrtNDManager.getSystemManager().newSubManager()) {
             NDArray zeros = manager.zeros(new Shape(1, 2));
             float[] data = zeros.toFloatArray();
@@ -110,9 +113,9 @@ public class OrtTest {
     }
 
     @Test
-    public void testStringTensor()
-            throws MalformedModelException, ModelNotFoundException, IOException,
-                    TranslateException {
+    public void testStringTensor() throws ModelException, IOException, TranslateException {
+        TestRequirements.notArm();
+
         System.setProperty("ai.djl.onnx.disable_alternative", "true");
         Criteria<NDList, NDList> criteria =
                 Criteria.builder()
diff --git a/engines/paddlepaddle/paddlepaddle-engine/src/test/java/ai/djl/paddlepaddle/jni/JniUtilsTest.java b/engines/paddlepaddle/paddlepaddle-engine/src/test/java/ai/djl/paddlepaddle/jni/JniUtilsTest.java
index dc8e76b63..2a9680af4 100644
--- a/engines/paddlepaddle/paddlepaddle-engine/src/test/java/ai/djl/paddlepaddle/jni/JniUtilsTest.java
+++ b/engines/paddlepaddle/paddlepaddle-engine/src/test/java/ai/djl/paddlepaddle/jni/JniUtilsTest.java
@@ -16,6 +16,7 @@ import ai.djl.ndarray.NDArray;
 import ai.djl.ndarray.NDManager;
 import ai.djl.ndarray.types.Shape;
 import ai.djl.paddlepaddle.engine.PpNDArray;
+import ai.djl.testing.TestRequirements;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -23,6 +24,8 @@ public class JniUtilsTest {
 
     @Test
     void createNDArray() {
+        TestRequirements.notArm();
+
         try (NDManager manager = NDManager.newBaseManager(null, "PaddlePaddle")) {
             NDArray array = manager.zeros(new Shape(1, 2));
             float[] expected = new float[] {0, 0};
@@ -38,6 +41,8 @@ public class JniUtilsTest {
 
     @Test
     void testLoD() {
+        TestRequirements.notArm();
+
         try (NDManager manager = NDManager.newBaseManager(null, "PaddlePaddle")) {
             PpNDArray array = (PpNDArray) manager.zeros(new Shape(1, 4));
             long[][] lodInfo = new long[][] {new long[] {1, 3}};
diff --git a/engines/paddlepaddle/paddlepaddle-engine/src/test/java/ai/djl/paddlepaddle/jni/PpEngineTest.java b/engines/paddlepaddle/paddlepaddle-engine/src/test/java/ai/djl/paddlepaddle/jni/PpEngineTest.java
new file mode 100644
index 000000000..6bcea6687
--- /dev/null
+++ b/engines/paddlepaddle/paddlepaddle-engine/src/test/java/ai/djl/paddlepaddle/jni/PpEngineTest.java
@@ -0,0 +1,25 @@
+/*
+ * 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.paddlepaddle.jni;
+
+import ai.djl.paddlepaddle.engine.PpEngine;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class PpEngineTest {
+    @Test
+    public void testGetVersion() {
+        String version = PpEngine.getInstance().getVersion();
+        Assert.assertEquals(version.split("\\.").length, 3);
+    }
+}
diff --git a/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/MaskDetectionTest.java b/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/MaskDetectionTest.java
index 6924c0cb1..4ca17f23e 100644
--- a/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/MaskDetectionTest.java
+++ b/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/MaskDetectionTest.java
@@ -24,6 +24,7 @@ import ai.djl.modality.cv.output.Rectangle;
 import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -38,6 +39,8 @@ public class MaskDetectionTest {
 
     @Test
     public void testMaskDetection() throws IOException, ModelException, TranslateException {
+        TestRequirements.notArm();
+
         String url =
                 "https://raw.githubusercontent.com/PaddlePaddle/PaddleHub/release/v1.5/demo/mask_detection/python/images/mask.jpg";
         Image img = ImageFactory.getInstance().fromUrl(url);
diff --git a/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/OCRTest.java b/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/OCRTest.java
index 99304bb88..6ab724a70 100644
--- a/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/OCRTest.java
+++ b/engines/paddlepaddle/paddlepaddle-model-zoo/src/test/java/ai/djl/paddlepaddle/zoo/OCRTest.java
@@ -13,7 +13,6 @@
 
 package ai.djl.paddlepaddle.zoo;
 
-import ai.djl.MalformedModelException;
 import ai.djl.ModelException;
 import ai.djl.inference.Predictor;
 import ai.djl.modality.Classifications;
@@ -26,8 +25,8 @@ import ai.djl.modality.cv.util.NDImageUtils;
 import ai.djl.ndarray.NDArray;
 import ai.djl.ndarray.NDManager;
 import ai.djl.repository.zoo.Criteria;
-import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.util.List;
@@ -37,6 +36,8 @@ public class OCRTest {
 
     @Test
     public void testOCR() throws IOException, ModelException, TranslateException {
+        TestRequirements.notArm();
+
         // load Character model
         Predictor<Image, DetectedObjects> detector = getWordDetector();
         Predictor<Image, String> recognizer = getRecognizer();
@@ -61,7 +62,9 @@ public class OCRTest {
     }
 
     private static Predictor<Image, DetectedObjects> getWordDetector()
-            throws ModelNotFoundException, MalformedModelException, IOException {
+            throws ModelException, IOException {
+        TestRequirements.notArm();
+
         Criteria<Image, DetectedObjects> criteria =
                 Criteria.builder()
                         .setTypes(Image.class, DetectedObjects.class)
@@ -71,8 +74,9 @@ public class OCRTest {
         return criteria.loadModel().newPredictor();
     }
 
-    private static Predictor<Image, String> getRecognizer()
-            throws MalformedModelException, ModelNotFoundException, IOException {
+    private static Predictor<Image, String> getRecognizer() throws ModelException, IOException {
+        TestRequirements.notArm();
+
         Criteria<Image, String> criteria =
                 Criteria.builder()
                         .setTypes(Image.class, String.class)
@@ -85,7 +89,7 @@ public class OCRTest {
     }
 
     private static Predictor<Image, Classifications> getRotateClassifer()
-            throws MalformedModelException, ModelNotFoundException, IOException {
+            throws ModelException, IOException {
         Criteria<Image, Classifications> criteria =
                 Criteria.builder()
                         .setTypes(Image.class, Classifications.class)
diff --git a/engines/tensorflow/tensorflow-engine/src/test/java/ai/djl/tensorflow/engine/TfNDManagerTest.java b/engines/tensorflow/tensorflow-engine/src/test/java/ai/djl/tensorflow/engine/TfNDManagerTest.java
index b947cf56d..13b981f0a 100644
--- a/engines/tensorflow/tensorflow-engine/src/test/java/ai/djl/tensorflow/engine/TfNDManagerTest.java
+++ b/engines/tensorflow/tensorflow-engine/src/test/java/ai/djl/tensorflow/engine/TfNDManagerTest.java
@@ -16,6 +16,7 @@ import ai.djl.ndarray.NDArray;
 import ai.djl.ndarray.NDManager;
 import ai.djl.ndarray.types.DataType;
 import ai.djl.ndarray.types.Shape;
+import ai.djl.testing.TestRequirements;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import org.testng.Assert;
@@ -25,6 +26,8 @@ public class TfNDManagerTest {
 
     @Test
     public void testNDArray() {
+        TestRequirements.notArm();
+
         try (NDManager manager = NDManager.newBaseManager()) {
             NDArray array = manager.create("string");
             Assert.assertEquals(array.toStringArray()[0], "string");
diff --git a/engines/tensorflow/tensorflow-engine/src/test/java/ai/djl/tensorflow/integration/TfCoverageTest.java b/engines/tensorflow/tensorflow-engine/src/test/java/ai/djl/tensorflow/integration/TfCoverageTest.java
index dede06953..e6ece4bcc 100644
--- a/engines/tensorflow/tensorflow-engine/src/test/java/ai/djl/tensorflow/integration/TfCoverageTest.java
+++ b/engines/tensorflow/tensorflow-engine/src/test/java/ai/djl/tensorflow/integration/TfCoverageTest.java
@@ -15,6 +15,7 @@ package ai.djl.tensorflow.integration;
 import ai.djl.engine.Engine;
 import ai.djl.tensorflow.engine.TfEngine;
 import ai.djl.testing.CoverageUtils;
+import ai.djl.testing.TestRequirements;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import org.testng.annotations.Test;
@@ -23,6 +24,8 @@ public class TfCoverageTest {
 
     @Test
     public void test() throws IOException, ReflectiveOperationException, URISyntaxException {
+        TestRequirements.notArm();
+
         // tensorflow-engine
         Engine.getEngine("TensorFlow");
         CoverageUtils.testGetterSetters(TfEngine.class);
diff --git a/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/MobileNetTest.java b/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/MobileNetTest.java
index 40c4c5bdb..d3f79abb2 100644
--- a/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/MobileNetTest.java
+++ b/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/MobileNetTest.java
@@ -20,6 +20,7 @@ import ai.djl.modality.cv.Image;
 import ai.djl.modality.cv.ImageFactory;
 import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.ProgressBar;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
@@ -32,6 +33,8 @@ public class MobileNetTest {
 
     @Test
     public void testMobileNetV2() throws IOException, ModelException, TranslateException {
+        TestRequirements.notArm();
+
         Criteria<Image, Classifications> criteria =
                 Criteria.builder()
                         .optApplication(Application.CV.IMAGE_CLASSIFICATION)
diff --git a/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/ResNetTest.java b/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/ResNetTest.java
index 10dbfa221..bf1c3f166 100644
--- a/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/ResNetTest.java
+++ b/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/ResNetTest.java
@@ -20,6 +20,7 @@ import ai.djl.modality.cv.Image;
 import ai.djl.modality.cv.ImageFactory;
 import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.ProgressBar;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
@@ -32,6 +33,8 @@ public class ResNetTest {
 
     @Test
     public void testResNet50V1() throws IOException, ModelException, TranslateException {
+        TestRequirements.notArm();
+
         Criteria<Image, Classifications> criteria =
                 Criteria.builder()
                         .optApplication(Application.CV.IMAGE_CLASSIFICATION)
diff --git a/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/TfSsdTest.java b/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/TfSsdTest.java
index d5cf7cb9e..bc5921194 100644
--- a/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/TfSsdTest.java
+++ b/engines/tensorflow/tensorflow-model-zoo/src/test/java/ai/djl/tensorflow/integration/modality/cv/TfSsdTest.java
@@ -22,6 +22,7 @@ import ai.djl.modality.cv.output.DetectedObjects;
 import ai.djl.ndarray.types.Shape;
 import ai.djl.repository.zoo.Criteria;
 import ai.djl.repository.zoo.ZooModel;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.ProgressBar;
 import ai.djl.translate.TranslateException;
 import ai.djl.util.Pair;
@@ -38,6 +39,8 @@ public class TfSsdTest {
 
     @Test
     public void testTfSSD() throws IOException, ModelException, TranslateException {
+        TestRequirements.notArm();
+
         Criteria<Image, DetectedObjects> criteria =
                 Criteria.builder()
                         .optApplication(Application.CV.OBJECT_DETECTION)
diff --git a/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteNDManagerTest.java b/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteNDManagerTest.java
index 42ab68356..bea89e80f 100644
--- a/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteNDManagerTest.java
+++ b/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteNDManagerTest.java
@@ -24,6 +24,7 @@ public class TfLiteNDManagerTest {
     @Test
     public void testNDArray() {
         TestRequirements.notWindows();
+        TestRequirements.notArm();
 
         try (NDManager manager = TfLiteNDManager.getSystemManager().newSubManager()) {
             NDArray zeros = manager.zeros(new Shape(1, 2));
diff --git a/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteTest.java b/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteTest.java
index 1a3650f87..6188246af 100644
--- a/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteTest.java
+++ b/engines/tflite/tflite-engine/src/test/java/ai/djl/tflite/engine/TfLiteTest.java
@@ -12,14 +12,13 @@
  */
 package ai.djl.tflite.engine;
 
-import ai.djl.MalformedModelException;
 import ai.djl.Model;
+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.repository.zoo.Criteria;
-import ai.djl.repository.zoo.ModelNotFoundException;
 import ai.djl.repository.zoo.ZooModel;
 import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
@@ -33,10 +32,9 @@ import org.testng.annotations.Test;
 public class TfLiteTest {
 
     @Test
-    void testTflite()
-            throws IOException, MalformedModelException, TranslateException,
-                    ModelNotFoundException {
+    void testTflite() throws IOException, TranslateException, ModelException {
         TestRequirements.notWindows();
+        TestRequirements.notArm();
 
         Criteria<Image, Classifications> criteria =
                 Criteria.builder()
diff --git a/examples/src/test/java/ai/djl/examples/inference/ObjectDetectionTest.java b/examples/src/test/java/ai/djl/examples/inference/ObjectDetectionTest.java
index bf2e85f28..5ce01b5dc 100644
--- a/examples/src/test/java/ai/djl/examples/inference/ObjectDetectionTest.java
+++ b/examples/src/test/java/ai/djl/examples/inference/ObjectDetectionTest.java
@@ -15,6 +15,7 @@ package ai.djl.examples.inference;
 import ai.djl.ModelException;
 import ai.djl.modality.Classifications;
 import ai.djl.modality.cv.output.DetectedObjects;
+import ai.djl.testing.TestRequirements;
 import ai.djl.translate.TranslateException;
 import java.io.IOException;
 import java.util.Arrays;
@@ -30,6 +31,8 @@ public class ObjectDetectionTest {
 
     @Test
     public void testObjectDetection() throws ModelException, TranslateException, IOException {
+        TestRequirements.engine("MXNet", "PyTorch", "TensorFlow");
+
         DetectedObjects result = ObjectDetection.predict();
         logger.info("{}", result);
 
diff --git a/examples/src/test/java/ai/djl/testing/TestRequirements.java b/examples/src/test/java/ai/djl/testing/TestRequirements.java
index 448f6aed5..80547fb6e 100644
--- a/examples/src/test/java/ai/djl/testing/TestRequirements.java
+++ b/examples/src/test/java/ai/djl/testing/TestRequirements.java
@@ -13,6 +13,7 @@
 package ai.djl.testing;
 
 import ai.djl.engine.Engine;
+import ai.djl.engine.EngineException;
 import java.util.Arrays;
 import java.util.Calendar;
 import org.testng.SkipException;
@@ -56,6 +57,11 @@ public final class TestRequirements {
         String engineName = Engine.getDefaultEngineName();
         for (String e : engines) {
             if (engineName.equals(e)) {
+                try {
+                    Engine.getEngine(engineName);
+                } catch (EngineException ex) {
+                    throw new SkipException("Engine cannot be loaded.", ex);
+                }
                 return;
             }
         }
diff --git a/extensions/audio/src/test/java/ai/djl/audio/processor/AudioProcessorTest.java b/extensions/audio/src/test/java/ai/djl/audio/processor/AudioProcessorTest.java
index 28cec1b62..cb4748b25 100644
--- a/extensions/audio/src/test/java/ai/djl/audio/processor/AudioProcessorTest.java
+++ b/extensions/audio/src/test/java/ai/djl/audio/processor/AudioProcessorTest.java
@@ -19,6 +19,7 @@ import ai.djl.audio.dataset.AudioData;
 import ai.djl.ndarray.NDArray;
 import ai.djl.ndarray.NDManager;
 import ai.djl.ndarray.types.Shape;
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.DownloadUtils;
 import java.io.IOException;
 import java.util.Arrays;
@@ -33,6 +34,7 @@ public class AudioProcessorTest {
 
     @BeforeClass
     public void setUp() throws IOException {
+        TestRequirements.notArm();
         DownloadUtils.download(URL, "build/test/test_01.wav");
     }
 
diff --git a/extensions/aws-ai/src/test/java/ai/djl/aws/s3/S3RepositoryTest.java b/extensions/aws-ai/src/test/java/ai/djl/aws/s3/S3RepositoryTest.java
index 96ab442f1..7afc9e17e 100644
--- a/extensions/aws-ai/src/test/java/ai/djl/aws/s3/S3RepositoryTest.java
+++ b/extensions/aws-ai/src/test/java/ai/djl/aws/s3/S3RepositoryTest.java
@@ -51,13 +51,12 @@ public class S3RepositoryTest {
         Criteria<NDList, NDList> criteria =
                 Criteria.builder()
                         .setTypes(NDList.class, NDList.class)
-                        .optModelUrls(
-                                "s3://djl-ai/mlrepo/model/cv/image_classification/ai/djl/mxnet/mlp/0.0.1")
-                        .optModelName("mlp")
+                        .optModelUrls("s3://djl-ai/resources/demo/pytorch/traced_resnet18")
+                        .optModelName("traced_resnet18")
                         .build();
 
         try (ZooModel<NDList, NDList> model = criteria.loadModel()) {
-            Assert.assertEquals(model.getName(), "mlp");
+            Assert.assertEquals(model.getName(), "traced_resnet18");
         }
     }
 
@@ -75,12 +74,12 @@ public class S3RepositoryTest {
         Criteria<NDList, NDList> criteria =
                 Criteria.builder()
                         .setTypes(NDList.class, NDList.class)
-                        .optModelUrls("s3://djl-ai/resources/test-models/mlp.tar.gz")
-                        .optModelName("mlp")
+                        .optModelUrls("s3://djl-ai/resources/demo/pytorch/traced_resnet18.zip")
+                        .optModelName("traced_resnet18")
                         .build();
 
         try (ZooModel<NDList, NDList> model = criteria.loadModel()) {
-            Assert.assertEquals(model.getName(), "mlp");
+            Assert.assertEquals(model.getName(), "traced_resnet18");
         }
     }
 
diff --git a/extensions/benchmark/src/test/java/ai/djl/benchmark/BenchmarkTest.java b/extensions/benchmark/src/test/java/ai/djl/benchmark/BenchmarkTest.java
index 931d609cc..5602b6c35 100644
--- a/extensions/benchmark/src/test/java/ai/djl/benchmark/BenchmarkTest.java
+++ b/extensions/benchmark/src/test/java/ai/djl/benchmark/BenchmarkTest.java
@@ -79,7 +79,14 @@ public class BenchmarkTest {
     @Test
     public void testBenchmark() {
         String[] args = {
-            "-u", "djl://ai.djl.mxnet/resnet/0.0.1/resnet18_v1", "-s", "1,3,224,224", "-c", "2"
+            "-e",
+            "PyTorch",
+            "-u",
+            "djl://ai.djl.pytorch/resnet/0.0.1/traced_resnet18",
+            "-s",
+            "1,3,224,224",
+            "-c",
+            "2"
         };
         new Benchmark().runBenchmark(args);
     }
@@ -90,9 +97,9 @@ public class BenchmarkTest {
         try {
             String[] args = {
                 "-e",
-                "MXNet",
+                "PyTorch",
                 "-u",
-                "djl://ai.djl.mxnet/resnet/0.0.1/resnet18_v1",
+                "djl://ai.djl.pytorch/resnet/0.0.1/traced_resnet18",
                 "-s",
                 "(1,3,224,224)f",
                 "-d",
diff --git a/extensions/fasttext/src/test/java/ai/djl/fasttext/CookingStackExchangeTest.java b/extensions/fasttext/src/test/java/ai/djl/fasttext/CookingStackExchangeTest.java
index c0bb2fc34..73ba63d4a 100644
--- a/extensions/fasttext/src/test/java/ai/djl/fasttext/CookingStackExchangeTest.java
+++ b/extensions/fasttext/src/test/java/ai/djl/fasttext/CookingStackExchangeTest.java
@@ -52,8 +52,9 @@ public class CookingStackExchangeTest {
     private static final Logger logger = LoggerFactory.getLogger(CookingStackExchangeTest.class);
 
     @Test
-    public void testTrainTextClassification() throws IOException, TranslateException {
+    public void testTrainTextClassification() throws IOException {
         TestRequirements.notWindows(); // fastText is not supported on windows
+        TestRequirements.notArm();
 
         CookingStackExchange dataset = CookingStackExchange.builder().build();
 
@@ -77,6 +78,7 @@ public class CookingStackExchangeTest {
             throws IOException, MalformedModelException, ModelNotFoundException,
                     TranslateException {
         TestRequirements.notWindows(); // fastText is not supported on windows
+        TestRequirements.notArm();
 
         Criteria<String, Classifications> criteria =
                 Criteria.builder()
@@ -102,6 +104,7 @@ public class CookingStackExchangeTest {
     @Test
     public void testWord2Vec() throws IOException, MalformedModelException, ModelNotFoundException {
         TestRequirements.notWindows(); // fastText is not supported on windows
+        TestRequirements.notArm();
 
         Criteria<String, Classifications> criteria =
                 Criteria.builder()
@@ -124,6 +127,7 @@ public class CookingStackExchangeTest {
     @Test
     public void testBlazingText() throws IOException, ModelException {
         TestRequirements.notWindows(); // fastText is not supported on windows
+        TestRequirements.notArm();
         TestRequirements.nightly();
 
         URL url = new URL("https://resources.djl.ai/test-models/blazingtext_classification.bin");
diff --git a/extensions/opencv/src/test/java/ai/djl/opencv/OpenCVImageFactoryTest.java b/extensions/opencv/src/test/java/ai/djl/opencv/OpenCVImageFactoryTest.java
index 9e7e47dfe..096e3577b 100644
--- a/extensions/opencv/src/test/java/ai/djl/opencv/OpenCVImageFactoryTest.java
+++ b/extensions/opencv/src/test/java/ai/djl/opencv/OpenCVImageFactoryTest.java
@@ -42,6 +42,7 @@ public class OpenCVImageFactoryTest {
     @Test
     public void testImage() throws IOException {
         TestRequirements.notWindows(); // failed on Windows ServerCore container
+        TestRequirements.notArm();
 
         ImageFactory factory = ImageFactory.getInstance();
         ImageFactory defFactory = new BufferedImageFactory();
@@ -129,6 +130,8 @@ public class OpenCVImageFactoryTest {
     @Test
     public void testBoundingBoxes() {
         TestRequirements.notWindows(); // failed on Windows ServerCore container
+        TestRequirements.notArm();
+
         ImageFactory factory = ImageFactory.getInstance();
         try (NDManager manager = NDManager.newBaseManager()) {
             int[][] arr =
diff --git a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTextEmbeddingTest.java b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTextEmbeddingTest.java
index 18c4fb8c3..8ddf25b97 100644
--- a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTextEmbeddingTest.java
+++ b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTextEmbeddingTest.java
@@ -12,6 +12,7 @@
  */
 package ai.djl.sentencepiece;
 
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.DownloadUtils;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -25,6 +26,8 @@ import org.testng.annotations.Test;
 public class SpTextEmbeddingTest {
     @BeforeTest
     public void downloadModel() throws IOException {
+        TestRequirements.notArm();
+
         Path modelFile = Paths.get("build/test/models/sententpiece_test_model.model");
         if (Files.notExists(modelFile)) {
             DownloadUtils.download(
diff --git a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTokenizerTest.java b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTokenizerTest.java
index 9838db194..635cf6ac6 100644
--- a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTokenizerTest.java
+++ b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpTokenizerTest.java
@@ -29,6 +29,8 @@ public class SpTokenizerTest {
 
     @BeforeTest
     public void downloadModel() throws IOException {
+        TestRequirements.notArm();
+
         Path modelFile = Paths.get("build/test/sp_model/sp_model.model");
         if (Files.notExists(modelFile)) {
             DownloadUtils.download(
diff --git a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpVocabularyTest.java b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpVocabularyTest.java
index fbc4d4970..2b1acc0d1 100644
--- a/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpVocabularyTest.java
+++ b/extensions/sentencepiece/src/test/java/ai/djl/sentencepiece/SpVocabularyTest.java
@@ -13,6 +13,7 @@
 
 package ai.djl.sentencepiece;
 
+import ai.djl.testing.TestRequirements;
 import ai.djl.training.util.DownloadUtils;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -26,6 +27,8 @@ public class SpVocabularyTest {
 
     @BeforeTest
     public void downloadModel() throws IOException {
+        TestRequirements.notArm();
+
         Path modelFile = Paths.get("build/test/models/sententpiece_test_model.model");
         if (Files.notExists(modelFile)) {
             DownloadUtils.download(
diff --git a/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/HuggingFaceTokenizerTest.java b/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/HuggingFaceTokenizerTest.java
index b4cd211bb..2dd1e5156 100644
--- a/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/HuggingFaceTokenizerTest.java
+++ b/extensions/tokenizers/src/test/java/ai/djl/huggingface/tokenizers/HuggingFaceTokenizerTest.java
@@ -14,6 +14,7 @@
 package ai.djl.huggingface.tokenizers;
 
 import ai.djl.huggingface.tokenizers.jni.CharSpan;
+import ai.djl.testing.TestRequirements;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -25,6 +26,8 @@ public class HuggingFaceTokenizerTest {
 
     @Test
     public void testTokenizer() {
+        TestRequirements.notArm();
+
         String input = "Hello, y'all! How are you 😁 ?";
         String[] inputs = {"Hello, y'all!", "How are you 😁 ?"};
 
diff --git a/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayAttachmentTest.java b/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayAttachmentTest.java
index e83bceb05..65fbb1990 100644
--- a/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayAttachmentTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayAttachmentTest.java
@@ -48,4 +48,37 @@ public class NDArrayAttachmentTest {
             }
         }
     }
+
+    @Test(expectedExceptions = IllegalStateException.class)
+    public void testCannotUseCappedManager() {
+        try (NDManager manager = NDManager.newBaseManager()) {
+            manager.cap();
+            manager.ones(new Shape(3, 4));
+        }
+    }
+
+    @Test(expectedExceptions = IllegalStateException.class)
+    public void testIndexationCannotUseCappedManager() {
+        try (NDManager manager = NDManager.newBaseManager()) {
+            NDArray array3x4 = manager.ones(new Shape(3, 4));
+            array3x4.setName("Test()");
+            manager.cap();
+            array3x4.get(1);
+        }
+    }
+
+    @Test
+    public void testIndexationUsesSpecificUncappedManager() {
+        try (NDManager manager = NDManager.newBaseManager()) {
+            NDArray array3x4 = manager.ones(new Shape(3, 4));
+            array3x4.setName("Test()");
+            manager.cap();
+            try (NDManager subManager = NDManager.newBaseManager()) {
+                NDArray array4sub1 = array3x4.get(subManager, 1);
+                Assert.assertEquals(array4sub1.getManager(), subManager);
+                array3x4.tempAttach(subManager);
+                Assert.assertEquals(array3x4.getManager(), subManager);
+            }
+        }
+    }
 }
diff --git a/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayNumericOpTest.java b/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayNumericOpTest.java
index fc0d41001..d50bb8536 100644
--- a/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayNumericOpTest.java
+++ b/integration/src/main/java/ai/djl/integration/tests/ndarray/NDArrayNumericOpTest.java
@@ -340,7 +340,7 @@ public class NDArrayNumericOpTest {
             array = manager.create(data, new Shape(2, 3));
             data = DoubleStream.of(data).map(Math::sin).toArray();
             expected = manager.create(data, new Shape(2, 3));
-            Assert.assertEquals(array.sin(), expected);
+            Assertions.assertAlmostEquals(array.sin(), expected);
             // test scalar
             array = manager.create(0.5 * Math.PI);
             expected = manager.create(1.0);
diff --git a/integration/src/test/java/ai/djl/integration/IntegrationTests.java b/integration/src/test/java/ai/djl/integration/IntegrationTests.java
index 090a6369e..490d0af77 100644
--- a/integration/src/test/java/ai/djl/integration/IntegrationTests.java
+++ b/integration/src/test/java/ai/djl/integration/IntegrationTests.java
@@ -33,6 +33,8 @@ public class IntegrationTests {
             // together
             if (System.getProperty("os.name").startsWith("Win")) {
                 engines = new String[] {"MXNet"};
+            } else if ("aarch64".equals(System.getProperty("os.arch"))) {
+                engines = new String[] {"PyTorch"};
             } else {
                 engines = new String[] {"MXNet", "PyTorch", "TensorFlow", "XGBoost"};
             }
diff --git a/testing/src/main/java/ai/djl/testing/TestRequirements.java b/testing/src/main/java/ai/djl/testing/TestRequirements.java
index 448f6aed5..a5975d583 100644
--- a/testing/src/main/java/ai/djl/testing/TestRequirements.java
+++ b/testing/src/main/java/ai/djl/testing/TestRequirements.java
@@ -91,4 +91,11 @@ public final class TestRequirements {
             throw new SkipException("This test requires a non-windows os.");
         }
     }
+
+    /** Requires that the test runs on x86_64 arch. */
+    public static void notArm() {
+        if ("aarch64".equals(System.getProperty("os.arch"))) {
+            throw new SkipException("This test requires a non-arm os.");
+        }
+    }
 }