1 /*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #define LOG_TAG "GraphDump"
18
19 #include "GraphDump.h"
20
21 #include "HalInterfaces.h"
22
23 #include <android-base/logging.h>
24 #include <set>
25 #include <iostream>
26 #include <sstream>
27
28 namespace android {
29 namespace nn {
30
31 // class Dumper is a wrapper around an std::ostream (if instantiated
32 // with a pointer to a stream) or around LOG(INFO) (otherwise).
33 //
34 // Send fragments of output to it with operator<<(), as per usual
35 // stream conventions. Unlike with LOG(INFO), there is no implicit
36 // end-of-line. To end a line, send Dumper::endl.
37 //
38 // Example:
39 //
40 // Dumper d(nullptr); // will go to LOG(INFO)
41 // d << "These words are";
42 // d << " all" << " on";
43 // d << " the same line." << Dumper::endl;
44 //
45 namespace {
46 class Dumper {
47 public:
Dumper(std::ostream * outStream)48 Dumper(std::ostream* outStream) : mStream(outStream) { }
49
50 Dumper(const Dumper&) = delete;
51 void operator=(const Dumper&) = delete;
52
53 template <typename T>
operator <<(const T & val)54 Dumper& operator<<(const T& val) {
55 mStringStream << val;
56 return *this;
57 }
58
59 class EndlType { };
60
operator <<(EndlType)61 Dumper& operator<<(EndlType) {
62 if (mStream) {
63 *mStream << mStringStream.str() << std::endl;
64 } else {
65 // TODO: There is a limit of how long a single LOG line
66 // can be; extra characters are truncated. (See
67 // LOGGER_ENTRY_MAX_PAYLOAD and LOGGER_ENTRY_MAX_LEN.) We
68 // may want to figure out the linebreak rules for the .dot
69 // format and try to ensure that we generate correct .dot
70 // output whose lines do not exceed some maximum length.
71 // The intelligence for breaking the lines might have to
72 // live in graphDump() rather than in the Dumper class, so
73 // that it can be sensitive to the .dot format.
74 LOG(INFO) << mStringStream.str();
75 }
76 std::ostringstream empty;
77 std::swap(mStringStream, empty);
78 return *this;
79 }
80
81 static const EndlType endl;
82 private:
83 std::ostream* mStream;
84 std::ostringstream mStringStream;
85 };
86
87 const Dumper::EndlType Dumper::endl;
88 }
89
90
91 // Provide short name for OperandType value.
translate(OperandType type)92 static std::string translate(OperandType type) {
93 switch (type) {
94 case OperandType::FLOAT32: return "F32";
95 case OperandType::INT32: return "I32";
96 case OperandType::UINT32: return "U32";
97 case OperandType::TENSOR_FLOAT32: return "TF32";
98 case OperandType::TENSOR_INT32: return "TI32";
99 case OperandType::TENSOR_QUANT8_ASYMM: return "TQ8A";
100 case OperandType::OEM: return "OEM";
101 case OperandType::TENSOR_OEM_BYTE: return "TOEMB";
102 default: return toString(type);
103 }
104 }
105
106 // If the specified Operand of the specified Model has OperandType
107 // nnType corresponding to C++ type cppType and is of
108 // OperandLifeTime::CONSTANT_COPY, then write the Operand's value to
109 // the Dumper.
110 namespace {
111 template<OperandType nnType, typename cppType>
tryValueDump(Dumper & dump,const Model & model,const Operand & opnd)112 void tryValueDump(Dumper& dump, const Model& model, const Operand& opnd) {
113 if (opnd.type != nnType ||
114 opnd.lifetime != OperandLifeTime::CONSTANT_COPY ||
115 opnd.location.length != sizeof(cppType)) {
116 return;
117 }
118
119 cppType val;
120 memcpy(&val, &model.operandValues[opnd.location.offset], sizeof(cppType));
121 dump << " = " << val;
122 }
123 }
124
graphDump(const char * name,const Model & model,std::ostream * outStream)125 void graphDump(const char* name, const Model& model, std::ostream* outStream) {
126 // Operand nodes are named "d" (operanD) followed by operand index.
127 // Operation nodes are named "n" (operatioN) followed by operation index.
128 // (These names are not the names that are actually displayed -- those
129 // names are given by the "label" attribute.)
130
131 Dumper dump(outStream);
132
133 dump << "// " << name << Dumper::endl;
134 dump << "digraph {" << Dumper::endl;
135
136 // model inputs and outputs
137 std::set<uint32_t> modelIO;
138 for (unsigned i = 0, e = model.inputIndexes.size(); i < e; i++) {
139 modelIO.insert(model.inputIndexes[i]);
140 }
141 for (unsigned i = 0, e = model.outputIndexes.size(); i < e; i++) {
142 modelIO.insert(model.outputIndexes[i]);
143 }
144
145 // model operands
146 for (unsigned i = 0, e = model.operands.size(); i < e; i++) {
147 dump << " d" << i << " [";
148 if (modelIO.count(i)) {
149 dump << "style=filled fillcolor=black fontcolor=white ";
150 }
151 dump << "label=\"" << i;
152 const Operand& opnd = model.operands[i];
153 const char* kind = nullptr;
154 switch (opnd.lifetime) {
155 case OperandLifeTime::CONSTANT_COPY:
156 kind = "COPY";
157 break;
158 case OperandLifeTime::CONSTANT_REFERENCE:
159 kind = "REF";
160 break;
161 case OperandLifeTime::NO_VALUE:
162 kind = "NO";
163 break;
164 default:
165 // nothing interesting
166 break;
167 }
168 if (kind) {
169 dump << ": " << kind;
170 }
171 dump << "\\n" << translate(opnd.type);
172 tryValueDump<OperandType::FLOAT32, float>(dump, model, opnd);
173 tryValueDump<OperandType::INT32, int>(dump, model, opnd);
174 tryValueDump<OperandType::UINT32, unsigned>(dump, model, opnd);
175 if (opnd.dimensions.size()) {
176 dump << "(";
177 for (unsigned i = 0, e = opnd.dimensions.size(); i < e; i++) {
178 if (i > 0) {
179 dump << "x";
180 }
181 dump << opnd.dimensions[i];
182 }
183 dump << ")";
184 }
185 dump << "\"]" << Dumper::endl;
186 }
187
188 // model operations
189 for (unsigned i = 0, e = model.operations.size(); i < e; i++) {
190 const Operation& operation = model.operations[i];
191 dump << " n" << i << " [shape=box";
192 const uint32_t maxArity = std::max(operation.inputs.size(), operation.outputs.size());
193 if (maxArity > 1) {
194 if (maxArity == operation.inputs.size()) {
195 dump << " ordering=in";
196 } else {
197 dump << " ordering=out";
198 }
199 }
200 dump << " label=\"" << i << ": "
201 << toString(operation.type) << "\"]" << Dumper::endl;
202 {
203 // operation inputs
204 for (unsigned in = 0, inE = operation.inputs.size(); in < inE; in++) {
205 dump << " d" << operation.inputs[in] << " -> n" << i;
206 if (inE > 1) {
207 dump << " [label=" << in << "]";
208 }
209 dump << Dumper::endl;
210 }
211 }
212
213 {
214 // operation outputs
215 for (unsigned out = 0, outE = operation.outputs.size(); out < outE; out++) {
216 dump << " n" << i << " -> d" << operation.outputs[out];
217 if (outE > 1) {
218 dump << " [label=" << out << "]";
219 }
220 dump << Dumper::endl;
221 }
222 }
223 }
224 dump << "}" << Dumper::endl;
225 }
226
227 } // namespace nn
228 } // namespace android
229