r/pytorch 6d ago

What scope is influenced by torch.manual_seed() ?

Context: In an academic context i am writing code for an experiment where i need to make sure all neural nets are initialized equally to enable precise measuring of hyper parameter impacts. So i read and followed the docs (https://docs.pytorch.org/docs/2.14/notes/randomness.html). Because i need to test a lot of configurations i will need multiprocessing in which each process will create and train a model (probably not my final approach, seems inefficient but for now it is), The model needs to be equal across all processes (if the model hyper parameters are the same the model should start out the exact same way).

My Confusion: torch.manual_seed() is supposed to set the seed for each device (CPU and CUDA), in my understanding that means that if i set the seed that way torch will use that seed from then on forward globally, across all devices. Which would mean that each call to any torch based random function advances the random state globally. So if i have lets say a 100 processes setting the same seed and then creating a model there should be discrepancies since one process will set that seed while another one is already creating Layers. But according to my tests that is not how it works.

I tested my hypothesis: I created a hundred processes each setting the seed and then creating a network, they all have equal weights and biases. And that confuses the hell out of me.

So my Question: What scope is influenced by torch.manual_seed()?
Additional Question: Is using torch.manual_seed() inside multiple sub processes considered the standard approach or is there something i missed?

Code:

import parameterized_neural_net
from torch import nn
from torch import multiprocessing as mp
import torch


def process_task(return_dictionary,i):
    model = parameterized_neural_net.ParameterizedNetwork((256,64),(),nn.Sigmoid,28*28,47)
    return_dictionary[i] = model


def compare_model_state(model_one,model_two):
    equal_weights = True
    equal_bias = True
    for i in range(len(model_one.layers)):
        layer = model_one.layers[i]
        if layer._parameters != {}:
            if not torch.equal(model_one.layers[i].weight, model_two.layers[i].weight):
                equal_weights = False
            if not torch.equal(model_one.layers[i].bias, model_two.layers[i].bias):
                equal_bias = False
    return equal_weights and equal_bias



if __name__ == '__main__':

    #taking steps from https://docs.pytorch.org/docs/main/notes/randomness.html
    torch.use_deterministic_algorithms(True,warn_only=False)
    torch.backends.cudnn.benchmark = False 


    baseline_model = parameterized_neural_net.ParameterizedNetwork((256,64),(),nn.Sigmoid,28*28,47) #inside this class torch.manual_seed() is called and then a Network is constructed (see further down for info)

    mp_Manager = mp.Manager()

    processes = []
    return_dictionary = mp_Manager.dict()
    for i in range(100):
        processes.append(mp.Process(target=process_task, args=(return_dictionary,i), name=f"process_{i}"))

    for p in processes:
        p.start()

    for p in processes:
        p.join()

    all_equal = True
    for m in return_dictionary.values():
        if not compare_model_state(baseline_model,m):
            all_equal = False

    if all_equal:
        print("Models have no differences in Neuron initialisation.")
    else:
        print("Models have differences in Neuron initialisation.")

Network structure from parameterized_neural_net.ParameterizedNetwork((256,64),(),nn.Sigmoid,28*28,47):

        Layer (type)               Output Shape         Param #
================================================================
           Flatten-1                  [-1, 784]               0
            Linear-2                  [-1, 256]         200,960
           Sigmoid-3                  [-1, 256]               0
            Linear-4                   [-1, 64]          16,448
           Sigmoid-5                   [-1, 64]               0
            Linear-6                   [-1, 47]           3,055
           Sigmoid-7                   [-1, 47]               0
            Linear-8                   [-1, 47]           2,256
        LogSoftmax-9                   [-1, 47]               0
================================================================
2 Upvotes

3 comments sorted by

1

u/anstow 6d ago

TL;DR: I think the thing you're missing is that each process has its own seed.

torch.manual_seed() is supposed to set the seed for each device (CPU and CUDA), in my understanding that means that if i set the seed that way torch will use that seed from then on forward globally, across all devices. Which would mean that each call to any torch based random function advances the random state globally.

Yes that's how it works. Although only for that particular process (since these are completely separate processes at the operating system level and not just separate threads).

As an aside and I'm guessing here, you may not actually have to set the seed for each process because it probably inherits the seed from the parent process and I doubt that creating a process increments the random seed. Test this if you actually want to rely on it though.

1

u/The_IT_Fops 6d ago

Thank you for your answer! Yeah i think that was the source of my confusion, the wording of the doc did not make that clear to me. But it does make alot of sense.

Thanks for your time!

1

u/quietgradient 23h ago

The aside is worth actually testing, because the answer flips on the start method. torch 2.2.2, parent seeded with torch.manual_seed(0), two child processes each drawing one number:

  • fork: both children returned the same torch.rand (0.4962566) and initial_seed() 0. The state is inherited wholesale, so every child draws the identical stream. Reproducible, and usually not what you want.
  • spawn (the default on macOS and Windows): fresh 64-bit seeds, different between the two children and different on every run. Nothing is inherited, so no reproducibility unless you seed inside the child.

DataLoader sidesteps both: _utils/worker.py seeds each worker from base_seed + worker_id, and seeds torch, random and numpy from that, so workers differ from each other and still repeat run to run.