Skip to content

ModuleListImpl erases concrete subclass types of stored Modules, causing ClassCastException when casting back to custom layer subclasses like MLP #1783

Description

@mullerhai

Hi @saudet

Problem Description

When iterating elements from ModuleListImpl via its iterator / .get() method, all returned objects are exposed as the base type org.bytedeco.pytorch.Module with full type erasure at the JNI boundary.
Even though the underlying native module is our custom Scala layer (e.g. torchrec.basic.layers.MLP which extends Module), attempting to cast the returned Module reference back to the concrete subclass throws a hard ClassCastException.
Exception Stack
plaintext
Exception in thread "main" java.lang.ClassCastException: class org.bytedeco.pytorch.Module cannot be cast to class torchrec.basic.layers.MLP
(org.bytedeco.pytorch.Module and torchrec.basic.layers.MLP are in unnamed module of loader 'app')
Root Cause Analysis
ModuleListImpl JNI accessor only returns the generic base Module handle without retaining runtime subclass metadata of wrapped custom layers;
All custom layers (MLP, CapsuleNetwork, CrossLayer etc.) inherit from Module, but JNI layer type information is lost after insertion into ModuleList;
Runtime reflection confirms the underlying native object is indeed the target subclass (MLP), yet direct Java/Scala cast fails due to mismatched JVM wrapper class identities;
Since the returned reference is only typed as plain Module, we cannot invoke subclass-specific forward() methods or layer APIs without successful casting, breaking sequential forward propagation logic for stacked layers stored inside ModuleList.
Minimal Reproduction Code (Scala JavaCPP-PyTorch)
scala
import org.bytedeco.pytorch._
import torchrec.basic.layers.MLP

// 1. Create custom concrete layer subclass of Module
val mlpLayer = new MLP(inputDim=8, hiddenDims=Seq(16), outputDim=1)

// 2. Add to ModuleListImpl
val moduleList = new ModuleListImpl()
moduleList.push_back("mlp_block", mlpLayer)

// 3. Retrieve element, only typed as base Module
val retrieved: Module = moduleList.get(0)

// 4. ClassCastException thrown here
val casted: MLP = retrieved.asInstanceOf[MLP]
Expected Behavior
Elements fetched from ModuleListImpl should preserve runtime subclass type information of custom user-defined layers that extend Module;
Safe casting from the returned handle back to the original concrete layer subclass should be allowed without ClassCastException;
Provide a supported API to unwrap the actual custom layer implementation stored inside ModuleList, enabling access to subclass-specific forward() and layer attributes.
Workaround Limitation
Pure Java reflection cannot bypass this cast error, because the JNI-wrapped Module wrapper instance is a distinct JVM class from our custom Scala layer class, even if they point to the same underlying C++ module object. There is no trivial user-land workaround to restore the concrete layer type after retrieval from ModuleListImpl.

class MLP(
  inputDim: Long,
  hiddenDims: List[Long],
  outputDim: Long = 1,
  activation: String = "relu",
  dropout: Float = 0.0f,
  useBatchNorm: Boolean = false,
  useLayerNorm: Boolean = false,
  outputLayer: Boolean = true,
  device: String = DeviceSupport.backend
) extends Module {

  // Use SequentialImpl like PyTorch's nn.Sequential
  private val sequential = new SequentialImpl()
  private var prevDim = inputDim
  private val layersRef = scala.collection.mutable.ListBuffer[AnyRef]()
  // Build MLP layers using SequentialImpl

  hiddenDims.foreach { dim =>
    sequential.push_back(new LinearImpl(prevDim, dim))

    if (useLayerNorm) {
      val vec = new LongVector(1)
      vec.put(0, dim)
      sequential.push_back(new LayerNormImpl(vec))
    } else if (useBatchNorm && !activation.equals("relu")) {
      sequential.push_back(new BatchNorm1dImpl(new BatchNormOptions(dim)))
    }
    activation.toLowerCase match {
      case "relu" => sequential.push_back(new ReLUImpl())
      case "sigmoid" =>sequential.push_back(new SigmoidImpl())
      case "tanh" => sequential.push_back(new TanhImpl())
      case "silu" | "swish" =>sequential.push_back(new SiLUImpl())
      case "gelu" => sequential.push_back(new GELUImpl())
      case "prelu" => sequential.push_back(new PReLUImpl())
      case "leaky_relu" | "leakyrelu" => sequential.push_back(new LeakyReLUImpl())
      case "none" | "identity" => sequential.push_back(new IdentityImpl())
      case _ =>sequential.push_back( new ReLUImpl())
    }

//    sequential.push_back(createActivation(activation))

    if (dropout > 0) {
      val dropoutLayer = new DropoutImpl(dropout)
      layersRef += dropoutLayer
      sequential.push_back(s"dropout_${layersRef.size}", dropoutLayer)
//      sequential.push_back(new DropoutImpl(dropout))
    }

    prevDim = dim
  }

  // Output layer
  if (outputLayer) {
    sequential.push_back(new LinearImpl(prevDim, outputDim))
  }

  // Move all layers to target device
  if (device != "cpu") {
    val dev = new org.bytedeco.pytorch.Device(device)
    sequential.to(dev, false)
    this.to(dev, false)
  }

  def forward(x: Tensor): Tensor = {
    sequential.forward(x)
  }

  private def createActivation(act: String): Module = {
    act.toLowerCase match {
      case "relu" => new ReLUImpl()
      case "sigmoid" => new SigmoidImpl()
      case "tanh" => new TanhImpl()
      case "silu" | "swish" => new SiLUImpl()
      case "gelu" => new GELUImpl()
      case "prelu" => new PReLUImpl()
      case "leaky_relu" | "leakyrelu" => new LeakyReLUImpl()
      case "none" | "identity" => new IdentityImpl()
      case _ => new ReLUImpl()
    }
  }
}
class MetaLinear(
  inFeatures: Long,
  outFeatures: Long,
  device: String = DeviceSupport.backend
) extends Module {

  private val targetDevice = new Device(device)
  private val linear = new LinearImpl(inFeatures, outFeatures)
  linear.to(targetDevice, false)
  register_module("linear", linear)

  def forward(x: Tensor): Tensor = {
    linear.forward(x).to(targetDevice, ScalarType.Float)
  }

  def forwardFast(x: Tensor, fastWeight: Tensor, fastBias: Tensor): Tensor = {
    forward(x)
  }
}

  private val criticGates: ModuleListImpl = {
    val moduleList = new ModuleListImpl()
    for (i <- 0 until taskNum) {
      val gate = new MetaLinear(embedDim * 2, criticNum, device)
      gate.to(targetDevice, false)
      moduleList.push_back(gate)
    }
    moduleList
  }


   val criticGateOut = criticGates.get(taskIdx).asInstanceOf[MetaLinear].forward(criticGateInput)

--- MetaHeac Benchmark ---
Exception in thread "main" java.lang.ClassCastException: class org.bytedeco.pytorch.Module cannot be cast to class torchrec.basic.layers.MLP (org.bytedeco.pytorch.Module and torchrec.basic.layers.MLP are in unnamed module of loader 'app')
	at torchrec.models.multi_task.MetaHeac.$anonfun$7(MetaHeac.scala:186)
	at torchrec.models.multi_task.MetaHeac.$anonfun$adapted$3(MetaHeac.scala:185)
	at scala.collection.StrictOptimizedIterableOps.strictOptimizedMap(StrictOptimizedIterableOps.scala:102)
	at scala.collection.StrictOptimizedIterableOps.strictOptimizedMap$(StrictOptimizedIterableOps.scala:29)
	at scala.collection.immutable.Range.strictOptimizedMap(Range.scala:61)
	at scala.collection.StrictOptimizedIterableOps.map(StrictOptimizedIterableOps.scala:90)
	at scala.collection.StrictOptimizedIterableOps.map$(StrictOptimizedIterableOps.scala:29)
	at scala.collection.immutable.Range.map(Range.scala:213)
	at torchrec.models.multi_task.MetaHeac.forwardByName(MetaHeac.scala:185)
	at torchrec.trainers.MTLTrainer.fit$$anonfun$1(MTLTrainer.scala:71)
	at scala.runtime.java8.JFunction1$mcVI$sp.apply(JFunction1$mcVI$sp.scala:20)
	at scala.collection.immutable.Range.foreach(Range.scala:256)
	at torchrec.trainers.MTLTrainer.fit(MTLTrainer.scala:49)
	at benchmarks.BenchmarkRunner$.runMultiTaskBenchmark(BenchmarkRunner.scala:1307)
	at benchmarks.BenchmarkRunner$.runMetaHeacBenchmark(BenchmarkRunner.scala:754)
	at benchmarks.BenchmarkRunner$.main(BenchmarkRunner.scala:74)
	at benchmarks.BenchmarkRunner.main(BenchmarkRunner.scala)


```

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions