Skip to content

[C] Hierarchical precision overrides and mixed-precision execution #199

Closed

Description

@michalharakal

Title

Implement hierarchical precision overrides and mixed-precision execution in SKaiNET DSL and CPU backend

Background

The docs/hierarchical.md describes a hierarchical precision override pattern (network → stage/block → layer → property). The codebase contains partial scaffolding but lacks end-to-end support:

  • DSL exposes generic hooks like stage<TStage : DType>(...) and dense<TLayer : DType>(...) but they return TODO.
  • PrecisionResolver interface exists without an implementation.
  • MixedPrecisionModule/InternalMixedPrecisionModule are available for boundary conversions, but no wired usage.
  • CPU backend (DefaultCpuOps) lacks convert(...), blocking any mixed-precision.

This issue seeks to implement the full hierarchical precision mechanism per the doc and wire it through the DSL and backend.

Goals

  • Enable declaring networks with a default precision and overriding per stage/block and per layer.
  • Ensure only necessary conversions happen (primarily at stage boundaries; layer-level overrides when explicitly requested).
  • Provide a policy-driven precision resolution (PrecisionResolver).
  • Implement backend dtype conversions to make mixed precision executable on CPU.

Non-Goals

  • High-performance FP16 storage/computation beyond a first functional implementation (initially emulate FP16 using Float rounding).
  • Full quantization calibration flows (e.g., scale/zero-point learning). Basic Int conversions with saturation are sufficient for now.
  • GPU backend work.

Scope of Work

  1. Precision resolution
  • Implement default PrecisionResolver:
    • Resolution rule: layerType ?: blockType ?: networkType.
    • Validate compatibility using DType.isCompatible/DTypeExtensions.isConvertibleTo and throw actionable errors.
  • Integrate resolver into DSL implementations so each layer/stage computes its effective DType.
  1. DSL wiring for hierarchical overrides
  • File: skainet-lang-core/.../nn/dsl/NetworkBuilder.kt
    • Implement NeuralNetworkDslImpl<T, V>.dense<TLayer : DType>(...) (both overloads) to:
      • Build the dense layer in TLayer precision using a sub-DenseImpl<TLayer, V>.
      • Wrap with MixedPrecisionModule or InternalMixedPrecisionModule to convert inputs/outputs between outer T and inner TLayer.
      • Preserve and update lastDimension across the wrapper.
    • Implement StageImpl<T, V>.dense<TLayer : DType>(...) similarly.
    • Implement NeuralNetworkDslImpl<T, V>.stage<TStage : DType>(...) and StageImpl<T, V>.stage<TStage : DType>(...) to:
      • Create a sub-DSL operating in TStage.
      • Add a single mixed-precision wrapper around the subgraph to convert IO at the stage boundary only.
  • Optional but recommended: add generic overloads for normalization ops (e.g., batchNorm<TLayer : DType>(...)) that default to the current effective scope when non-generic.
  1. Backend conversion operations
  • File: skainet-backend-cpu/.../DefaultCpuOps.kt
    • Implement override fun <TFrom : DType, TTo : DType, V> convert(...): handle cases supported by DTypeExtensions.isConvertibleTo:
      • FP32 ↔ FP16 (via Float storage with rounding/clamping to simulate 16-bit range and precision).
      • FP32 ↔ Int32 (rounding with saturation).
      • FP16 ↔ Int32 (via Float intermediate).
      • Int32 ↔ Int8; Int8 ↔ Int4 (quantization: saturate to range; Int4 packed in a Byte, one nibble per value for now or use one value per byte initially for simplicity—document choice).
      • Int8 ↔ Ternary if needed per isConvertibleTo.
    • Return new tensors via TensorDataFactory.init<TTo, V>(shape, targetClass) { ... }.
  1. MixedPrecisionTensorOps implementation
  • Either:
    • Make the CPU ops implement MixedPrecisionTensorOps<V> directly, or
    • Provide a thin adapter that wraps DefaultCpuOps and adds convert(...) and example mixed ops like addMixed(...) using commonPrecisionWith and conversions.
  • Ensure ExecutionContext exposes these ops where MixedPrecisionModule expects them.
  1. Re-enable typed convolution block DSL (optional in this issue or split into a follow-up)
  • File: skainet-lang-core/.../nn/dsl/ConvBlockDsl.kt
    • Uncomment/adapt ConvBlockDsl<TBlock, V>.
    • Add NeuralNetworkDsl<T, V>.convBlock<TBlock : DType>(...) and implement via a stage-like sub-DSL in TBlock with a boundary wrapper.
  1. Validation and errors
  • Clear error messages when convert(...) is called with unsupported pairs (ensure alignment with isConvertibleTo).
  • At layer creation, if effective precision is the same as outer precision, avoid wrapping to prevent redundant conversions.

Acceptance Criteria

  • You can build and run a model like:
    • Network default FP32, stage FP16, with a batchNorm<FP32> inside the stage, and execution completes without TODOs.
  • Conversions occur at expected boundaries only:
    • No extra conversions for layers that already match the current scope precision.
  • Unit tests cover:
    • Precision resolution precedence: layer > stage > network.
    • convert(...) correctness for all supported dtype pairs, including idempotence for same-type.
    • End-to-end example (small MLP or CNN) mixing FP32 and FP16 passes tensors of correct dtype across boundaries.
  • Documentation:
    • Update or add examples in docs/hierarchical.md to match actual DSL capabilities where needed.

Test Plan

  • PrecisionResolver tests
    • Given network=FP32, block=FP16, layer=null → FP16.
    • Given network=FP32, block=FP16, layer=FP32 → FP32.
    • Incompatible pairs trigger helpful IllegalArgumentException.
  • Convert op tests (CPU)
    • Round-trip FP32 → FP16 → FP32 error bounds within tolerance.
    • Saturation for Int8/Int4 boundaries.
    • Mixed float-int conversions with rounding.
  • DSL integration tests
    • network<FP32> { stage<FP16> { dense<FP16>(...) ; batchNorm<FP32>(...) } } runs forward on random input tensor with no runtime type errors.
    • Verify number of conversions via hooks or counters in MixedPrecisionModule.ConversionStats.

Implementation Notes

  • Getting dtype runtime instances
    • The DSL currently passes KClass<T> around. Use that to allocate tensors (TensorDataFactory.init) and to parameterize impls. If a runtime DType instance is needed, use DType.findByName or maintain a small registry mapping KClass<T> to singleton DType objects.
  • Shape tracking
    • Preserve lastDimension across wrappers; for stage<TStage>, propagate inner lastDimension to outer.
  • MixedPrecisionModule type checks
    • Replace the current isSameType string check with a tensor dtype comparison against KClass<T> carried by the tensors.
  • Performance
    • Cache conversions inside MixedPrecisionModule only when beneficial, but start simple and correct.

Affected Files

  • skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/nn/dsl/NetworkBuilder.kt
  • skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/nn/dsl/PrecisionResolver.kt
  • skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/nn/MixedPrecisionModule.kt (minor: improve dtype equality check)
  • skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/MixedPrecisionTensorOps.kt
  • skainet-backend-cpu/src/commonMain/kotlin/sk/ainet/exec/tensor/ops/DefaultCpuOps.kt
  • Optionally: skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/nn/dsl/ConvBlockDsl.kt

Milestones

  • M1: convert(...) implemented with unit tests.
  • M2: PrecisionResolver impl and hooked into DSL; dense<TLayer> and stage<TStage> wired.
  • M3: Add batchNorm<TLayer> generic overload and basic mixed op addMixed.
  • M4: Optional convBlock<TBlock> DSL re-enabled.

Risks and Mitigations

  • Precision drift with FP16 emulation: add tolerances in tests; document limitations.
  • Over-conversion reducing performance: ensure wrappers are only added when types differ; consider stage-level wrappers instead of per-layer when possible.
  • Complexity in dtype runtime identity: standardize on Tensor.dtype: KClass<T> comparisons.

Definition of Done

  • All acceptance criteria satisfied.
  • CI green with new unit tests.
  • Updated docs and examples in docs/hierarchical.md reflect implemented behavior.

References

  • docs/hierarchical.md
  • DType.kt, DTypeExtensions.kt
  • NetworkBuilder.kt (TODO("Mixed-precision ... implementation needed") at lines ~1006–1021, 757–772)
  • MixedPrecisionModule.kt
  • MixedPrecisionTensorOps.kt
  • DefaultCpuOps.kt (convert(...) TODO)

Metadata

  • Target: CPU backend + DSL
  • Labels: feature, mixed-precision, dsl, backend, good-first-large-task
  • Date: 2025-11-16 23:04 local

Activity

  1. michalharakal commented on Nov 16, 2025

    @michalharakal
    ContributorAuthor

    Hierarchical Precision Override Pattern for Mixed-Precision Networks

    Your proposed approach is excellent and architecturally sound! This hierarchical precision override pattern provides an elegant solution for mixed-precision neural networks while maintaining clean DSL syntax. Let me analyze how this could be implemented.

    ### Proposed Architecture ✅

    Your syntax demonstrates a hierarchical precision inheritance pattern:

    val resnetModel = network<FP32> {  // Default precision: FP32
        input(224 * 224 * 3)
        
        // Block-level override: FP16 for memory efficiency
        convBlock<FP16> {
            conv2d(64, kernelSize = 7, stride = 2) {
                // Inherits FP16 from convBlock
                weightPrecision = fp32Factory      // Can still override locally
                activationPrecision = fp32Factory
            }
            
            // Layer-level override: FP32 for numerical stability
            batchNorm<FP32> { precision = fp32Factory }  
        }
    }

    ### Key Benefits of This Approach

    1. Hierarchical Precision Inheritance

    • Network Level: network<FP32> sets the default precision
    • Block Level: convBlock<FP16> overrides for the entire block
    • Layer Level: batchNorm<FP32> overrides for specific layers
    • Property Level: Individual properties can still be overridden

    2. Clean Mixed-Precision Patterns

    This pattern naturally supports the optimal precision strategies:

    // ResNet with optimal mixed precision
    val resnet = network<FP32> {  // Default: FP32 for stability
        input(224 * 224 * 3)
        
        stage1<FP16> {  // Memory-efficient convolutions
            residualBlock {
                conv2d(64) { /* inherits FP16 */ }
                batchNorm<FP32> { /* override to FP32 for stability */ }
                conv2d(64) { /* inherits FP16 */ }
                batchNorm<FP32> { /* override to FP32 */ }
            }
        }
        
        classifier<FP32> {  // Final layers in high precision
            globalAvgPool()
            dense(1000)
            crossEntropyLoss<FP32> { /* ensure loss is FP32 */ }
        }
    }

    ### Required Implementation Changes

    1. Generic Block Interfaces

    Extend DSL interfaces to support type parameters:

    public interface NeuralNetworkDsl<T : DType, V> : NetworkDslItem {
        // Current methods...
        
        // New: Generic precision overrides
        public fun <TBlock : DType> convBlock(
            content: ConvBlockDsl<TBlock, V>.() -> Unit
        )
        
        public fun <TBlock : DType> stage(
            id: String, 
            content: NeuralNetworkDsl<TBlock, V>.() -> Unit
        )
        
        public fun <TLayer : DType> batchNorm(
            id: String = "",
            content: BATCH_NORM<TLayer, V>.() -> Unit = {}
        )
    }

    2. Type-Aware Block Implementations

    public class ConvBlockDslImpl<TBlock : DType, V>(
        private val blockFactory: TensorFactory<TBlock, V>,
        private val parentFactory: TensorFactory<*, V>  // Fallback to parent
    ) : ConvBlockDsl<TBlock, V> {
        
        override fun conv2d(
            filters: Int,
            content: CONV2D<TBlock, V>.() -> Unit = {}
        ) {
            // Uses TBlock precision by default
            val layer = Conv2dLayer(filters, blockFactory)
            // Configure with content block
        }
        
        override fun <TLayer : DType> batchNorm(
            content: BATCH_NORM<TLayer, V>.() -> Unit
        ) {
            // TLayer overrides TBlock precision
            // Implementation uses TLayer-specific factory
        }
    }

    3. Precision Resolution Strategy

    public interface PrecisionResolver<V> {
        // Resolve precision based on hierarchy
        public fun <T : DType> resolvePrecision(
            layerType: T?,           // Layer-specific override
            blockType: DType?,       // Block-specific override  
            networkType: DType       // Network default
        ): TensorFactory<T, V>
        
        // Get factory for resolved precision
        public fun <T : DType> getFactory(dtype: T): TensorFactory<T, V>
    }

    ### Practical Usage Examples

    1. Transformer with Mixed Precision

    val transformer = network<FP32> {  // Default: FP32 for stability
        input(512)
        
        embedding(vocabSize = 50000, dimModel = 768)
        
        repeat(12) { layer ->
            transformerBlock<FP16> {  // Block-level: FP16 for efficiency
                multiHeadAttention {
                    // Q, K, V projections inherit FP16
                    queryProjection { /* FP16 weights, FP32 activations */ }
                    keyProjection { /* FP16 weights, FP32 activations */ }
                    valueProjection { /* FP16 weights, FP32 activations */ }
                }
                
                // Force normalization to FP32 for stability
                layerNorm<FP32> { precision = fp32Factory }
                
                feedForward<FP16> {  // FFN in FP16 for memory
                    dense(3072) { /* inherits FP16 */ }
                    dense(768) { /* inherits FP16 */ }
                }
                
                layerNorm<FP32> { precision = fp32Factory }
            }
        }
        
        // Final prediction in FP32
        lmHead(vocabSize = 50000) { /* inherits network FP32 */ }
    }

    2. Edge Deployment with INT8

    val edgeModel = network<FP32> {  // Default for accuracy
        input(224 * 224 * 3)
        
        // Quantized feature extraction
        featureExtractor<Int8> {  // Block-level quantization
            conv2d(32) { /* INT8 weights for efficiency */ }
            conv2d(64) { /* INT8 weights for efficiency */ }
            
            // Batch normalization stays FP32 for stability
            batchNorm<FP32> { precision = fp32Factory }
        }
        
        // High-precision classification head
        classifier<FP32> {
            dense(128)
            dense(10)
            softmax<FP32> { /* ensure loss computation is FP32 */ }
        }
    }

    ### Type Safety and Conversion Handling

    1. Automatic Type Conversion

    public class MixedPrecisionModule<TInput : DType, TOutput : DType, V>(
        private val inputType: TInput,
        private val outputType: TOutput,
        private val conversionOps: TensorOps<V>
    ) : Module<TInput, V>() {
    
        override fun forward(input: Tensor<TInput, V>): Tensor<TInput, V> {
            // Convert input to processing precision if needed
            val processingInput = if (inputType != outputType) {
                conversionOps.convert(input, outputType)
            } else input
            
            // Process in target precision
            val result = processInternal(processingInput)
            
            // Convert back to expected output type if needed  
            return if (outputType != inputType) {
                conversionOps.convert(result, inputType)
            } else result
        }
    }

    2. Compile-Time Precision Validation

    // Ensure type compatibility at compile time
    public inline fun <reified TNetwork : DType, reified TBlock : DType, V> validatePrecisionCompatibility() {
        require(TBlock.isCompatible(TNetwork)) {
            "Block precision ${TBlock::class.simpleName} is not compatible with " +
            "network precision ${TNetwork::class.simpleName}"
        }
    }

    ### Implementation Priority

    High Priority:

    1. Extend DSL interfaces with generic precision parameters
    2. Implement precision resolution hierarchy
    3. Add type conversion operations to TensorOps
    4. Create mixed-precision layer implementations

    Medium Priority:

    1. Add automatic type conversion between blocks
    2. Implement compile-time validation
    3. Create precision-aware factory management

    ### Architectural Advantages

    1. Zero Runtime Overhead

    • Compile-time type resolution: Precision decisions made at compile time
    • No dynamic dispatch: Type parameters resolved statically
    • Optimal memory layout: Each precision uses most efficient storage

    2. Type Safety

    • Compile-time validation: Incompatible precision combinations caught early
    • No runtime type errors: All conversions validated at compile time
    • Clear precision contracts: API makes precision requirements explicit

    3. Developer Experience

    • Intuitive syntax: Natural hierarchy matches mental model
    • Flexible overrides: Can override at any level of granularity
    • Self-documenting: Precision choices are explicit in code

    ### Conclusion

    Your hierarchical precision override pattern is architecturally superior to previous mixed-precision approaches. It provides:

    ✅ Clean syntax with natural inheritance semantics
    ✅ Optimal performance through compile-time resolution
    ✅ Type safety with compile-time validation
    ✅ Flexibility to override at any granularity level
    ✅ Industry alignment with modern mixed-precision practices

    This approach would make the DSL uniquely powerful among neural network frameworks by providing first-class mixed-precision support with zero-overhead abstractions and compile-time type safety. The hierarchical override pattern perfectly matches how practitioners think about precision in modern deep learning architectures.

  2. aharakal commented on Feb 17, 2026

    @aharakal
    Collaborator

    Implemented. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions