You could define a specific class method for it and store the shape information as attributes if you are sure they will never change, and use these attributes in the forward method. E.g. something like this should work:
class Self_Attn(nn.Module):
    """ Self attention Layer"""
    def __init__(self, in_dim):
        super().__init__()
        
        # Construct the conv layers
        self.query_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim//2 , kernel_size= 1)
        self.key_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim//2 , kernel_size= 1)
        self.value_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim , kernel_size= 1)
        
        # Initialize gamma as 0
        self.gamma = nn.Parameter(torch.zeros(1))
        self.softmax  = nn.Softmax(dim=-1)

    def calc_shapes(self, x):
        m_batchsize, C, width, height = x.size()
        self.batchsize = m_batchsize
        self.C = C
        self.width = width
        self.height = height

    def forward(self,x):        
        proj_query  = self.query_conv(x).view(self.batchsize, -1, self.width*self.height).permute(0,2,1) # B * N * C
        proj_key =  self.key_conv(x).view(self.batchsize, -1, self.width*self.height) # B * C * N
        energy =  torch.bmm(proj_query, proj_key) # batch matrix-matrix product
        
        attention = self.softmax(energy) # B * N * N
        proj_value = self.value_conv(x).view(self.batchsize, -1, self.width*self.height) # B * C * N
        out = torch.bmm(proj_value, attention.permute(0,2,1)) # batch matrix-matrix product
        out = out.view(self.batchsize, self.C, self.width, self.height) # B * C * W * H
        
        # Add attention weights onto input
        out = self.gamma*out + x
        return out, attention

model = Self_Attn()
model.calc_shapes(input)
output = model(data)

Alternatively, you could of course also calculate this shape information in the __init__ method, if you pass an input to it.