TorchSharp

repository·main·Indexed 23 days ago

https://github.com/dotnet/torchsharp

A .NET wrapper library for LibTorch that provides .NET developers with tensor computation and neural network capabilities similar to PyTorch. Part of the .NET Foundation, it offers bundled NuGet packages for CPU and CUDA (Windows/Linux) and mirrors PyTorch's API design and naming conventions for easier porting.

Tokens
14K
Snippets
29
Records
81
Agent score
84%

What's inside TorchSharp

  1. Manage memory in TorchSharp

    main

    TorchSharp provides three approaches to memory management for CPU and GPU tensors. Choosing the right approach depends on your memory constraints:

    1. Automatic disposal via Garbage Collection (GC): The simplest method where tensors are implicitly disposed via .NET finalizers. This is recommended for small models but may fail for large models because the .NET GC is unaware of the memory pressure on CPU or GPU resources.
    2. Explicit disposal: Using using var (C#) or use (F#) to manage the lifetime of every tensor. This is more cumbersome but provides higher memory ceilings and is often required for training on a GPU.
    3. Dispose Scopes: Using torch.NewDisposeScope() (details in subsequent sections) for more structured management.

    General Tips:

    • If experiencing memory issues, try reducing the batch size.
    • Even when using explicit disposal, it is good practice to call GC.Collect() after each mini-batch to catch overlooked or inconveniently managed temporaries.
  2. Manage dynamic submodules with ModuleDict

    main

    Use ModuleDict to hold a dynamic number of submodules in an ordered dictionary. This is useful for parameterizing architecture or selecting layers by name. Like ModuleList, you must manually iterate through or access the dictionary items within your forward() implementation. Since it is an ordered dictionary, you can iterate through it to receive a sequence of tuples containing the submodule name and the module itself.

    private class TestModule1 : Module<Tensor, Tensor>
    {
        public TestModule1()
            : base("TestModule1")
        {
            dict.Add("lin1", Linear(100, 10));
            dict.Add("lin2", Linear(10, 5));
            RegisterComponents();
        }
    
        public override Tensor forward(Tensor input)
        {
            using (var x = submodules["lin1"].forward(input))
                return submodules["lin2"].forward(x);
        }
    
        private ModuleDict submodules = new ModuleDict();
    }
  3. Understand TorchSharp API design and naming conventions

    main

    TorchSharp aims to mirror the PyTorch experience, which leads to several deviations from standard .NET conventions:

    1. Pythonic Naming: Many APIs follow Python/PyTorch naming rather than .NET PascalCase to make it easier to port Python snippets.
    2. Factory Methods and Modules: To allow constructor calls to look like PyTorch (e.g., Conv1d(...)), class declarations are moved to a nested Modules scope. Calling a factory method like Conv1d(...) actually creates an instance of Modules.Conv1d.
    3. Named Parameters: C# uses : for named parameters (e.g., param: value), whereas Python/F# use =. You cannot copy-paste Python code directly into C#.
    4. Enums: Where PyTorch uses strings for configuration, TorchSharp uses proper .NET enum types.
    5. Device Types: The type torch.device is represented as torch.Device in C#, though the factory methods are still lowercase device().
  4. Breaking Changes: Tensor naming and properties

    main

    The following breaking changes were introduced in version 0.95.4:

    • Property Renaming: The Weight and Bias properties on some modules were renamed to lowercase weight and bias.
    • LRScheduler: The LRScheduler.LearningRate property was removed. To log the learning rate, retrieve it directly from the optimizer currently in use.
  5. Manage dynamic submodules with ModuleList

    main

    If you need a dynamic number of submodules (e.g., to parameterize architecture or avoid defining many fields), use ModuleList.

    Important: Unlike Sequential, calling .forward() on a ModuleList will throw an exception. You must manually iterate through the modules within your custom module's forward() implementation.

    private class TestModule1 : Module<Tensor, Tensor>
    {
        public TestModule1()
            : base("TestModule1")
        {
            RegisterComponents();
        }
    
        public override Tensor forward(Tensor input)
        {
            for (int i = 0; i < submodules.Count; i++) {
                input = submodules[i].forward(input); 
            }
            return input;
        }
    
        private ModuleList submodules = new ModuleList(Linear(100, 10), Linear(10, 5));
    }
  6. Optimize memory using Sequential layers

    main
    When building neural networks, prefer using the Sequential layer collection rather than manually passing tensor arguments between layers inside a custom module's forward() method. Sequential is designed to be more efficient at managing the memory of intermediate tensors produced during the forward pass.
  7. Monitor Tensor and PackedSequence statistics separately

    main

    For more granular debugging, especially when working with Recurrent Neural Networks (RNNs), you can drill down into specific statistics via:

    • DisposeScopeManager.Statistics.TensorStatistics
    • DisposeScopeManager.Statistics.PackedSequenceStatistics

    Note that a PackedSequence uses tensors internally. These internal tensors are immediately detached from any active scope, which will increment the DetachedFromScopeCount property. When the PackedSequence is disposed, its internal tensors are also disposed.

  8. Breaking Changes: Optimizer and Scheduler APIs

    main

    The following breaking changes were introduced in version 0.96.3:

    • Optimizers: All APIs to create optimizers now require both parameters() and named_parameters().
    • Parameter Groups: Support for parameter groups was added to most optimizers and LR schedulers.
  9. Implement custom OptimizerState

    main

    When deriving from OptimizerState (starting in v0.101.3), you must follow these two requirements to maintain correct linkage and state management:

    1. Pass Parameter to Base Constructor: You must explicitly pass the related torch.nn.Parameter object to the OptimizerState base constructor.
    2. Implement Initialize: You must implement an Initialize function to set up the state's properties. Since this function can be called for re-initialization, ensure you properly dispose of any existing tensor objects before re-assigning them to prevent memory leaks.
  10. Breaking Changes: Module API updates

    main

    The following breaking changes were introduced in version 0.96.0:

    • Return Types: Module.named_parameters(), parameters(), named_modules(), and named_children() now return IEnumerable instances instead of arrays.
    • Naming Convention: Methods were lower-cased to match PyTorch conventions: Module.Train $\rightarrow$ Module.train and Module.Eval $\rightarrow$ Module.eval.
  11. Define non-trainable buffers in custom modules

    main

    If a module requires tensors that are not updated during back-propagation (e.g., a dropout mask), you should declare them as 'buffers'.

    To ensure they are properly registered for tasks like disk storage, declare them as fields of type Tensor (rather than Parameter). This ensures they are correctly handled when RegisterComponents() is called.

  12. Use DisposeScope for memory management

    main

    TorchSharp uses a DisposeScope system to manage the lifecycle of native resources like Tensors. To avoid memory leaks and ensure proper disposal, you should use torch.NewDisposeScope() instead of manually constructing dispose scopes.

    Key features:

    • Attachment: Use Attach() to add objects to the current scope.
    • Accessing Current Scope: You can retrieve the active scope using torch.CurrentDisposeScope.
    • LIFO Order: Scopes can be disposed out of LIFO (Last-In, First-Out) order.
    • Statistics: DisposeScopeManager.Statistics provides metrics for debugging memory leaks, including DisposedOutsideScopeCount, AttachedToScopeCount, and ThreadTotalLiveCount.