maxas/MaxAs/MaxAs.pm (976 lines of code) (raw):

package MaxAs::MaxAs; require 5.10.0; use strict; use Data::Dumper; use MaxAs::MaxAsGrammar; use File::Spec; use Carp; our $VERSION = '1.06'; # these ops need to be converted from absolute addresses to relative in the sass output by cuobjdump my %relOffset = map { $_ => 1 } qw(BRA SSY CAL PBK PCNT); # these ops use absolute addresses my %absOffset = map { $_ => 1 } qw(JCAL); my %jumpOp = (%relOffset, %absOffset); # These instructions use r0 but do not write to r0 my %noDest = map { $_ => 1 } qw(ST STG STS STL RED); # Map register slots to reuse control codes my %reuseSlots = (r8 => 1, r20 => 2, r39 => 4); # Preprocess and Assemble a source file sub Assemble { my ($file, $include, $doReuse, $nowarn) = @_; my $regMap = {}; $file = Preprocess($file, $include, 0, $regMap); my $vectors = delete $regMap->{__vectors}; my $regBank = delete $regMap->{__regbank}; # initialize cubin counts my $regCnt = 0; my $barCnt = 0; my ($lineNum, @instructs, %labels, $ctrl, @branches, %reuse); # initialize the first control instruction push @instructs, $ctrl = {}; foreach my $line (split "\n", $file) { # keep track of line nums in the physical file $lineNum++; next unless preProcessLine($line); # match an instruction if (my $inst = processAsmLine($line, $lineNum)) { # Save us from crashing the display driver die "It is illegal to set a Read-After-Write dependency on a memory store op (store ops don't write to a register)\n$inst->{inst}\n" if exists $noDest{$inst->{op}} && ($inst->{ctrl} & 0x000e0) != 0x000e0; # track branches/jumps/calls/etc for label remapping push @branches, @instructs+0 if exists $jumpOp{$inst->{op}}; # push the control code onto the control instruction push @{$ctrl->{ctrl}}, $inst->{ctrl}; # now point the instruction to its associated control instruction $inst->{ctrl} = $ctrl; # add the op name and full instruction text push @instructs, $inst; # add a 4th control instruction for every 3 instructions push @instructs, $ctrl = {} if ((@instructs & 3) == 0); } # match a label elsif ($line =~ m'^([a-zA-Z]\w*):') { # map the label name to the index of the instruction about to be inserted $labels{$1} = @instructs+0; } else { die "badly formed line at $lineNum: $line\n"; } } # add the final BRA op and align the number of instructions to a multiple of 8 push @{$ctrl->{ctrl}}, 0x007ff; push @instructs, { op => 'BRA', inst => 'BRA 0xfffff8;' }; while (@instructs & 7) { push @instructs, $ctrl = {} if ((@instructs & 3) == 0); push @{$ctrl->{ctrl}}, 0x007e0; push @instructs, { op => 'NOP', inst => 'NOP;' }; } # remap labels foreach my $i (@branches) { if ($instructs[$i]{inst} !~ m'(\w+);$' || !exists $labels{$1}) { die "instruction has invalid label: $instructs[$i]{inst}"; } $instructs[$i]{jump} = $labels{$1}; if (exists $relOffset{$instructs[$i]{op}}) { $instructs[$i]{inst} =~ s/(\w+);$/sprintf '0x%06x;', (($labels{$1} - $i - 1) * 8) & 0xffffff/e; } else { $instructs[$i]{inst} =~ s/(\w+);$/sprintf '0x%06x;', ($labels{$1} * 8) & 0xffffff/e; } } # calculate optimal register reuse # This effects register bank decisions so do it before analyzing register use foreach my $i (0 .. $#instructs) { #skip control instructions next unless $i & 3; my ($op, $inst, $ctrl) = @{$instructs[$i]}{qw(op inst ctrl)}; my $match = 0; foreach my $gram (@{$grammar{$op}}) { # Apply the rule pattern my $capData = parseInstruct($inst, $gram) or next; if ($doReuse) { # get any vector registers for r0 my @r0 = getVecRegisters($vectors, $capData); # There are 2 reuse slots per register slot # The reuse hash points to most recent instruction index where register was last used in this slot # For writes to a register, clear any reuse opportunity if (@r0 && !exists $noDest{$op}) { foreach my $slot (keys %reuseSlots) { if (my $reuse = $reuse{$slot}) { # if writing with a vector op, clear all linked registers delete $reuse->{$_} foreach @r0; } } } # clear cache if jumping elsewhere %reuse = () if exists $jumpOp{$op}; # only track register reuse for instruction types this works with if ($gram->{type}{reuse}) { foreach my $slot (keys %reuseSlots) { next unless exists $capData->{$slot}; my $r = $capData->{$slot}; next if $r eq 'RZ'; next if $r eq $capData->{r0}; # dont reuse if we're writing this reg in the same instruction my $reuse = $reuse{$slot} ||= {}; # if this register was previously marked for potential reuse if (my $p = $reuse->{$r}) { # flag the previous instruction's ctrl reuse array slot $instructs[$p]{ctrl}{reuse}[($p & 3) - 1] |= $reuseSlots{$slot}; #print "reuse $slot $r $instructs[$p]{inst}\n"; } # list full, delete the oldest elsif (keys %$reuse > 2) { my $oldest = (sort {$reuse->{$a} <=> $reuse->{$b}} keys %$reuse)[0]; delete $reuse->{$oldest}; } # mark the new instruction for potential reuse $reuse->{$r} = $i; } } } # if reuse is disabled then pull value from code. elsif ($gram->{type}{reuse}) { $ctrl->{reuse}[($i & 3) - 1] = genReuseCode($capData); } $match = 1; last; } unless ($match) { print "$_->{rule}\n\n" foreach @{$grammar{$op}}; die "Unable to encode instruction: $inst\n"; } } # Assign registers to requested banks if possible foreach my $r (sort keys %$regBank) { my $bank = $regBank->{$r}; my $avail = $regMap->{$r}; foreach my $pos (0 .. $#$avail) { if ($bank == ($avail->[$pos] & 3)) { # assign it, while removing the assigned register from the pool $regMap->{$r} = 'R' . splice @$avail, $pos, 1; last; } } } # calculate register live times and preferred banks for non-fixed registers. # LiveTime only half implemented... my (%liveTime, %pairedBanks, %reuseHistory); foreach my $i (0 .. $#instructs) { #skip control instructions next unless $i & 3; my ($op, $inst, $ctrl) = @{$instructs[$i]}{qw(op inst ctrl)}; my $match = 0; foreach my $gram (@{$grammar{$op}}) { # Apply the rule pattern my $capData = parseInstruct($inst, $gram) or next; my $reuseType = $gram->{type}{reuse}; # liveTimes and bank conflicts with source operands my (%addReuse, %delReuse); foreach my $slot (qw(r8 r20 r39)) { my $r = $capData->{$slot} or next; next if $r eq 'RZ'; my $liveR = ref $regMap->{$r} ? $r : $regMap->{$r}; # All registers should be written prior to being read.. if (my $liveTime = $liveTime{$liveR}) { # for each read set the current instruction index as the high value $liveTime->[$#$liveTime][1] = $i; push @{$liveTime->[$#$liveTime]}, "$i $inst"; } else { warn "register used without initialization ($r): $inst\n" unless $nowarn; push @{$liveTime{$liveR}}, [$i,$i]; } # Is this register active in the reuse cache? my $slotHist = $reuseHistory{$slot} ||= {}; my $selfReuse = $reuseType ? exists $slotHist->{$r} : 0; #print "IADD3-1: $slot:$r (!$selfReuse && $regMap->{$r})\n" if $op eq 'IADD3'; # If this is an auto reg, look at the open banks. # No need to look at banks if this register is in the reuse cache. if (!$selfReuse && ref $regMap->{$r}) { # Look at other source operands in this instruction and flag what banks are being used foreach my $slot2 (grep {$_ ne $slot && exists $capData->{$_}} qw(r8 r20 r39)) { my $r2 = $capData->{$slot2}; next if $r2 eq 'RZ' || $r2 eq $r; my $slotHist2 = $reuseHistory{$slot2} ||= {}; #print "IADD3-2: $slot:$r $slot2:$r2 (!$reuseType && !$slotHist2->{$r2})\n" if $op eq 'IADD3'; # Dont be concerned with non-reuse type instructions or # If this operand is in the reuse cache, we don't care what bank it's on. if (!$reuseType || !exists $slotHist2->{$r2}) { # if the operand is also an auto-allocated register then link them # Once we choose the bank for one we want to update that choice for the other register. if (ref $regMap->{$r2}) { push @{$pairedBanks{$r}{pairs}}, $r2; $pairedBanks{$r}{banks} ||= []; } # For a fixed register, calculate the bank, flag it, and update the count of banks to avoid. else { my $bank = substr($regMap->{$r2},1) & 3; #print "IADD3-3: $r2:$bank\n" if $op eq 'IADD3'; $pairedBanks{$r}{bnkCnt}++ unless $pairedBanks{$r}{banks}[$bank]++; $pairedBanks{$r}{pairs} ||= []; } # Update the total use count for this register. # This will be the number of times the register is pulled out of the bank. $pairedBanks{$r}{useCnt}++; } } } # update the reuse history so we know which bank conflicts we can ignore. if ($reuseType) { # flag these slots for addition or removal from reuseHistory if ($ctrl->{reuse}[($i & 3) - 1] & $reuseSlots{$slot}) { $addReuse{$slot} = $r; } else { $delReuse{$slot} = $r; } } } # update reuse history after we're done with the instruction (when the flag is actually in effect). # we don't want to updated it in the middle since that can interfere with the checks, $reuseHistory{$_}{$addReuse{$_}} = 1 foreach keys %addReuse; delete $reuseHistory{$_}{$delReuse{$_}} foreach keys %delReuse; # liveTimes for destination operands and vector registers foreach my $r0 (getVecRegisters($vectors, $capData)) { # fixed register mappings can have aliases so use the actual register value for those. my $liveR = ref $regMap->{$r0} ? $r0 : $regMap->{$r0}; # If not writing treat just like a read if (exists $noDest{$op}) { if (my $liveTime = $liveTime{$liveR}) { $liveTime->[$#$liveTime][1] = $i; push @{$liveTime->[$#$liveTime]}, "$i $inst"; } else { warn "register used without initialization ($r0): $inst\n" unless $nowarn; push @{$liveTime{$liveR}}, [$i,$i]; } } # If writing, push a new bracket on this register's stack. elsif (my $liveTime = $liveTime{$liveR}) { if ($i > $liveTime->[$#$liveTime][1]) { push @{$liveTime{$liveR}}, [$i,$i, "$i $inst"]; } } else { # Initialize the liveTime stack for this register. push @{$liveTime{$liveR}}, [$i,$i, "$i $inst"]; } } $match = 1; last; } unless ($match) { print "$_->{rule}\n\n" foreach @{$grammar{$op}}; die "Unable to encode instruction: $inst\n"; } } #print Dumper(\%liveTime); exit(1); # assign unassigned registers # sort by most restricted, then most used, then name foreach my $r (sort { $pairedBanks{$b}{bnkCnt} <=> $pairedBanks{$a}{bnkCnt} || $pairedBanks{$b}{useCnt} <=> $pairedBanks{$a}{useCnt} || $a cmp $b } keys %pairedBanks) { my $banks = $pairedBanks{$r}{banks}; my $avail = $regMap->{$r}; #printf "%10s: (%d,%d) %d,%d,%d,%d, %s\n", $r, $pairedBanks{$r}{bnkCnt}, $pairedBanks{$r}{useCnt}, @{$banks}[0,1,2,3], join ',', @$avail; # Pick a bank with zero or the smallest number of conflicts BANK: foreach my $bank (sort {$banks->[$a] <=> $banks->[$b] || $a <=> $b } (0..3)) { # pick an available register that matches the requested bank foreach my $pos (0 .. $#$avail) { if ($bank == ($avail->[$pos] & 3)) { # assign it, while removing the assigned register from the pool $regMap->{$r} = 'R' . splice @$avail, $pos, 1; # update bank info for any unassigned pair $pairedBanks{$_}{banks}[$bank]++ foreach @{$pairedBanks{$r}{pairs}}; last BANK; } } } } # Now assign any remaining to first available foreach my $r (sort keys %$regMap) { if (ref($regMap->{$r}) eq 'ARRAY') { $regMap->{$r} = 'R' . shift @{$regMap->{$r}}; } } #print map "$regMap->{$_}: $_\n", sort { substr($regMap->{$a},1) <=> substr($regMap->{$b},1) } keys %$regMap; # apply the register mapping and assemble the instructions to op codes foreach my $i (0 .. $#instructs) { #skip control instructions next unless $i & 3; # save the original and replace the register names with numbers $instructs[$i]{orig} = $instructs[$i]{inst}; $instructs[$i]{inst} =~ s/(?<!\.)\b(\w+)\b(?!\[)/ exists($regMap->{$1}) ? $regMap->{$1} : $1 /ge; my ($op, $inst, $ctrl) = @{$instructs[$i]}{qw(op inst ctrl)}; my $match = 0; foreach my $gram (@{$grammar{$op}}) { # Apply the rule pattern my $capData = parseInstruct($inst, $gram) or next; # update the register count foreach my $r (qw(r0 r8 r20 r39)) { next unless exists($capData->{$r}) && $capData->{$r} ne 'RZ'; # get numeric portion of regname my $val = substr $capData->{$r}, 1; my @r0 = getVecRegisters($vectors, $capData); my @r8 = getAddrVecRegisters($vectors, $capData); # smart enough to count vector registers for memory instructions. my $regInc = $r eq 'r0' ? scalar(@r0) || 1 : 1; my $regInc = $r eq 'r8' ? scalar(@r8) || 1 : 1; if ($val + $regInc > $regCnt) { $regCnt = $val + $regInc; #print "$val $regCnt $regInc\n"; } } # update the barrier resource count if ($op eq 'BAR') { if (exists $capData->{i8w4}) { $barCnt = $capData->{i8w4}+1 if $capData->{i8w4}+1 > $barCnt; } # if a barrier value is a register, assume the maximum elsif (exists $capData->{r8}) { $barCnt = 16; } } # Generate the op code. my ($code, $reuse) = genCode($op, $gram, $capData); $instructs[$i]{code} = $code; # cache this for final pass when we want to calculate reuse stats. if ($gram->{type}{reuse}) { $instructs[$i]{caps} = $capData; } # use the parsed value of reuse for non-reuse type instructions else { $ctrl->{reuse}[($i & 3) - 1] = $reuse; } $match = 1; last; } unless ($match) { print "$_->{rule}\n\n" foreach @{$grammar{$op}}; die "Unable to encode instruction: $inst\n"; } } # final pass to piece together control codes my (@codes, %reuseHistory, @exitOffsets, @ctaidOffsets, $ctaidzUsed); foreach my $i (0 .. $#instructs) { # op code if ($i & 3) { push @codes, $instructs[$i]{code}; if ($instructs[$i]{caps}) { # calculate stats on registers registerHealth(\%reuseHistory, $instructs[$i]{ctrl}{reuse}[($i & 3) - 1], $instructs[$i]{caps}, $i * 8, "$instructs[$i]{inst} ($instructs[$i]{orig})", $nowarn); } if ($instructs[$i]{inst} =~ m'EXIT') { push @exitOffsets, (scalar(@codes)-1)*8; } elsif ($instructs[$i]{inst} =~ m'SR_CTAID\.(X|Y|Z)') { push @ctaidOffsets, (scalar(@codes)-1)*8; $ctaidzUsed = 1 if $1 eq 'Z'; } } # control code else { my ($ctrl, $ruse) = @{$instructs[$i]}{qw(ctrl reuse)}; push @codes, ($ctrl->[0] << 0) | ($ctrl->[1] << 21) | ($ctrl->[2] << 42) | # ctrl codes ($ruse->[0] << 17) | ($ruse->[1] << 38) | ($ruse->[2] << 59); # reuse codes } } # return the kernel data return { RegCnt => $regCnt, BarCnt => $barCnt, ExitOffsets => \@exitOffsets, CTAIDOffsets => \@ctaidOffsets, CTAIDZUsed => $ctaidzUsed, ConflictCnt => $reuseHistory{conflicts}, ReuseCnt => $reuseHistory{reuse}, ReuseTot => $reuseHistory{total}, ReusePct => ($reuseHistory{total} ? 100 * $reuseHistory{reuse} / $reuseHistory{total} : 0), KernelData => \@codes, }; } # Useful for testing op code coverage of existing code, extracting new codes and flags sub Test { my ($fh, $printConflicts, $all) = @_; my @instructs; my %reuseHistory; my ($pass, $fail) = (0,0); while (my $line = <$fh>) { my (@ctrl, @reuse); next unless processSassCtrlLine($line, \@ctrl, \@reuse); foreach my $fileReuse (@reuse) { $line = <$fh>; my $inst = processSassLine($line) or next; $inst->{reuse} = $fileReuse; my $fileCode = $inst->{code}; if (exists $relOffset{$inst->{op}}) { # these ops need to be converted from absolute addresses to relative in the sass output by cuobjdump $inst->{inst} =~ s/(0x[0-9a-f]+)/sprintf '0x%06x', ((hex($1) - $inst->{num} - 8) & 0xffffff)/e; } my $match = 0; foreach my $gram (@{$grammar{$inst->{op}}}) { my $capData = parseInstruct($inst->{inst}, $gram) or next; my @caps; # Run in test mode to list what capture groups were captured my ($code, $reuse) = genCode($inst->{op}, $gram, $capData, \@caps); # Detect register bank conflicts but only for reuse type instructions. # If a bank conflict is avoided by a reuse flag then ignore it. registerHealth(\%reuseHistory, $reuse, $capData, $inst->{num}, $printConflicts ? $inst->{inst} : '') if $gram->{type}{reuse}; $inst->{caps} = join ', ', sort @caps; $inst->{codeDiff} = $fileCode ^ $code; $inst->{reuseDiff} = $fileReuse ^ $reuse; # compare calculated and file values if ($code == $fileCode && $reuse == $fileReuse) { $inst->{grade} = 'PASS'; push @instructs, $inst if $all; $pass++; } else { $inst->{grade} = 'FAIL'; push @instructs, $inst; $fail++; } $match = 1; last; } unless ($match) { $inst->{grade} = 'FAIL'; $inst->{codeDiff} = $fileCode; $inst->{reuseDiff} = $fileReuse; push @instructs, $inst; $fail++; } } } my %maxLen; foreach (@instructs) { $maxLen{$_->{op}} = length($_->{ins}) if length($_->{ins}) > $maxLen{$_->{op}}; } my ($lastOp, $template); foreach my $inst (sort { $a->{op} cmp $b->{op} || $a->{codeDiff} <=> $b->{codeDiff} || $a->{reuseDiff} <=> $b->{reuseDiff} || $a->{ins} cmp $b->{ins} } @instructs) { if ($lastOp ne $inst->{op}) { $lastOp = $inst->{op}; $template = "%s 0x%016x %x 0x%016x %x %5s%-$maxLen{$lastOp}s %s\n"; printf "\n%s %-18s %s %-18s %s %-5s%-$maxLen{$lastOp}s %s\n", qw(Grad OpCode R opCodeDiff r Pred Instruction Captures); } printf $template, @{$inst}{qw(grade code reuse codeDiff reuseDiff pred ins caps)}; } my $reusePct = $reuseHistory{total} ? 100 * $reuseHistory{reuse} / $reuseHistory{total} : 0; printf "\nRegister Bank Conflicts: %d, Reuse: %.1f% (%d/%d)\nOp Code Coverage Totals: Pass: $pass Fail: $fail\n", $reuseHistory{conflicts}, $reusePct, $reuseHistory{reuse}, $reuseHistory{total}; return $fail; } # Convert cuobjdump sass to the working format sub Extract { my ($in, $out, $params) = @_; my %paramMap; my %constants = ( blockDimX => 'c[0x0][0x8]', blockDimY => 'c[0x0][0xc]', blockDimZ => 'c[0x0][0x10]', gridDimX => 'c[0x0][0x14]', gridDimY => 'c[0x0][0x18]', gridDimZ => 'c[0x0][0x1c]', ); print $out "<CONSTANT_MAPPING>\n"; foreach my $const (sort keys %constants) { print $out " $const : $constants{$const}\n"; $paramMap{$constants{$const}} = $const; } print $out "\n"; foreach my $p (@$params) { my ($ord,$offset,$size,$align) = split ':', $p; if ($size > 4) { my $num = 0; $offset = hex $offset; while ($size > 0) { my $param = sprintf 'param_%d[%d]', $ord, $num; my $const = sprintf 'c[0x0][0x%x]', $offset; $paramMap{$const} = $param; print $out " $param : $const\n"; $size -= 4; $offset += 4; $num += 1; } } else { my $param = sprintf 'param_%d', $ord; my $const = sprintf 'c[0x0][%s]', $offset; $paramMap{$const} = $param; print $out " $param : $const\n"; } } print $out "</CONSTANT_MAPPING>\n\n"; my %labels; my $labelnum = 1; my @data; FILE: while (my $line = <$in>) { my (@ctrl, @ruse); next unless processSassCtrlLine($line, \@ctrl, \@ruse); CTRL: foreach my $ctrl (@ctrl) { $line = <$in>; my $inst = processSassLine($line) or next CTRL; # Convert branch/jump/call addresses to labels if (exists($jumpOp{$inst->{op}}) && $inst->{ins} =~ m'(0x[0-9a-f]+)') { my $target = hex($1); # skip the final BRA and stop processing the file last FILE if $inst->{op} eq 'BRA' && ($target == $inst->{num} || $target == $inst->{num}-8); # check to see if we've already generated a label for this target address my $label = $labels{$target}; unless ($label) { # generate a label name and cache it $label = $labels{$target} = "TARGET$labelnum"; $labelnum++; } # replace address with name $inst->{ins} =~ s/(0x[0-9a-f]+)/$label/; } $inst->{ins} =~ s/(c\[0x0\])\s*(\[0x[0-9a-f]+\])/ $paramMap{$1 . $2} || $1 . $2 /eg; $inst->{ctrl} = printCtrl($ctrl); push @data, $inst; } } # make a second pass now that we have the complete instruction address to label mapping foreach my $inst (@data) { print $out "$labels{$inst->{num}}:\n" if exists $labels{$inst->{num}}; printf $out "%s %5s%s\n", @{$inst}{qw(ctrl pred ins)}; } } my $CommentRe = qr'^[\t ]*<COMMENT>.*?^\s*</COMMENT>\n?'ms; my $IncludeRe = qr'^[\t ]*<INCLUDE\s+file="([^"]+)"\s*/?>\n?'ms; my $CodeRe = qr'^[\t ]*<CODE(\d*)>(.*?)^\s*<\/CODE\1>\n?'ms; my $ConstMapRe = qr'^[\t ]*<CONSTANT_MAPPING>(.*?)^\s*</CONSTANT_MAPPING>\n?'ms; my $RegMapRe = qr'^[\t ]*<REGISTER_MAPPING>(.*?)^\s*</REGISTER_MAPPING>\n?'ms; my $ScheduleRe = qr'^[\t ]*<SCHEDULE_BLOCK>(.*?)^\s*</SCHEDULE_BLOCK>\n?'ms; my $InlineRe = qr'\[(\+|\-)(.+?)\1\]'ms; sub IncludeFile { my ($file, $include) = @_; my ($vol,$dir,$name) = File::Spec->splitpath($file); local $/; my $fh; if (!open $fh, $file) { open $fh, File::Spec->catpath(@$include, $name) or die "Could not open file for INCLUDE: $file ($!)\n"; } my $content = <$fh>; close $fh; return $content; } sub Preprocess { my ($file, $include, $debug, $regMap) = @_; my $constMap = {}; my $removeRegMap; if ($regMap) { $removeRegMap = 1; } else { $regMap = {}; } # include nested files 1 while $file =~ s|$IncludeRe| IncludeFile($1, $include) |eg; # Strip out comments $file =~ s|$CommentRe||g; # Execute the CODE sections (old way to run code, to be deprecated) 1 while $file =~ s|$CodeRe| my $out = eval "package MaxAs::MaxAs::CODE; $2"; $@ ? die("CODE:\n$2\n\nError: $@\n") : $out |eg; # Execute the inline code (new way) $file =~ s|$InlineRe| my ($type, $code) = ($1, $2); my $out = eval "package MaxAs::MaxAs::CODE; $code"; $@ ? die("CODE:\n$code\n\nError: $@\n") : $type eq "+" ? $out : "" |eg; #Pull in the constMap $file =~ s/$ConstMapRe/ setConstMap($constMap, $1) /eg; my @newFile; foreach my $line (split "\n", $file) { # skip comments if ($line !~ m'^\s*(?:#|//).*') { $line =~ s|(\w+(?:\[\d+\])?)| exists $constMap->{$1} ? $constMap->{$1} : $1 |eg; } push @newFile, $line; } $file = join "\n", @newFile; # Pull in the reg map first as the Scheduler will need it to handle vector instructions # Remove the regmap if we're going on to assemble $file =~ s/$RegMapRe/ setRegisterMap($regMap, $1); $removeRegMap ? '' : $& /eg; # Pick out the SCHEDULE_BLOCK sections my @schedBlocks = $file =~ /$ScheduleRe/g; # Schedule them foreach my $i (0 .. $#schedBlocks) { # XMAD macros should only appear in SCHEDULE_BLOCKs $schedBlocks[$i] = replaceXMADs($schedBlocks[$i]); $schedBlocks[$i] = Scheduler($schedBlocks[$i], $i+1, $regMap, $debug); } # Replace the results $file =~ s|$ScheduleRe| shift @schedBlocks |eg; return $file; } # break the registers down into source and destination categories for the scheduler my %srcReg = map { $_ => 1 } qw(r8 r20 r39 p12 p29 p39 X); my %destReg = map { $_ => 1 } qw(r0 p0 p3 p45 p48 CC); my %regops = (%srcReg, %destReg); my @itypes = qw(class lat rlat tput dual); sub Scheduler { my ($block, $blockNum, $regMap, $debug) = @_; my $vectors = $regMap->{__vectors}; my $lineNum = 0; my (@instructs, @comments, $ordered, $first); foreach my $line (split "\n", $block) { # keep track of line nums in the physical file $lineNum++; unless (preProcessLine($line)) { push @comments, $line if $line =~ m'\S'; next; } # match an instruction if (my $inst = processAsmLine($line, $lineNum)) { # if the first instruction in the block is waiting on a dep, it should go first. $inst->{first} = !$first++ && ($inst->{ctrl} & 0x1f800) ? 0 : 1; # if the instruction has a stall of zero set, it's meant to be last (to mesh with next block) #$inst->{first} = $inst->{ctrl} & 0x0000f ? 1 : 2; $inst->{exeTime} = 0; $inst->{order} = $ordered++ if $ordered; $inst->{force_stall} = $inst->{ctrl} & 0xf if $inst->{comment} =~ m'FORCE'; push @instructs, $inst; } # match a label elsif ($line =~ m'^([a-zA-Z]\w*):') { die "SCHEDULE_BLOCK's cannot contain labels. block: $blockNum line: $lineNum\n"; } # open an ORDERED block elsif ($line =~ m'^<ORDERED>') { die "you cannot use nested <ORDERED> tags" if $ordered; $ordered = 1; } # close an ORDERED block elsif ($line =~ m'^</ORDERED>') { die "missing opening <ORDERED> for closing </ORDERED> tag" if !$ordered; $ordered = 0; } else { die "badly formed line at block: $blockNum line: $lineNum: $line\n"; } } my (%writes, %reads, @ready, @schedule, $orderedParent); # assemble the instructions to op codes foreach my $instruct (@instructs) { my $match = 0; foreach my $gram (@{$grammar{$instruct->{op}}}) { my $capData = parseInstruct($instruct->{inst}, $gram) or next; my (@dest, @src); # copy over instruction types for easier access @{$instruct}{@itypes} = @{$gram->{type}}{@itypes}; $instruct->{dualCnt} = $instruct->{dual} ? 1 : 0; # A predicate prefix is treated as a source reg push @src, $instruct->{predReg} if $instruct->{pred}; # Handle P2R and R2P specially if ($instruct->{op} =~ m'P2R|R2P' && $capData->{i20w7}) { my $list = $instruct->{op} eq 'R2P' ? \@dest : \@src; my $mask = hex($capData->{i20w7}); foreach my $p (0..6) { if ($mask & (1 << $p)) { push @$list, "P$p"; } # make this instruction dependent on any predicates it's not setting # this is to prevent a race condition for any predicate sets that are pending elsif ($instruct->{op} eq 'R2P') { push @src, "P$p"; } } # These instructions can't be dual issued $instruct->{nodual} = 1; } # Populate our register source and destination lists, skipping any zero or true values foreach my $operand (grep { exists $regops{$_} } sort keys %$capData) { # figure out which list to populate my $list = exists($destReg{$operand}) && !exists($noDest{$instruct->{op}}) ? \@dest : \@src; # Filter out RZ and PT my $badVal = substr($operand,0,1) eq 'r' ? 'RZ' : 'PT'; if ($capData->{$operand} ne $badVal) { # add the value to list with the correct prefix push @$list, $operand eq 'r0' ? map(getRegNum($regMap, $_), getVecRegisters($vectors, $capData)) : $operand eq 'r8' ? map(getRegNum($regMap, $_), getAddrVecRegisters($vectors, $capData)) : $operand eq 'CC' ? 'CC' : $operand eq 'X' ? 'CC' : getRegNum($regMap, $capData->{$operand}); } } $instruct->{const} = 1 if exists($capData->{c20}) || exists($capData->{c39}); # Find Read-After-Write dependencies foreach my $src (grep { exists $writes{$_} } @src) { # Memory operations get delayed access to registers but not to the predicate my $regLatency = $src eq $instruct->{predReg} ? 0 : $instruct->{rlat}; # the parent should be the most recently added dest op to the stack foreach my $parent (@{$writes{$src}}) { # add this instruction as a child of the parent # set the edge to the total latency of reg source availability #print "R $parent->{inst}\n\t\t$instruct->{inst}\n"; my $latency = $src =~ m'^P\d' ? 13 : $parent->{lat}; push @{$parent->{children}}, [$instruct, $latency - $regLatency]; $instruct->{parents}++; # if the destination was conditionally executed, we also need to keep going back till it wasn't last unless $parent->{pred}; } } # Find Write-After-Read dependencies foreach my $dest (grep { exists $reads{$_} } @dest) { # Flag this instruction as dependent to any previous read foreach my $reader (@{$reads{$dest}}) { # no need to stall for these types of dependencies #print "W $reader->{inst} \t\t\t $instruct->{inst}\n"; push @{$reader->{children}}, [$instruct, 0]; $instruct->{parents}++; } # Once dependence is marked we can clear out the read list (unless this write was conditional). # The assumption here is that you would never want to write out a register without # subsequently reading it in some way prior to writing it again. delete $reads{$dest} unless $instruct->{pred}; } # Enforce instruction ordering where requested if ($instruct->{order}) { if ($orderedParent && $instruct->{order} > $orderedParent->{order}) { push @{$orderedParent->{children}}, [$instruct, 0]; $instruct->{parents}++; } $orderedParent = $instruct; } elsif ($orderedParent) { $orderedParent = 0; } # For a dest reg, push it onto the write stack unshift @{$writes{$_}}, $instruct foreach @dest; # For a src reg, push it into the read list push @{$reads{$_}}, $instruct foreach @src; # if this instruction has no dependencies it's ready to go push @ready, $instruct if !exists $instruct->{parents}; $match = 1; last; } die "Unable to recognize instruction at block: $blockNum line: $lineNum: $instruct->{inst}\n" unless $match; } %writes = (); %reads = (); if (@ready) { # update dependent counts for sorting hueristic my $readyParent = { children => [ map { [ $_, 1 ] } @ready ], inst => "root" }; countUniqueDescendants($readyParent, {}); updateDepCounts($readyParent, {}); # sort the initial ready list @ready = sort { $a->{first} <=> $b->{first} || $b->{deps} <=> $a->{deps} || $a->{dualCnt} <=> $b->{dualCnt} || $a->{lineNum} <=> $b->{lineNum} } @ready; if ($debug) { print "0: Initial Ready List State:\n\tf,ext,stl,mix,dep,lin, inst\n"; printf "\t%d,%3s,%3s,%3s,%3s,%3s,%3s, %s\n", @{$_}{qw(first exeTime stall dualCnt mix deps lineNum inst)} foreach @ready; } } # Process the ready list, adding new instructions to the list as we go. my $clock = 0; while (my $instruct = shift @ready) { my $stall = $instruct->{stall}; # apply the stall to the previous instruction if (@schedule && $stall < 16) { my $prev = $schedule[$#schedule]; $stall = $prev->{force_stall} if $prev->{force_stall} > $stall; # if stall is greater than 4 then also yield # the yield flag is required to get stall counts 12-15 working correctly. $prev->{ctrl} &= $stall > 4 ? 0x1ffe0 : 0x1fff0; $prev->{ctrl} |= $stall; $clock += $stall; } # For stalls bigger than 15 we assume the user is managing it with a barrier else { $instruct->{ctrl} &= 0x1fff0; $instruct->{ctrl} |= 1; $clock += 1; } print "$clock: $instruct->{inst}\n" if $debug; # add a new instruction to the schedule push @schedule, $instruct; # update each child with a new earliest execution time if (my $children = $instruct->{children}) { foreach (@$children) { my ($child, $latency) = @$_; # update the earliest clock value this child can safely execute my $earliest = $clock + $latency; $child->{exeTime} = $earliest if $child->{exeTime} < $earliest; print "\t\t$child->{exeTime},$child->{parents} $child->{inst}\n" if $debug; # decrement parent count and add to ready queue if none remaining. push @ready, $child if --$child->{parents} < 1; } delete $instruct->{children}; } # update stall and mix values in the ready queue on each iteration foreach my $ready (@ready) { # calculate how many instructions this would cause the just added instruction to stall. $stall = $ready->{exeTime} - $clock; $stall = 1 if $stall < 1; # if using the same compute resource as the prior instruction then limit the throughput if ($ready->{class} eq $instruct->{class}) { $stall = $ready->{tput} if $stall < $ready->{tput}; } # dual issue with a simple instruction (tput <= 2) # can't dual issue two instructions that both load a constant elsif ($ready->{dual} && !$instruct->{dual} && $instruct->{tput} <= 2 && !$instruct->{nodual} && $stall == 1 && $ready->{exeTime} <= $clock && !($ready->{const} && $instruct->{const})) { $stall = 0; } $ready->{stall} = $stall; # add an instruction class mixing huristic that catches anything not handled by the stall $ready->{mix} = $ready->{class} ne $instruct->{class} || 0; $ready->{mix} = 2 if $ready->{mix} && $ready->{op} eq 'R2P'; } # sort the ready list by stall time, mixing huristic, dependencies and line number @ready = sort { $a->{first} <=> $b->{first} || $a->{stall} <=> $b->{stall} || $a->{dualCnt} <=> $b->{dualCnt} || $b->{mix} <=> $a->{mix} || $b->{deps} <=> $a->{deps} || $a->{lineNum} <=> $b->{lineNum} } @ready; if ($debug) { print "\tf,ext,stl,duc,mix,dep,lin, inst\n"; printf "\t%d,%3s,%3s,%3s,%3s,%3s,%3s, %s\n", @{$_}{qw(f exeTime stall dualCnt mix deps lineNum inst)} foreach @ready; } foreach my $ready (@ready) { $ready->{dualCnt} = 0 if $ready->{dualCnt} && $ready->{stall} == 1; } } my $out; #$out .= "$_\n" foreach @comments; $out .= join('', printCtrl($_->{ctrl}), @{$_}{qw(space inst comment)}, "\n") foreach @schedule; return $out; } sub setConstMap { my ($constMap, $constMapText) = @_; foreach my $line (split "\n", $constMapText) { # strip leading space $line =~ s|^\s+||; # strip comments $line =~ s{(?:#|//).*}{}; # strip trailing space $line =~ s|\s+$||; # skip blank lines next unless $line =~ m'\S'; my ($name, $value) = split '\s*:\s*', $line; $constMap->{$name} = $value; } return; } sub setRegisterMap { my ($regMap, $regmapText) = @_; my $vectors = $regMap->{__vectors} ||= {}; my $regBank = $regMap->{__regbank} ||= {}; my %aliases; foreach my $line (split "\n", $regmapText) { # strip leading space $line =~ s|^\s+||; # strip comments $line =~ s{(?:#|//).*}{}; # strip trailing space $line =~ s|\s+$||; # skip blank lines next unless $line =~ m'\S'; my $auto = $line =~ /~/; my $share = $line =~ /=/; my ($regNums, $regNames) = split '\s*[:~=]\s*', $line; my (@numList, @nameList, %vecAliases); foreach my $num (split '\s*,\s*', $regNums) { my ($start, $stop) = split '\s*\-\s*', $num; die "REGISTER_MAPPING Error: Bad register number or range: $num\nLine: $line\nFull Context:\n$regmapText\n" if grep m'\D', $start, $stop; push @numList, ($start .. $stop||$start); } foreach my $fullName (split '\s*,\s*', $regNames) { if ($fullName =~ m'^(\w+)<((?:\d+(?:\s*\-\s*\d+)?\s*\|?\s*)+)>(\w*)(?:\[([0-3])\])?$') { my ($name1, $name2, $bank) = ($1, $3, $4); foreach (split '\s*\|\s*', $2) { my ($start, $stop) = split '\s*\-\s*'; foreach my $r (map "$name1$_$name2", $start .. $stop||$start) { # define an alias for use in vector instructions that omits the number portion $aliases{$r} = "$name1$name2" unless exists $aliases{$r}; push @nameList, $r; $regBank->{$r} = $bank if $auto && defined $bank; warn "Cannot request a bank for a fixed register range: $fullName\n" if !$auto && defined $bank; } } } elsif ($fullName =~ m'^(\w+)(?:\[([0-3])\])?$') { push @nameList, $1; $regBank->{$1} = $2 if $auto && defined $2; warn "Cannot request a bank for a fixed register range: $fullName\n" if !$auto && defined $2; } else { die "Bad register name: '$fullName' at: $line\n"; } } die "Missmatched register mapping at: $line\n" if !$share && @numList < @nameList; die "Missmatched register mapping at: $line\n" if $share && @numList > 1; # detect if this list is monotonically ascending with no gaps my $i = 0; while ($i < $#numList-1) { last if $numList[$i] + 1 != $numList[$i+1]; $i++; } my $ascending = $i+1 == $#numList; foreach my $n (0..$#nameList) { die "register defined twice: $nameList[$n]" if exists $regMap->{$nameList[$n]}; if ($auto) { # assign possible values to be assigned on assembly $regMap->{$nameList[$n]} = \@numList; } elsif ($share) { # each name shares the same single register $regMap->{$nameList[$n]} = 'R' . $numList[0]; } else { $regMap->{$nameList[$n]} = 'R' . $numList[$n]; # flag any even register as a potential vector if ($ascending && ($numList[$n] & 1) == 0) { # constrain potential range to vector alignment my $end = $n + ($numList[$n] & 2 || $n + 3 > $#nameList ? 1 : 3); if ($end <= $#nameList) { $vectors->{$nameList[$n]} = [ @nameList[$n .. $end] ]; #setup an alias for the base name without the number if (exists $aliases{$nameList[$n]} && !exists $regMap->{$aliases{$nameList[$n]}}) { $regMap->{$aliases{$nameList[$n]}} = $regMap->{$nameList[$n]}; $vectors->{$aliases{$nameList[$n]}} = $vectors->{$nameList[$n]}; delete $aliases{$nameList[$n]}; } } } } } } #print Dumper($regMap); exit(1); } sub preProcessLine { # strip leading space $_[0] =~ s|^\s+||; # preserve comment but check for emptiness my $val = shift; # strip comments $val =~ s{(?:#|//).*}{}; # skip blank lines return $val =~ m'\S'; } # traverse the graph and count total descendants per node. # only count unique nodes (by lineNum) sub countUniqueDescendants { my ($node, $edges) = @_; #print "P:$node->{inst}\n"; if (my $children = $node->{children}) { foreach my $child (grep $_->[1], @$children) # skip WaR deps and traversed edges { next if $edges->{"$node->{lineNum}^$child->[0]{lineNum}"}++; $node->{deps}{$_}++ foreach countUniqueDescendants($child->[0], $edges); } foreach my $child (grep !$_->[1], @$children) # WaR deps { next if $edges->{"$node->{lineNum}^$child->[0]{lineNum}"}++; 1 foreach countUniqueDescendants($child->[0], $edges); } } else { return $node->{lineNum}; } return ($node->{lineNum}, keys %{$node->{deps}}); } # convert hash to count for easier sorting. sub updateDepCounts { my ($node, $edges) = @_; #warn "$node->{inst}\n"; if (my $children = $node->{children}) { foreach my $child (@$children) { next if $edges->{"$node->{lineNum}^$child->[0]{lineNum}"}++; updateDepCounts($child->[0], $edges); } } $node->{deps} = ref $node->{deps} ? keys %{$node->{deps}} : $node->{deps}+0; } # Detect register bank conflicts and calculate reuse stats sub registerHealth { my ($reuseHistory, $reuseFlags, $capData, $instAddr, $inst, $nowarn) = @_; my (@banks, @conflicts); foreach my $slot (qw(r8 r20 r39)) { my $r = $capData->{$slot} or next; next if $r eq 'RZ'; my $slotHist = $reuseHistory->{$slot} ||= {}; $reuseHistory->{total}++; # if this register is in active reuse then ignore for bank conflict checking. if (exists $slotHist->{$r}) { $reuseHistory->{reuse}++; } else { # extract number from reg and take the modulo-4 value. This is the bank id. my $bank = substr($r,1) & 3; # check for conflict if ($banks[$bank] && $banks[$bank] ne $r) { push @conflicts, $banks[$bank] if !@conflicts; push @conflicts, $r; $reuseHistory->{conflicts}++; } $banks[$bank] = $r; } # update the history if ($reuseFlags & $reuseSlots{$slot}) { $slotHist->{$r} = 1; } else { delete $slotHist->{$r}; } } if ($inst && @conflicts && !$nowarn) { printf "CONFLICT at 0x%04x (%s): $inst\n", $instAddr, join(',', @conflicts); } return scalar @conflicts; } 1; __END__ =head1 NAME MaxAs::MaxAs - Assembler for NVIDIA Maxwell architecture =head1 SYNOPSIS maxas.pl [opts] =head1 DESCRIPTION See the documentation at: https://github.com/NervanaSystems/maxas =head1 SEE ALSO See the documentation at: https://github.com/NervanaSystems/maxas =head1 AUTHOR Scott Gray, E<lt>sgray@nervanasys.com<gt> =head1 COPYRIGHT AND LICENSE The MIT License (MIT) Copyright (c) 2014 Scott Gray Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =cut