circt icon indicating copy to clipboard operation
circt copied to clipboard

[Arc] Implement memory initializers

Open fzi-hielscher opened this issue 1 year ago • 2 comments

Following #7480, This PR adds support for initialization of arcilator memories. The code still needs tidying and testing. But, as always, I appreciate early feedback on the overall approach.

Specifically, it adds the arc.initmem.filled and arc.initmem.randomized operations, enabling initialization with a constant value and runtime generated random values respectively. Both ops produce a SSA value of the newly added !arc.memory_initializer<* x *> type, indicating that they can be used to initialize memories irrespective of size and word type. Sadly, there is currently no front or middle end equivalent of these operations, so for the moment they mostly serve as a template showing how initialization can be handled with and without help from the runtime environment.

The memory_initializer value is passed as an optional argument to the arc.memory op. During LowerState it gets moved to the 'initital' pseudo clock-tree and a arc.initialize_memory op is inserted to associate the initializer with the memory's state. Finally, the newly added LowerMemoryInitializers pass converts the initializers to the low level state writes, which, depending on the initializer, may involve invoking the runtime environment with a reference to the storage. This utilizes the also newly added arc.call_environment caller and arc.environment_call callee op pair. Right now they don't do anything special, but I think it makes sense to separate calls to the runtime environment from generic function calls.

Speaking of the runtime environment: As discussed in #7445, the increasing complexity makes implementing it as effectively a header library impractical. I'm still working on restructuring it and it probably doesn't make much sense to land this PR here before that has been done (the new call ops also don't really belong in this PR). In the end, this should allow us to add an initializer op for .mem files, with the static reading and parsing logic stashed away in the runtime library.

fzi-hielscher avatar Aug 28 '24 22:08 fzi-hielscher

Thanks for your comments @uenoku!

I think this representation can be lowered into arc.storage+arc.initial in LowerState and represent the same initialization to InitMemoryFilledOp and InitMemoryFilledOp. I feel this approach would be more extensible and instead of preparing special operations which encode specific kinds of initialization patterns.

An initializer op with a region that allows accessing individual words of the memory is definitely on the list of things I'd want to add. If we could use seq.initial directly that would be even better. My concerns when just using the seq.initial op as you have described it would be:

  • A blanket initialization with constant/random values is, I'd guess, the most common use-case. When applying this to memories which may have a depth in the tens of thousands (maybe even millions, at least for simulation) requiring an equally large ArrayCreate op (and for random initialization a distinct SSA value for every single word) seems excessive to me.
  • I think it would be nice to be able to express sparse initializers and combine them. E.g., a .mem file may not initialize the entire memory. So, as a not entirely unrealistic use-case: Can we do a randomized fill first, then overlay a .mem file and finally manually change a few words?

All of this of course depends on what we can and want to express in the frontends. But based on my implementation here I could imagine a representation of the given example to look something like this:

// Use a random fill as base, producing '!arc.memory_initializer<* x *>'
%0 = arc.initmem.randomized

// Overlay a sparse .mem file, produce a type specialized initializer
%1 = arc.initmem.readmem hex "foo.mem" initial %0 : (!arc.memory_initializer<* x *>') -> !arc.memory_initializer<* x i32>

// Insert a custom value, produce a fully specialized initializer
%2  = arc.initmem.array initial %1 : (!arc.memory_initializer<* x i32>') -> !arc.memory_initializer<1024 x i32> {
  // Provide an argument containing the previous memory contents
  ^bb0 (%array: !hw.array<1024xi32>):
    %addr = hw.constant 0x123 : i10
    %val = hw.constant 0xcafe : i32
    // This op sadly doesn't exist, but can effectively be constructed using slice, create and concat
    %newInit = hw.array_inject %array[%addr], %val : !hw.array<1024xi32>
    arc.initmem.yield %newInit : !hw.array<1024xi32>
}

How useful is this? - I don't know. But I enjoy tinkering with the possibilities here. 😅 I'm in no hurry to land this. As mentioned, there is still plenty to do on the runtime library front. But I want to make sure that my stuff lines up with what you are doing in FIRRTL/Seq. The arc.initmem.array op would be pretty close to your seq.initial op example. So, again thanks for your feedback.

fzi-hielscher avatar Aug 29 '24 13:08 fzi-hielscher

A blanket initialization with constant/random values is, I'd guess, the most common use-case.

I see, that looks reasonable design point. I'm fine with introducing arc.initmem.randomized operation as a starting point.

uenoku avatar Aug 30 '24 15:08 uenoku