It seems that I have fixed the problem. I replaced the custom conv layer with nn.conv2d and saw what would happen. After that, I found that the customized ReLU layer worked fine. When I tracked the type of data flowing between these layers. I found something different. See:
class ReLU_func(torch.autograd.Function):
    @staticmethod
    def forward(ctx, input):
        ctx.save_for_backward(input)
        output = torch.max(torch.Tensor([0.0]).cuda(), input)
        print("ReLU")
        print(type(output))    
        # it printed <class 'torch.cuda.FloatTensor'>, instead of <class 'torch.autograd.variable.Variable'>
        return output

class ReLU_q(nn.Module):
    def __init__(self):
        super(ReLU_q, self).__init__()

    def forward(self, input):
        return ReLU_func.apply(input)

Then I finally understand what the error message means. The data returned by customized functions should be tensor(rather than Variable), only then can ‘apply’ work normally. Here is the revised code, as you can see:
class Conv2d_func(torch.autograd.Function):

    @staticmethod
    def forward(ctx, input, weight, bias, strides, padding):
        ctx.save_for_backward(input, weight, bias)
        output = Function.conv2d(Variable(input), Variable(weight), Variable(bias),
                                 stride=strides[1], padding=1)
        # up to now the type of 'output' is variable, but tensor needed
        output = output.data.cuda()
        # here 'output' is converted to torch.cuda.FloatTensor, as is requested.
        return output

class Conv2d_q(nn.Module):
    def __init__(self, name, bits, ksize, strides, padding=0, weight_decay=0):
        super(Conv2d_q, self).__init__()
        
        self.bits = bits
        self.ksize = ksize
        self.strides = strides
        self.padding = padding
        self.weight_decay = weight_decay

        # order: (out_channels, in_channels, H, W)
        self.weight = nn.Parameter(torch.Tensor(ksize[3], ksize[0], ksize[1], ksize[2]))
        self.bias = nn.Parameter(torch.Tensor(ksize[3]))
        # initialization
        self.weight.data.uniform_(-0.1, 0.1)
        self.bias.data.uniform_(-0.1, 0.1)

    def forward(self, input):       
        return Conv2d_func.apply(input, self.weight, self.bias, self.strides, self.padding)

Thanks for reply anyway, I still don’t quite understand why it is designed in this way, though.